Как дешево получить 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 У вас есть, и возвращение этого результата.

у вас есть лучший способ сделать это, не читая весь файл? Не уверенный... Лучшее решение всегда будет связано с вводом-выводом, лучшее, что вы можете сделать, это убедиться, что вы не используете ненужную память, но похоже, что у вас есть это.


одна строка, вероятно, довольно быстро:

num_lines = sum(1 for line in open('myfile.txt'))

Я считаю, что сопоставленный с памятью файл будет самым быстрым решением. Я попробовал четыре функции: функция, размещенная 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 также очень быстро


print open('file.txt', 'r').read().count("\n") + 1

count = max(enumerate(open(filename)))[0]


Я изменил случай буфера следующим образом:

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 строк и оценить среднюю длину строки, а затем разделить общий размер файла на эти числа? Если вам не нужно точное значение, это может сработать.


так же:

lines = 0
with open(path) as f:
    for line in f:
        lines += 1