Pythonで大きなファイル(数十万行)の行数を取得する必要があります。メモリと時間の両方で最も効率的な方法は何ですか?
現時点で私は:
def file_len(fname):
with open(fname) as f:
for i, l in enumerate(f):
pass
return i + 1
より良いことはできますか?
enumerate(f, 1)
そして、捨てi + 1
ますか?
Pythonで大きなファイル(数十万行)の行数を取得する必要があります。メモリと時間の両方で最も効率的な方法は何ですか?
現時点で私は:
def file_len(fname):
with open(fname) as f:
for i, l in enumerate(f):
pass
return i + 1
より良いことはできますか?
enumerate(f, 1)
そして、捨てi + 1
ますか?
回答:
それ以上のものはありません。
結局のところ、どのようなソリューションでも、ファイル全体を読み取って、その数\n
を調べ、その結果を返す必要があります。
ファイル全体を読み取らずにそれを行うより良い方法はありますか?わからない...最善の解決策は常にI / Oバウンドです。最善の解決策は、不要なメモリを使用しないことですが、それをカバーしているように見えます。
1行、おそらくかなり高速:
num_lines = sum(1 for line in open('myfile.txt'))
メモリマップファイルが最速のソリューションになると思います。私は4つの関数を試しましたopcount
。ファイル内の行を単純に繰り返します(simplecount
); メモリマップファイル(mmap)(mapcount
);を使用したreadline Mykola Kharechkoが提供するバッファ読み取りソリューション(bufcount
)。
各関数を5回実行し、120万行のテキストファイルの平均実行時間を計算しました。
Windows XP、Python 2.5、2 GB RAM、2 GHz AMDプロセッサ
これが私の結果です:
mapcount : 0.465599966049
simplecount : 0.756399965286
bufcount : 0.546800041199
opcount : 0.718600034714
編集:Python 2.6の数値:
mapcount : 0.471799945831
simplecount : 0.634400033951
bufcount : 0.468800067902
opcount : 0.602999973297
したがって、バッファの読み取り戦略は、Windows / Python 2.6の場合は最速のようです
これがコードです:
from __future__ import with_statement
import time
import mmap
import random
from collections import defaultdict
def mapcount(filename):
f = open(filename, "r+")
buf = mmap.mmap(f.fileno(), 0)
lines = 0
readline = buf.readline
while readline():
lines += 1
return lines
def simplecount(filename):
lines = 0
for line in open(filename):
lines += 1
return lines
def bufcount(filename):
f = open(filename)
lines = 0
buf_size = 1024 * 1024
read_f = f.read # loop optimization
buf = read_f(buf_size)
while buf:
lines += buf.count('\n')
buf = read_f(buf_size)
return lines
def opcount(fname):
with open(fname) as f:
for i, l in enumerate(f):
pass
return i + 1
counts = defaultdict(list)
for i in range(5):
for func in [mapcount, simplecount, bufcount, opcount]:
start_time = time.time()
assert func("big_file.txt") == 1209138
counts[func].append(time.time() - start_time)
for key, vals in counts.items():
print key.__name__, ":", sum(vals) / float(len(vals))
wccount()
は最速のgist.github.com/0ac760859e614cd03652
私の評判スコアが少し上がるまで、同様の質問にこれを投稿しなければなりませんでした(私をぶつけた人に感謝します!)。
これらのソリューションはすべて、これを大幅に高速化する1つの方法を無視します。つまり、バッファリングされていない(raw)インターフェイスを使用し、バイト配列を使用し、独自のバッファリングを実行します。(これはPython 3にのみ適用されます。Python2では、rawインターフェースはデフォルトで使用される場合と使用されない場合がありますが、Python 3ではデフォルトでUnicodeになります。)
タイミングツールの変更されたバージョンを使用すると、次のコードは、提供されているどのソリューションよりも高速(そしてわずかにPythonic)だと思います。
def rawcount(filename):
f = open(filename, 'rb')
lines = 0
buf_size = 1024 * 1024
read_f = f.raw.read
buf = read_f(buf_size)
while buf:
lines += buf.count(b'\n')
buf = read_f(buf_size)
return lines
別のジェネレーター関数を使用すると、smidgeがより速く実行されます。
def _make_gen(reader):
b = reader(1024 * 1024)
while b:
yield b
b = reader(1024*1024)
def rawgencount(filename):
f = open(filename, 'rb')
f_gen = _make_gen(f.raw.read)
return sum( buf.count(b'\n') for buf in f_gen )
これはitertoolsを使用してインラインでジェネレーター式で完全に実行できますが、見た目がかなり奇妙になります。
from itertools import (takewhile,repeat)
def rawincount(filename):
f = open(filename, 'rb')
bufgen = takewhile(lambda x: x, (f.raw.read(1024*1024) for _ in repeat(None)))
return sum( buf.count(b'\n') for buf in bufgen )
ここに私のタイミングがあります:
function average, s min, s ratio
rawincount 0.0043 0.0041 1.00
rawgencount 0.0044 0.0042 1.01
rawcount 0.0048 0.0045 1.09
bufcount 0.008 0.0068 1.64
wccount 0.01 0.0097 2.35
itercount 0.014 0.014 3.41
opcount 0.02 0.02 4.83
kylecount 0.021 0.021 5.05
simplecount 0.022 0.022 5.25
mapcount 0.037 0.031 7.46
wccount
サブプロセスのシェルについては、この表のwc
ツール?
rawincount
使用することで、ソリューションの見た目をおかしくすることができます。bufgen = iter(partial(f.raw.read, 1024*1024), b'')
takewhile
repeat
サブプロセスを実行して実行できます wc -l filename
import subprocess
def file_len(fname):
p = subprocess.Popen(['wc', '-l', fname], stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
result, err = p.communicate()
if p.returncode != 0:
raise IOError(err)
return int(result.strip().split()[0])
マルチプロセッシングライブラリを使用して、マシン/コア間で行カウントを分散するpythonプログラムを次に示します。私のテストでは、8コアのWindows 64サーバーを使用して、2000万行のファイルのカウントを26秒から7秒に改善しています。注:メモリマッピングを使用しないと、処理が非常に遅くなります。
import multiprocessing, sys, time, os, mmap
import logging, logging.handlers
def init_logger(pid):
console_format = 'P{0} %(levelname)s %(message)s'.format(pid)
logger = logging.getLogger() # New logger at root level
logger.setLevel( logging.INFO )
logger.handlers.append( logging.StreamHandler() )
logger.handlers[0].setFormatter( logging.Formatter( console_format, '%d/%m/%y %H:%M:%S' ) )
def getFileLineCount( queues, pid, processes, file1 ):
init_logger(pid)
logging.info( 'start' )
physical_file = open(file1, "r")
# mmap.mmap(fileno, length[, tagname[, access[, offset]]]
m1 = mmap.mmap( physical_file.fileno(), 0, access=mmap.ACCESS_READ )
#work out file size to divide up line counting
fSize = os.stat(file1).st_size
chunk = (fSize / processes) + 1
lines = 0
#get where I start and stop
_seedStart = chunk * (pid)
_seekEnd = chunk * (pid+1)
seekStart = int(_seedStart)
seekEnd = int(_seekEnd)
if seekEnd < int(_seekEnd + 1):
seekEnd += 1
if _seedStart < int(seekStart + 1):
seekStart += 1
if seekEnd > fSize:
seekEnd = fSize
#find where to start
if pid > 0:
m1.seek( seekStart )
#read next line
l1 = m1.readline() # need to use readline with memory mapped files
seekStart = m1.tell()
#tell previous rank my seek start to make their seek end
if pid > 0:
queues[pid-1].put( seekStart )
if pid < processes-1:
seekEnd = queues[pid].get()
m1.seek( seekStart )
l1 = m1.readline()
while len(l1) > 0:
lines += 1
l1 = m1.readline()
if m1.tell() > seekEnd or len(l1) == 0:
break
logging.info( 'done' )
# add up the results
if pid == 0:
for p in range(1,processes):
lines += queues[0].get()
queues[0].put(lines) # the total lines counted
else:
queues[0].put(lines)
m1.close()
physical_file.close()
if __name__ == '__main__':
init_logger( 'main' )
if len(sys.argv) > 1:
file_name = sys.argv[1]
else:
logging.fatal( 'parameters required: file-name [processes]' )
exit()
t = time.time()
processes = multiprocessing.cpu_count()
if len(sys.argv) > 2:
processes = int(sys.argv[2])
queues=[] # a queue for each process
for pid in range(processes):
queues.append( multiprocessing.Queue() )
jobs=[]
prev_pipe = 0
for pid in range(processes):
p = multiprocessing.Process( target = getFileLineCount, args=(queues, pid, processes, file_name,) )
p.start()
jobs.append(p)
jobs[0].join() #wait for counting to finish
lines = queues[0].get()
logging.info( 'finished {} Lines:{}'.format( time.time() - t, lines ) )
この回答に似た1行のbashソリューションで、モダンsubprocess.check_output
関数を使用します。
def line_count(filename):
return int(subprocess.check_output(['wc', '-l', filename]).split()[0])
wc -l
かかりますが、サブプロセスの呼び出しには5秒ほどかかります。
shell=True
セキュリティに悪影響があるため、回避することをお勧めします。
readlines
次のように、Pythonのファイルオブジェクトメソッドを使用します。
with open(input_file) as foo:
lines = len(foo.readlines())
これにより、ファイルが開き、ファイルの行のリストが作成され、リストの長さがカウントされ、変数に保存されて、ファイルが再び閉じます。
xreadlines
、反復子を返すだけなので、2.3以降非推奨になりました。 for line in file
記載されている代替品です。参照:docs.python.org/2/library/stdtypes.html#file.xreadlines
これが私が使っているもので、かなりきれいに見えます:
import subprocess
def count_file_lines(file_path):
"""
Counts the number of lines in a file using wc utility.
:param file_path: path to file
:return: int, no of lines
"""
num = subprocess.check_output(['wc', '-l', file_path])
num = num.split(' ')
return int(num[0])
更新:これは純粋なpythonを使用するよりもわずかに高速ですが、メモリ使用量が犠牲になります。サブプロセスは、コマンドの実行中に、親プロセスと同じメモリフットプリントで新しいプロセスをフォークします。
:-)
これは、純粋なpythonを使用して見つけた最速のものです。バッファを設定することで、必要なメモリ量を使用できますが、私のコンピュータでは2 ** 16がスイートスポットのようです。
from functools import partial
buffer=2**16
with open(myfile) as f:
print sum(x.count('\n') for x in iter(partial(f.read,buffer), ''))
私はここで答えを見つけましたC ++でstdinからの行の読み取りがPythonよりもはるかに遅いのはなぜですか?ほんの少し微調整しました。行数をすばやくカウントする方法を理解するのに非常に役立ちますが、wc -l
他のものよりも約75%高速です。
このバージョンでは定数バッファを再利用するため、メモリが少しも(GCも)オーバーヘッドを回避できるように、少し(4〜8%)改善されています。
lines = 0
buffer = bytearray(2048)
with open(filename) as f:
while f.readinto(buffer) > 0:
lines += buffer.count('\n')
バッファサイズをいじってみると、少し改善されるかもしれません。
num_lines = sum(1 for line in open('my_file.txt'))
おそらく最高ですが、これの代替策は
num_lines = len(open('my_file.txt').read().splitlines())
これは両方のパフォーマンスの比較です
In [20]: timeit sum(1 for line in open('Charts.ipynb'))
100000 loops, best of 3: 9.79 µs per loop
In [21]: timeit len(open('Charts.ipynb').read().splitlines())
100000 loops, best of 3: 12 µs per loop
1行のソリューション:
import os
os.system("wc -l filename")
私のスニペット:
>>> os.system('wc -l *.txt')
0 bar.txt
1000 command.txt
3 test_file.txt
1003 total
os.system()
とにかく、出力を変数に保存して後処理することはできません。
上記の方法を完了するために、fileinputモジュールでバリアントを試しました:
import fileinput as fi
def filecount(fname):
for line in fi.input(fname):
pass
return fi.lineno()
そして60mil linesファイルを上記のすべてのメソッドに渡しました:
mapcount : 6.1331050396
simplecount : 4.588793993
opcount : 4.42918205261
filecount : 43.2780818939
bufcount : 0.170812129974
ファイル入力が他のすべての方法よりも悪く、スケーリングがはるかに悪いのは少し驚きです...
私にとっては、このバリアントが最速になります:
#!/usr/bin/env python
def main():
f = open('filename')
lines = 0
buf_size = 1024 * 1024
read_f = f.read # loop optimization
buf = read_f(buf_size)
while buf:
lines += buf.count('\n')
buf = read_f(buf_size)
print lines
if __name__ == '__main__':
main()
理由:行ごとに読み取るよりもバッファリングが速く、string.count
また非常に速い
私はこのようにバッファーケースを変更しました:
def CountLines(filename):
f = open(filename)
try:
lines = 1
buf_size = 1024 * 1024
read_f = f.read # loop optimization
buf = read_f(buf_size)
# Empty file
if not buf:
return 0
while buf:
lines += buf.count('\n')
buf = read_f(buf_size)
return lines
finally:
f.close()
空のファイルと最後の行(\ nなし)もカウントされるようになりました。
count = max(enumerate(open(filename)))[0]
enumerate()
は、docs.python.org
def line_count(path):
count = 0
with open(path) as lines:
for count, l in enumerate(lines, start=1):
pass
return count
def count_text_file_lines(path):
with open(path, 'rt') as file:
line_count = sum(1 for _line in file)
return line_count
ファイルを開いた結果はイテレータで、これは長さを持つシーケンスに変換できます。
with open(filename) as f:
return len(list(f))
これは明示的なループよりも簡潔で、を回避しenumerate
ます。