Pythonでゼロ埋めなしの数字の文字列リストをソート

Pythonでリストなどを昇順・降順にソートする(並べ替える)にはsort()メソッドやsorted()関数を使います。

ここでは、ゼロ埋めされていない数字の文字列のリストをソートする方法について説明します。

sort()とsorted() ゼロ埋めされていない数字の文字列の注意点 引数keyにint()やfloat()を指定 正規表現で文字列中の数値を抽出 文字列中の数値が一つだけの場合 文字列中の数値が複数ある場合 文字列中に数値がない要素もある場合

sort()とsorted()

sort()はリスト型のメソッドで、元のリスト自体がソートされます。

l = [10, 1, 5]

l.sort()
print(l)
# [1, 5, 10]

sorted()は組み込み関数で、ソートされた新たなリストが生成されます。元のリストは変更されません。

l = [10, 1, 5]

print(sorted(l))
# [1, 5, 10]

print(l)
# [10, 1, 5]

デフォルトは昇順。降順にしたい場合は引数reverseをTrueとします。例はsorted()だが、sort()でも同じです。

print(sorted(l, reverse=True))
# [10, 5, 1]

タプルや文字列に対するソートなど、より

ゼロ埋めされていない数字の文字列の注意点

ゼロ埋めされている数字の文字列のリストの場合、特に問題なくソートされます。なお、以降のサンプルコードではsorted()を使うが、sort()でも同じです。

l = ['10', '01', '05']

print(sorted(l))
# ['01', '05', '10']

ゼロ埋めされていない数字の文字列のリストの場合、数値としての大小ではなく文字列を辞書の並びにソートするので、以下のような結果になってしまいます。例えば'10'は'5'より小さいとみなされます。

l = ['10', '1', '5']

print(sorted(l))
# ['1', '10', '5']

引数keyにint()やfloat()を指定

sort()とsorted()では引数keyに関数を指定することで、その関数を適用した結果に対してソートが行われる。 引数keyに文字列を数値に変換するint()やfloat()を指定することで、数値の大小で並べ替えられます。

関数を引数に指定するときは()を書くとエラーになるので気をつけてください。

l = ['10', '1', '5']

print(sorted(l, key=int))
# ['1', '5', '10']

print(sorted(l, key=float))
# ['1', '5', '10']

整数の文字列はint()でもfloat()でも変換可能だが、小数はfloat()を使う必要があります。

l = ['10.0', '1.0', '5.0']

print(sorted(l, key=float))
# ['1.0', '5.0', '10.0']

sort()でも同様に引数keyを指定できます。

l = ['10', '1', '5']

l.sort(key=int)
print(l)
# ['1', '5', '10']

これまでの結果からもわかるように、keyに指定した関数はあくまでもソートの比較のために適用されるのみで、結果は元のまま。上の例では文字列のままで、整数int型や浮動小数点数float型になったりはしない。 int型やfloat型の結果がほしい場合は、リスト内包表記で変換したリストをソートすればOKです。

l = ['10', '1', '5']

print([int(s) for s in l])
# [10, 1, 5]

print(sorted([int(s) for s in l]))
# [1, 5, 10]

正規表現で文字列中の数値を抽出

数字だけの文字列はint()やfloat()を指定するだけでよいが、以下のように文字列中に数値が埋め込まれている場合は注意が必要。

l = ['file10.txt', 'file1.txt', 'file5.txt']

正規表現モジュールreを使って文字列中の数字部分を抽出してから数値に変換します。

文字列中の数値が一つだけの場合

search()でmatchオブジェクトを取得し、group()メソッドでマッチした部分を文字列として取り出す。 正規表現のパターンとして\d+を使います。\dは数字、+は1文字以上の繰り返しを表し、\d+は1文字以上の連続した数字にマッチします。

import re

s = 'file5.txt'

print(re.search(r'\d+', s).group())
# 5

ここではバックスラッシュ\をそのまま書けるようにraw文字列を使っています。

文字列が返されるので数値に変換する場合はint()やfloat()を使います。

print(type(re.search(r'\d+', s).group()))
# <class 'str'>

print(type(int(re.search(r'\d+', s).group())))
# <class 'int'>

これを無名関数(ラムダ式)でsort()やsorted()の引数keyに指定します。

l = ['file10.txt', 'file1.txt', 'file5.txt']

print(sorted(l))
# ['file1.txt', 'file10.txt', 'file5.txt']

print(sorted(l, key=lambda s: int(re.search(r'\d+', s).group())))
# ['file1.txt', 'file5.txt', 'file10.txt']

要素数が少ない場合はあまり気にしなくてもよいが、compile()で正規表現オブジェクトを生成して使用したほうが効率的。

p = re.compile(r'\d+')
print(sorted(l, key=lambda s: int(p.search(s).group())))
# ['file1.txt', 'file5.txt', 'file10.txt']

文字列中の数値が複数ある場合

search()が返すのは最初にマッチした部分のみ。

s = '100file5.txt'

print(re.search(r'\d+', s).group())
# 100

findall()はマッチするすべての部分をリストとして返します。

print(re.findall(r'\d+', s))
# ['100', '5']

print(re.findall(r'\d+', s)[1])
# 5

パターン中の部分を()で囲んでおくと、groups()メソッドで該当部分のみを取り出すこともできます。 例えば、file(\d+)パターンはfileXXXという文字列のXXX部分(数字)を抽出できます。該当部分が一つだけでもタプルを返すので気をつけてください。

print(re.search(r'file(\d+)', s).groups())
# ('5',)

print(re.search(r'file(\d+)', s).groups()[0])
# 5

(\d+).とするとXXX.という文字列のXXX部分(数字)を抽出できます。.にはバックスラッシュが必要。

print(re.search(r'(\d+)\.', s).groups()[0])
# 5

いずれの方法を使っても問題ありません。findall()はシンプルだが、数字部分の個数が要素によってバラバラだと使えないので気をつけてください。

l = ['100file10.txt', '100file1.txt', '100file5.txt']

print(sorted(l, key=lambda s: int(re.findall(r'\d+', s)[1])))
# ['100file1.txt', '100file5.txt', '100file10.txt']

print(sorted(l, key=lambda s: int(re.search(r'file(\d+)', s).groups()[0])))
# ['100file1.txt', '100file5.txt', '100file10.txt']

print(sorted(l, key=lambda s: int(re.search(r'(\d+)\.', s).groups()[0])))
# ['100file1.txt', '100file5.txt', '100file10.txt']

コンパイルする場合も同じです。

p = re.compile(r'file(\d+)')
print(sorted(l, key=lambda s: int(p.search(s).groups()[0])))
# ['100file1.txt', '100file5.txt', '100file10.txt']

文字列中に数値がない要素もある場合

すべての要素の中に数値が含まれていれば問題ないが、そうでない場合はマッチしない場合のケアが必要。 これまでのようにするとエラーとなります。

l = ['file10.txt', 'file1.txt', 'file5.txt', 'file.txt']

# print(sorted(l, key=lambda s:int(re.search(r'\d+', s).group())))
# AttributeError: 'NoneType' object has no attribute 'group'

例えば、以下のような関数を定義します。第一引数に文字列、第二引数に正規表現オブジェクト、第三引数にマッチしない場合の返り値を指定します。

def extract_num(s, p, ret=0):
    search = p.search(s)
    if search:
        return int(search.groups()[0])
    else:
        return ret

結果は以下の通り。groups()を使っているのでパターンには()が必要。

p = re.compile(r'(\d+)')

print(extract_num('file10.txt', p))
# 10

print(extract_num('file.txt', p))
# 0

print(extract_num('file.txt', p, 100))
# 100

第三引数は省略できます。

この関数をsort()やsorted()の引数keyに指定すればOKです。

print(sorted(l, key=lambda s: extract_num(s, p)))
# ['file.txt', 'file1.txt', 'file5.txt', 'file10.txt']

print(sorted(l, key=lambda s: extract_num(s, p, float('inf'))))
# ['file1.txt', 'file5.txt', 'file10.txt', 'file.txt']

数値が含まれていない要素を昇順の最後に持っていきたい場合、適当に大きい数値を返り値としてもよいが、無限大infとしておくとどの値よりも大きくできます。

数値が複数含まれている場合は、正規表現オブジェクトを変更すれば問題ありません。

l = ['100file10.txt', '100file1.txt', '100file5.txt', '100file.txt']

p = re.compile(r'file(\d+)')
print(sorted(l, key=lambda s: extract_num(s, p)))
# ['100file.txt', '100file1.txt', '100file5.txt', '100file10.txt']

print(sorted(l, key=lambda s: extract_num(s, p, float('inf'))))
# ['100file1.txt', '100file5.txt', '100file10.txt', '100file.txt']
Last Updated: 6/26/2019, 10:34:03 PM