Как дешево получить line count в Python?
Мне нужно получить количество строк большого файла (сотни тысяч строк) в python. Каков наиболее эффективный способ как памяти, так и времени?
на данный момент я делаю:
def file_len(fname):
with open(fname) as f:
for i, l in enumerate(f):
pass
return i + 1
можно ли сделать лучше?
30 ответов
вы не можете получить лучше, чем это.
В конце концов, любое решение должно будет прочитать весь файл, выяснить, сколько \n
У вас есть, и возвращение этого результата.
у вас есть лучший способ сделать это, не читая весь файл? Не уверенный... Лучшее решение всегда будет связано с вводом-выводом, лучшее, что вы можете сделать, это убедиться, что вы не используете ненужную память, но похоже, что у вас есть это.
Я считаю, что сопоставленный с памятью файл будет самым быстрым решением. Я попробовал четыре функции: функция, размещенная OP (opcount
); простая итерация по строкам в файле (simplecount
); readline с отображенным в памяти файлом (mmap) (mapcount
); и решение для чтения буфера, предложенное Николаем Харечко (bufcount
).
я запустил каждую функцию пять раз и рассчитал среднее время выполнения для текстового файла в 1,2 миллиона строк.
Windows XP, Python 2.5, 2 ГБ оперативной памяти, 2 ГГц Процессор 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))
Я должен был опубликовать это на аналогичном вопросе, пока мой рейтинг репутации не подскочил немного (спасибо тому, кто меня ударил!).
все эти решения игнорируют один способ сделать этот запуск значительно быстрее, а именно, используя unbuffered (raw) интерфейс, используя bytearrays, и делать свою собственную буферизацию. (Это применимо только в Python 3. В Python 2 необработанный интерфейс может использоваться или не использоваться по умолчанию, но в 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
используя отдельную функцию генератора, это работает чуточку быстрее:
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
вы можете выполнить подпроцесс и запустить 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 для использования многопроцессорной библиотеки для распределения подсчета строк между машинами / ядрами. Мой тест улучшает подсчет файла строки 20million с 26 секунд до 7 секунд с помощью 8-ядерный сервер windows 64. Примечание: не использование сопоставления памяти делает вещи намного медленнее.
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 ) )
Я бы использовал метод файлового объекта Python readlines
следующим образом:
with open(input_file) as foo:
lines = len(foo.readlines())
это открывает файл, создает список строк в файле, подсчитывает длину списка, сохраняет его в переменной и снова закрывает файл.
def file_len(full_path):
""" Count number of lines in a file."""
f = open(full_path)
nr_of_lines = sum(1 for line in f)
f.close()
return nr_of_lines
Я получил небольшое (4-8%) улучшение с этой версией, которая повторно использует постоянный буфер, поэтому она должна избегать каких-либо накладных расходов памяти или GC:
lines = 0
buffer = bytearray(2048)
with open(filename) as f:
while f.readinto(buffer) > 0:
lines += buffer.count('\n')
вы можете поиграть с размером буфера и, возможно, увидеть небольшое улучшение.
Кайла!--5-->
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
одно из решений строку
import os
os.system("wc -l filename")
мой фрагмент
os.система ('wc-l *.txt')
0 bar.txt
1000 command.txt
3 test_file.txt
1003 total
этот код короче и яснее. Это, вероятно, лучший способ:
num_lines = open('yourfile.ext').read().count('\n')
Это самая быстрая вещь, которую я нашел, используя чистый 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), ''))
Я нашел ответ здесь почему чтение строк из stdin намного медленнее в C++, чем Python? и немного подправил его. Его очень хорошо читать, чтобы понять, как быстро считать строки, хотя wc -l
по-прежнему примерно на 75% быстрее, чем что-либо еще.
просто для завершения вышеуказанных методов я попробовал вариант с модулем fileinput:
import fileinput as fi
def filecount(fname):
for line in fi.input(fname):
pass
return fi.lineno()
и передал файл строк 60mil всем вышеуказанным методам:
mapcount : 6.1331050396
simplecount : 4.588793993
opcount : 4.42918205261
filecount : 43.2780818939
bufcount : 0.170812129974
меня немного удивляет, что fileinput настолько плох и масштабируется намного хуже, чем все другие методы...
вот что я использую, кажется довольно чистым:
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])
UPDATE: это немного быстрее, чем использование чистого python, но за счет использования памяти. Подпроцесс разветвит новый процесс с тем же объемом памяти, что и родительский процесс, пока он выполняет вашу команду.
результатом открытия файла является итератор, который может быть преобразован в последовательность, имеющую длину:
with open(filename) as f:
return len(list(f))
это более лаконично, чем ваш явный цикл, и позволяет избежать enumerate
.
как для меня этот вариант будет самым быстрым:
#!/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).
однострочное решение bash, подобное ответ, используя современные subprocess.check_output
функция:
def line_count(file):
return int(subprocess.check_output('wc -l {}'.format(file), shell=True).split()[0])
Как насчет этого
def file_len(fname):
counts = itertools.count()
with open(fname) as f:
for _ in f: counts.next()
return counts.next()
Как насчет этого?
import fileinput
import sys
counter=0
for line in fileinput.input([sys.argv[1]]):
counter+=1
fileinput.close()
print counter
Как насчет этого-вкладыш:
file_length = len(open('myfile.txt','r').read().split('\n'))
занимает 0.003 сек, используя этот метод, чтобы время его на 3900 файл строки
def c():
import time
s = time.time()
file_length = len(open('myfile.txt','r').read().split('\n'))
print time.time() - s
def line_count(path):
count = 0
with open(path) as lines:
for count, l in enumerate(lines, start=1):
pass
return count
Если кто-то хочет получить количество строк дешево в Python в Linux, я рекомендую этот метод:
import os
print os.popen("wc -l file_path").readline().split()[0]
file_path может быть как абстрактным путем к файлу, так и относительным путем. Надеюсь, это поможет.
другая возможность:
import subprocess
def num_lines_in_file(fpath):
return int(subprocess.check_output('wc -l %s' % fpath, shell=True).strip().split()[0])
def count_text_file_lines(path):
with open(path, 'rt') as file:
line_count = sum(1 for _line in file)
return line_count
Почему бы не прочитать первые 100 и последние 100 строк и оценить среднюю длину строки, а затем разделить общий размер файла на эти числа? Если вам не нужно точное значение, это может сработать.