【Python】file2よりfile10が先に来る?番号付きファイルを自然順に並べる

PythonのTopに戻る

数字の部分だけ数値として比べる

file1.txt、file2.txt、file10.txtを文字列順に並べると、file10.txtがfile2.txtより先になる。数字も左から文字として比較されるためである。既存のファイル名を変更せず自然な連番順に並べたいなら、名前を文字部分と数字部分に分け、数字部分だけを整数にしたソート用キーを作る。

自然順には一つの絶対的な規格があるわけではない。file2とfile02の扱い、大文字小文字、複数の数字、小数らしい表記などで期待する順序が変わる。以下ではASCII数字の連続を非負整数として扱い、文字部分はcasefold()で比較し、同じキーなら元の名前で決める。この規則をコードと結果の両方で明示する。

比較対象の型が混ざっても壊れないキーにする

example.py

import re


def natural_key(name):
    parts = re.split(r"([0-9]+)", name)
    tokens = tuple(
        (1, int(part)) if re.fullmatch(r"[0-9]+", part)
        else (0, part.casefold())
        for part in parts
    )
    return tokens, name


names = ["file10.txt", "file2.txt", "file02.txt", "file1.txt"]
result = sorted(names, key=natural_key)
assert result == ["file1.txt", "file02.txt", "file2.txt", "file10.txt"]
print(result)

runs = ["run2_part10.csv", "run10_part1.csv", "run2_part2.csv"]
assert sorted(runs, key=natural_key) == ["run2_part2.csv", "run2_part10.csv", "run10_part1.csv"]
assert sorted(["2.txt", "a2.txt"], key=natural_key) == ["2.txt", "a2.txt"]
assert names[0] == "file10.txt"
print(sorted(runs, key=natural_key))

実行結果

['file1.txt', 'file02.txt', 'file2.txt', 'file10.txt']
['run2_part2.csv', 'run2_part10.csv', 'run10_part1.csv']

分割・型タグ・同点の規則を読む

re.split()の数字部分を括弧で囲むと、区切りとして使った数字も結果に残る。file12.txtなら文字列のfile、数字文字列の12、文字列の.txtへ分かれる。数字だけint()へ変換することで、文字列の"10"と"2"ではなく整数の10と2を比較できる。元の名前を返す前に変えているわけではないので、先頭ゼロを含むファイル名もそのまま残る。

各要素に(0, 文字列)または(1, 整数)という型タグを付けている。単純に文字列と整数だけを混ぜたキーでは、名前の形が違うとstrとintを直接比較しようとしてTypeErrorになることがある。タグを最初に比べれば、異なる種類の値を直接比較しない規則にできる。この例では文字部分を数字部分より前に扱う。

file2とfile02は数値部分がどちらも2なので、主要キーは同じになる。最後に元の名前を比較するため、ここではfile02が先になる。入力順を保ちたいなら、返り値をtokensだけにすればPythonの安定ソートが使える。ただし探索時の入力順が未確定なら、同点時の結果も毎回同じとは限らない。用途に合わせた同点の規則が必要である。

番号に見えるものの意味を決める

run2_part10のように数字が二つある場合は、左の番号を先に比べ、同じなら次の番号を比べる。日時や版番号も一見似ているが、区切りごとに異なる意味を持つ形式では専用の解釈が適している。例えば小数点はこの例では文字部分として扱うので、測定値1.10と1.2を実数として比較するキーにはならない。

負数の符号や桁区切りカンマも数値部分へ含めていない。file-2のハイフンを連番の区切りとみなすのか、負数の符号とみなすのかは名前の仕様で決まる。正規表現を変更する前に、望む順序を短いリストで書き出しておくと混乱が少ない。全角数字もこの例では数字として変換しない。

大文字小文字の比較にはcasefold()を使っているが、これは日本語の読み順や自然言語の照合順を作るものではない。最終の同点判定では元の名前を使うので、大文字小文字の違いも順序に影響する。複数のフォルダーをまたぐ場合は、ファイル名だけで比較するか相対パス全体で比較するかも決めておく。

並べ替えとリネームを区別する

sorted()は新しいリストを返し、元のnamesも実ファイル名も変更しない。単に解析順をそろえるなら、この方が名前の付け替えより安全である。リネームが必要な場合は、別途衝突検査と実行計画を用意する。数字が極端に長いなど不自然な名前を扱う場合は、入力長を制限しておくと整数変換やソートの負荷を抑えやすい。

実行環境と関連情報

掲載コードはLinux上のCPython 3.12.14で実行した。OS固有のファイル操作や対話環境の違いは、本文に記した条件に従って扱う。

関連:フォルダー内の対象ファイルだけ集める:globと除外条件 / 大量ファイルの名前を安全に変える:事前確認と衝突検出

PythonのTopに戻る