Как получить количество строк дешево в Python? - PullRequest
874 голосов
/ 10 мая 2009

Мне нужно получить количество строк большого файла (сотни тысяч строк) в Python. Как наиболее эффективно использовать память и время?

На данный момент я делаю:

def file_len(fname):
    with open(fname) as f:
        for i, l in enumerate(f):
            pass
    return i + 1

можно ли сделать что-нибудь лучше?

Ответы [ 37 ]

537 голосов
/ 19 июня 2009

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

num_lines = sum(1 for line in open('myfile.txt'))
304 голосов
/ 10 мая 2009

Вы не можете стать лучше, чем это.

В конце концов, любое решение должно будет прочитать весь файл, выяснить, сколько у вас \n, и вернуть этот результат.

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

191 голосов
/ 12 мая 2009

Я считаю, что файл с отображением в памяти будет самым быстрым решением. Я попробовал четыре функции: функция, опубликованная OP (opcount); простая итерация по строкам в файле (simplecount); readline с полем с отображением в память (mmap) (mapcount); и решение для чтения из буфера, предложенное Николаем Харечко (bufcount).

Я запускал каждую функцию пять раз и вычислял среднее время выполнения для текстового файла длиной в 1,2 миллиона строк.

Windows XP, Python 2.5, 2 ГБ ОЗУ, процессор AMD 2 ГГц

Вот мои результаты:

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))
103 голосов
/ 17 декабря 2014

Мне приходилось публиковать это на похожем вопросе, пока оценка моей репутации немного не подскочила (спасибо тому, кто ударил меня!).

Все эти решения игнорируют один способ сделать это значительно быстрее, а именно, используя небуферизованный (необработанный) интерфейс, используя байтовые массивы и выполняя собственную буферизацию. (Это применимо только в Python 3. В Python 2 необработанный интерфейс может или не может использоваться по умолчанию, но в Python 3 вы по умолчанию будете использовать Unicode.)

Используя модифицированную версию инструмента синхронизации, я полагаю, что следующий код работает быстрее (и чуть более питонно), чем любое из предложенных решений:

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
82 голосов
/ 10 мая 2009

Вы можете выполнить подпроцесс и запустить 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])
40 голосов
/ 26 июля 2011

Вот программа на Python, которая использует многопроцессорную библиотеку для распределения подсчета строк по машинам / ядрам. Мой тест улучшает подсчет 20-миллионного файла строки с 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 ) )
12 голосов
/ 08 октября 2013

Я бы использовал метод файлового объекта Python readlines следующим образом:

with open(input_file) as foo:
    lines = len(foo.readlines())

Это откроет файл, создаст список строк в файле, посчитает длину списка, сохранит ее в переменной и снова закроет файл.

10 голосов
/ 26 июля 2017

Вот то, что я использую, кажется довольно чистым:

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

10 голосов
/ 10 мая 2009
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
9 голосов
/ 25 февраля 2013

Я получил небольшое (4-8%) улучшение с этой версией, в которой повторно используется постоянный буфер, поэтому следует избегать любых затрат памяти или GC: * ​​1001 *

lines = 0
buffer = bytearray(2048)
with open(filename) as f:
  while f.readinto(buffer) > 0:
      lines += buffer.count('\n')

Вы можете поиграть с размером буфера и, возможно, увидеть небольшое улучшение.

...