Pythonゼロ埋めなしリストの自然順ソート完全ガイド – 文字列に含まれる数値を正しく並び替える方法
Pythonでファイル名やバージョン番号など、文字列に数値が含まれるリストをソートする際、通常の文字列ソートでは期待通りの結果になりません。例えば['file1.txt', 'file10.txt', 'file2.txt']を通常のソートにかけると、['file1.txt', 'file10.txt', 'file2.txt']となり、数値的な順序になりません。この記事では、ゼロ埋めされていない数値を含む文字列を自然な順序でソートする様々な手法を詳しく解説します。
1. 問題の理解:通常のソートでは期待通りにならない
問題の例
# 通常の文字列ソート(期待通りにならない)
files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt', 'file3.txt']
sorted_files = sorted(files)
print(sorted_files)
# ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt', 'file3.txt']
# → file10.txtがfile2.txtより前に来てしまう
# 期待する結果
# ['file1.txt', 'file2.txt', 'file3.txt', 'file10.txt', 'file20.txt']
数値文字列での問題
versions = ['1.2.3', '1.10.1', '1.2.10', '1.3.0']
print(sorted(versions))
# ['1.10.1', '1.2.10', '1.2.3', '1.3.0']
# → 数値として正しくない順序
2. 正規表現を使った自然順ソート
基本的な自然順ソート関数
import re
def natural_sort_key(text):
"""文字列を数値部分と文字部分に分けてソートキーを作成"""
return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', text)]
files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt', 'file3.txt']
sorted_files = sorted(files, key=natural_sort_key)
print(sorted_files)
# ['file1.txt', 'file2.txt', 'file3.txt', 'file10.txt', 'file20.txt']
より複雑なパターンでの自然順ソート
import re
def advanced_natural_sort_key(text):
"""数値部分を適切に処理する改良版"""
parts = re.split(r'(\d+)', text)
result = []
for part in parts:
if part.isdigit():
result.append(int(part))
else:
result.append(part.lower())
return result
data = ['item1a', 'item10b', 'item2c', 'item100d', 'item21e']
sorted_data = sorted(data, key=advanced_natural_sort_key)
print(sorted_data)
# ['item1a', 'item2c', 'item10b', 'item21e', 'item100d']
3. バージョン番号の自然順ソート
セマンティックバージョンのソート
import re
def version_sort_key(version):
"""バージョン番号を適切にソートするキー関数"""
parts = re.split(r'[.\-]', version)
result = []
for part in parts:
if part.isdigit():
result.append(int(part))
else:
# アルファベット部分は文字列として処理
result.append(part)
return result
versions = ['1.2.3', '1.10.1', '1.2.10', '1.3.0', '2.0.0', '1.2.3-alpha']
sorted_versions = sorted(versions, key=version_sort_key)
print(sorted_versions)
# ['1.2.3', '1.2.3-alpha', '1.2.10', '1.3.0', '1.10.1', '2.0.0']
複雑なバージョン形式への対応
import re
def complex_version_key(version):
"""複雑なバージョン形式に対応"""
# v1.2.3-beta.1 のような形式にも対応
# 英字の前後で分割
parts = re.split(r'([a-zA-Z]+)', version)
result = []
for part in parts:
if not part:
continue
# 数値と記号を分離
subparts = re.split(r'(\d+)', part)
for subpart in subparts:
if subpart.isdigit():
result.append(int(subpart))
elif subpart:
result.append(subpart.lower())
return result
versions = ['v1.2.3', 'v1.10.1-beta', 'v1.2.10-alpha.2', 'v2.0.0-rc.1']
sorted_versions = sorted(versions, key=complex_version_key)
print(sorted_versions)
4. ファイル名・パス名の自然順ソート
ファイル名のソート
import re
import os
def filename_sort_key(filename):
"""ファイル名を自然順でソートするキー"""
# 拡張子とファイル名を分離
name, ext = os.path.splitext(filename)
# 数値部分を分離
parts = re.split(r'(\d+)', name)
result = []
for part in parts:
if part.isdigit():
result.append(int(part))
else:
result.append(part.lower())
# 拡張子も追加
result.append(ext.lower())
return result
files = ['image1.jpg', 'image10.png', 'image2.gif', 'document1.pdf', 'document10.pdf']
sorted_files = sorted(files, key=filename_sort_key)
print(sorted_files)
# ['document1.pdf', 'document10.pdf', 'image1.jpg', 'image2.gif', 'image10.png']
パス名での自然順ソート
import re
import os
def path_sort_key(path):
"""パス全体を自然順でソート"""
# パスを分解
parts = path.split(os.sep)
result = []
for part in parts:
# 各部分を自然順ソート用に処理
subparts = re.split(r'(\d+)', part)
for subpart in subparts:
if subpart.isdigit():
result.append(int(subpart))
else:
result.append(subpart.lower())
result.append('/') # セパレータマーカー
return result
paths = [
'folder1/file10.txt',
'folder1/file2.txt',
'folder10/file1.txt',
'folder2/file1.txt'
]
sorted_paths = sorted(paths, key=path_sort_key)
print(sorted_paths)
5. 外部ライブラリを使った自然順ソート
natsortsライブラリの使用
# pip install natsort が必要
try:
from natsort import natsorted, ns
files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']
sorted_files = natsorted(files)
print(sorted_files)
# ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']
# 大文字小文字を無視
mixed_case = ['File1.txt', 'file10.txt', 'File2.txt']
sorted_mixed = natsorted(mixed_case, alg=ns.IGNORECASE)
print(sorted_mixed)
except ImportError:
print("natsort ライブラリをインストールしてください: pip install natsort")
6. 数値とアルファベットが混在する場合
複雑な混在パターンのソート
import re
def mixed_sort_key(text):
"""数値、アルファベット、記号が混在する文字列のソート"""
# より詳細な分割パターン
parts = re.split(r'(\d+|\W+)', text)
result = []
for part in parts:
if not part:
continue
elif part.isdigit():
# 数値部分
result.append((0, int(part)))
elif part.isalpha():
# アルファベット部分
result.append((1, part.lower()))
else:
# 記号部分
result.append((2, part))
return result
mixed_data = ['A1B', 'A10B', 'A2B', 'A1C', 'B1A', 'A1-B', 'A1_B']
sorted_mixed = sorted(mixed_data, key=mixed_sort_key)
print(sorted_mixed)
7. 日本語が含まれる場合の自然順ソート
日本語ファイル名の処理
import re
def japanese_natural_sort_key(text):
"""日本語を含む文字列の自然順ソート"""
# 数値部分を分離
parts = re.split(r'(\d+)', text)
result = []
for part in parts:
if part.isdigit():
result.append(int(part))
else:
# 日本語文字列はそのまま
result.append(part)
return result
japanese_files = ['ファイル1.txt', 'ファイル10.txt', 'ファイル2.txt', '画像1.jpg']
sorted_japanese = sorted(japanese_files, key=japanese_natural_sort_key)
print(sorted_japanese)
# ['ファイル1.txt', 'ファイル2.txt', 'ファイル10.txt', '画像1.jpg']
8. パフォーマンスを考慮した実装
大量データでの効率的な自然順ソート
import re
from functools import lru_cache
@lru_cache(maxsize=1000)
def cached_natural_sort_key(text):
"""キャッシュ機能付きの自然順ソートキー"""
return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', text)]
def sort_large_list(data):
"""大量データの効率的ソート"""
return sorted(data, key=cached_natural_sort_key)
# 大量データのテスト
large_data = [f'item{i}' for i in range(1000, 0, -1)]
sorted_large = sort_large_list(large_data)
print(f"ソート完了: {len(sorted_large)}件")
print(sorted_large[:5]) # 最初の5件表示
9. 特殊なケースへの対応
小数点を含む数値の処理
import re
def decimal_sort_key(text):
"""小数点を含む数値に対応した自然順ソート"""
# 小数点を含む数値パターンを考慮
parts = re.split(r'(\d+(?:\.\d+)?)', text)
result = []
for part in parts:
if re.match(r'^\d+(?:\.\d+)?$', part):
result.append(float(part))
else:
result.append(part.lower())
return result
decimal_data = ['value1.5', 'value10.2', 'value2.1', 'value2.10']
sorted_decimal = sorted(decimal_data, key=decimal_sort_key)
print(sorted_decimal)
# ['value1.5', 'value2.1', 'value2.10', 'value10.2']
負の数値を含む場合
import re
def signed_number_sort_key(text):
"""正負の数値を含む文字列のソート"""
parts = re.split(r'([-+]?\d+)', text)
result = []
for part in parts:
if re.match(r'^[-+]?\d+$', part):
result.append(int(part))
else:
result.append(part.lower())
return result
signed_data = ['temp-5', 'temp10', 'temp-10', 'temp5', 'temp+15']
sorted_signed = sorted(signed_data, key=signed_number_sort_key)
print(sorted_signed)
# ['temp-10', 'temp-5', 'temp5', 'temp10', 'temp+15']
10. 実用的な応用例
ログファイルのソート
import re
from datetime import datetime
def log_file_sort_key(filename):
"""ログファイル名の自然順ソート"""
# app.log.2024-01-01.1, app.log.2024-01-01.10 などに対応
parts = re.split(r'(\d{4}-\d{2}-\d{2})|(\d+)', filename)
result = []
for part in parts:
if not part:
continue
elif re.match(r'^\d{4}-\d{2}-\d{2}$', part):
# 日付部分
result.append(datetime.strptime(part, '%Y-%m-%d'))
elif part.isdigit():
# 数値部分
result.append(int(part))
else:
# 文字列部分
result.append(part.lower())
return result
log_files = [
'app.log.2024-01-01.1',
'app.log.2024-01-01.10',
'app.log.2024-01-01.2',
'app.log.2024-01-02.1'
]
sorted_logs = sorted(log_files, key=log_file_sort_key)
print(sorted_logs)
データベースレコードのソート
import re
class Record:
def __init__(self, id, name):
self.id = id
self.name = name
def __repr__(self):
return f"Record(id='{self.id}', name='{self.name}')"
def record_sort_key(record):
"""レコードオブジェクトの自然順ソート"""
# IDフィールドを基準にソート
parts = re.split(r'(\d+)', record.id)
result = []
for part in parts:
if part.isdigit():
result.append(int(part))
else:
result.append(part.lower())
return result
records = [
Record('USER1', 'Alice'),
Record('USER10', 'Bob'),
Record('USER2', 'Charlie'),
Record('ADMIN1', 'Dave')
]
sorted_records = sorted(records, key=record_sort_key)
for record in sorted_records:
print(record)
11. エラーハンドリングと堅牢性
安全な自然順ソート
import re
def safe_natural_sort_key(text):
"""エラーハンドリングを含む安全な自然順ソート"""
try:
if not isinstance(text, str):
text = str(text)
parts = re.split(r'(\d+)', text)
result = []
for part in parts:
try:
if part.isdigit():
# 非常に大きな数値への対応
num = int(part)
if num > 10**15: # 制限を設ける
result.append(part.lower())
else:
result.append(num)
else:
result.append(part.lower())
except (ValueError, OverflowError):
result.append(part.lower())
return result
except Exception:
# 何らかのエラーが発生した場合は文字列として処理
return [str(text).lower()]
# テストデータ(エラーを含む可能性のある)
test_data = ['file1', None, 'file10', 123, 'file2', 'file' + '9' * 20]
cleaned_data = []
for item in test_data:
if item is not None:
cleaned_data.append(str(item))
sorted_safe = sorted(cleaned_data, key=safe_natural_sort_key)
print(sorted_safe)
12. ベンチマークとパフォーマンス比較
各手法のパフォーマンス比較
import time
import re
def benchmark_sorting_methods(data, iterations=1000):
"""各ソート手法のパフォーマンス比較"""
# 通常のソート
start = time.time()
for _ in range(iterations):
sorted(data)
normal_time = time.time() - start
# 自然順ソート
def natural_key(text):
return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', text)]
start = time.time()
for _ in range(iterations):
sorted(data, key=natural_key)
natural_time = time.time() - start
print(f"通常ソート: {normal_time:.4f}秒")
print(f"自然順ソート: {natural_time:.4f}秒")
print(f"比率: {natural_time/normal_time:.2f}倍")
# テストデータ
test_data = [f'file{i}.txt' for i in [1, 10, 2, 20, 3, 30, 4, 40, 5]]
benchmark_sorting_methods(test_data)
13. 実装の選択指針
用途別の推奨手法
def choose_sort_method(data_type, performance_priority=False):
"""用途に応じた最適なソート手法を提案"""
recommendations = {
'simple_files': 'basic_natural_sort',
'versions': 'version_specific_sort',
'mixed_content': 'advanced_natural_sort',
'large_dataset': 'cached_natural_sort',
'multilingual': 'unicode_aware_sort'
}
if performance_priority and data_type in ['large_dataset']:
return 'use_natsort_library'
return recommendations.get(data_type, 'basic_natural_sort')
# 使用例
print("ファイル名のソート:", choose_sort_method('simple_files'))
print("バージョン番号のソート:", choose_sort_method('versions'))
print("大規模データ:", choose_sort_method('large_dataset', performance_priority=True))
まとめ
Pythonでゼロ埋めなし数値を含む文字列の自然順ソートを実現するには、以下の主要な手法があります:
- 正規表現による分割 –
re.split(r'(\d+)', text)を使った基本的な手法 - 専用ライブラリの活用 –
natsortライブラリによる高性能実装 - 用途特化型の実装 – バージョン番号、ファイル名等に最適化された処理
- パフォーマンス最適化 – キャッシュ機能やメモリ効率を考慮した実装
用途に応じて適切な手法を選択し、エラーハンドリングも含めて堅牢な実装を行うことで、ユーザーにとって自然で直感的な並び順を実現できます。特にファイル管理システムやバージョン管理において、この技術は必須のスキルとなります。
■プロンプトだけでオリジナルアプリを開発・公開してみた!!
■AI時代の第一歩!「AI駆動開発コース」はじめました!
テックジム東京本校で先行開始。
■テックジム東京本校
「武田塾」のプログラミング版といえば「テックジム」。
講義動画なし、教科書なし。「進捗管理とコーチング」で効率学習。
より早く、より安く、しかも対面型のプログラミングスクールです。
<短期講習>5日で5万円の「Pythonミニキャンプ」開催中。
<月1開催>放送作家による映像ディレクター養成講座
<オンライン無料>ゼロから始めるPython爆速講座
