普通はこう?
普通は以下の方法ですが、ファイルサイズ分のメモリを消費してしまいます。
with open('/path/to/not-sorted-file', 'r') as fr:
new_lines = sorted(fr.readlines()) # ファイルサイズ分メモリに展開
with open('/path/to/sorted-file', 'wb') as fw:
fw.write(''.join(new_lines))
大きいサイズに対応する
関数を書きました。大体↓のような事をしてます。
- ファイルを行毎に読む
- 行毎に以下を取っておいて、1 listにまとめる
- 各行のfile offset値
- ソートの比較条件にする各行のsubstring
- #2のlistをソートする
- ファイル
fr
を開き直す - 書き込み様のファイル
fw
を開く - #3のlistをforeachし、ファイル
fr
の位置をfile offset値の箇所に移動する -
line = fr.readline()
する -
fw.write(line)
する
この方法だと "substring x 行数" 分はメモリを消費してしまうのが、いただけないところ (;_;)
import os
import uuid
import tempfile
def sort_large_file(filename, key=lambda l: l[:5]):
'''
sort large file without on-memory.
:param str filename: abspath of file.
:param function key: the function makes sort-key from a line.
'''
# ソート前ファイルを退避する.
tmpname = os.path.join(tempfile.gettempdir(), 'sortlargefile_%s' % (uuid.uuid4().get_hex()))
os.rename(filename, tmpname)
# make a list of offsets.
offset_list = []
with open(tmpname, 'r') as fr:
while True:
offset = fr.tell()
line = fr.readline()
if not line:
break
keyword = key(line)
offset_list.append((keyword, offset, ))
# sort offsets.
offset_list.sort(key=lambda e: e[0])
# sort (write to new file).
with open(filename, 'wb') as fw, open(tmpname, 'r') as fr:
for keyword, offset in offset_list:
fr.seek(offset)
line = fr.readline()
fw.write(line)
# remove tmp.
os.remove(tmpname)
実際に試してみる
以下の様に関数を呼びます。
- 第1引数はソート対象ファイルのパスです。
- 第2引数は「1行を引数にとって、ソートの比較条件になるsubstringを返す」関数です。
- ここでは、CSV行の1列目の要素を切り出すlambdaを与えています。
> sort_large_file('/path/to/your/file', lambda l: l[:l.find(',')])
↓もとのCSVです。
2016-10-01,apple,red
2016-09-29,orange,orange
2015-12-21,banana,yellow
sort_large_file()関数は最終行にも改行が必要。
↓ソートされてこうなります。
2015-12-21,banana,yellow
2016-09-29,orange,orange
2016-10-01,apple,red