13

Как дешево подсчитать количество строк в большом файле на Python

16

Как получить количество строк в большом файле наиболее эффективным с точки зрения памяти и времени образом?

У меня есть функция для подсчета строк в файле:

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

Хотелось бы узнать, есть ли более оптимальные способы добиться этой же цели. Рассматриваю как использование меньшего количества памяти, так и сокращение времени выполнения. Буду признателен за советы и оптимизации!

5 ответ(ов)

4

Вы не найдете ничего лучше этого.

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

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

[Правка май 2023]

Как комментировалось в многих других ответах, в Python 3 есть более эффективные альтернативы. Цикл for не является самым производительным решением. Например, использование mmap или буферов будет более эффективным.

2

Все предложенные решения игнорируют способ значительно ускорить выполнение, а именно использование необработанного (сыро) интерфейса, применение bytearrays и создание собственного буферизованного чтения. (Это касается только 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)

Вот мои замеры времени:

функция       среднее, с   мин, с   соотношение
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

Эти результаты показывают, что использование необработанного интерфейса и буферизация очень эффективно при подсчете строк в файлах.

1

Вы можете использовать модуль subprocess для выполнения команды и подсчета строк в файле с помощью 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])

В этой функции создается новый процесс, который выполняет команду wc -l для указанного файла. Результат команды и возможные ошибки захватываются с помощью stdout и stderr. Если выполнение команды завершилось с ошибкой (то есть returncode не равен 0), возникает исключение IOError с текстом ошибки. В противном случае функция возвращает число строк в файле, которое извлекается из результата выполнения команды.

0

Вот программа на Python, использующая библиотеку multiprocessing для распределения подсчета строк по машинам/ядрам. Мой тест показывает, что подсчет строк в файле на 20 миллионов строк занимает 26 секунд, а с помощью этой программы — всего 7 секунд на сервере Windows 64-bit с 8 ядрами. Обратите внимание: если не использовать отображение памяти, процесс значительно замедляется.

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()  # Новый логгер на корневом уровне
    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('начало')

    physical_file = open(file1, "r")
    # mmap.mmap(fileno, length[, tagname[, access[, offset]]]

    m1 = mmap.mmap(physical_file.fileno(), 0, access=mmap.ACCESS_READ)

    # Определяем размер файла для распределения подсчета строк

    fSize = os.stat(file1).st_size
    chunk = (fSize / processes) + 1

    lines = 0

    # Определяем, откуда начинать и где заканчивать
    _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

    # Находим, с какой позиции начинать
    if pid > 0:
        m1.seek(seekStart)
        # Читаем следующую строку
        l1 = m1.readline()  # Используем readline с отображаемыми файлами
        seekStart = m1.tell()

    # Сообщаем предыдущему процессу, с какой позиции начинать, чтобы тот знал, где заканчивать

    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('завершено')
    # Подсчитываем результаты
    if pid == 0:
        for p in range(1, processes):
            lines += queues[0].get()
        queues[0].put(lines)  # Общее количество строк
    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('необходимы параметры: имя файла [число процессов]')
        exit()

    t = time.time()
    processes = multiprocessing.cpu_count()
    if len(sys.argv) > 2:
        processes = int(sys.argv[2])
    queues = []  # Очередь для каждого процесса
    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()  # Ждем завершения подсчета
    lines = queues[0].get()

    logging.info('завершено {} Lines:{}'.format(time.time() - t, lines))

Этот код демонстрирует, как можно эффективно использовать многопоточность для ускорения обработки больших файлов, особенно при использовании mmap для улучшения производительности.

0

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

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

Этот код открывает файл, создает список строк из содержимого файла, подсчитывает количество строк в списке, сохраняет это значение в переменную и автоматически закрывает файл.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь