Самый эффективный способ изменить последнюю строку большого текстового файла в Python

Мне нужно обновить последнюю строку из нескольких файлов размером более 2 ГБ, состоящих из строк текста, которые невозможно прочитать с помощью readlines(), В настоящее время он работает нормально, перебирая строку за строкой. Тем не менее, мне интересно, если есть какая-либо скомпилированная библиотека может достичь этого более эффективно? Спасибо!

Текущий подход

    myfile = open("large.XML")
    for line in myfile:
        do_something()

2 ответа

Решение

Обновление: используйте ответ ShadowRanger. Это намного короче и надежнее.

Для потомков:

Прочитайте последние N байтов файла и найдите новую строку в обратном направлении.

#!/usr/bin/env python

with open("test.txt", "wb") as testfile:
    testfile.write('\n'.join(["one", "two", "three"]) + '\n')

with open("test.txt", "r+b") as myfile:
    # Read the last 1kiB of the file
    # we could make this be dynamic, but chances are there's
    # a number like 1kiB that'll work 100% of the time for you
    myfile.seek(0,2)
    filesize = myfile.tell()
    blocksize = min(1024, filesize)
    myfile.seek(-blocksize, 2)
    # search backwards for a newline (excluding very last byte
    # in case the file ends with a newline)
    index = myfile.read().rindex('\n', 0, blocksize - 1)
    # seek to the character just after the newline
    myfile.seek(index + 1 - blocksize, 2)
    # read in the last line of the file
    lastline = myfile.read()
    # modify last_line
    lastline = "Brand New Line!\n"
    # seek back to the start of the last line
    myfile.seek(index + 1 - blocksize, 2)
    # write out new version of the last line
    myfile.write(lastline)
    myfile.truncate()

Если это действительно что-то основанное на строках (где настоящий XML-парсер не является лучшим решением), mmap может помочь здесь

mmap файл, затем позвоните .rfind('\n') в результирующем объекте (возможно, с настройками для обработки файла, заканчивающегося новой строкой, когда вы действительно хотите, чтобы перед ним была непустая строка, а не пустая "строка", следующая за ним). Затем вы можете вырезать последнюю строку в одиночку. Если вам нужно изменить файл на месте, вы можете изменить его размер, чтобы уменьшить (или добавить) количество байтов, соответствующее разнице между отрезанной строкой и новой строкой, а затем записать обратно новую строку. Избегает чтения или записи файла больше, чем вам нужно.

Пример кода (пожалуйста, прокомментируйте, если я допустил ошибку):

import mmap

# In Python 3.1 and earlier, you'd wrap mmap in contextlib.closing; mmap
# didn't support the context manager protocol natively until 3.2; see example below
with open("large.XML", 'r+b') as myfile, mmap.mmap(myfile.fileno(), 0, access=mmap.ACCESS_WRITE) as mm:
    # len(mm) - 1 handles files ending w/newline by getting the prior line
    # + 1 to avoid catching prior newline (and handle one line file seamlessly)
    startofline = mm.rfind(b'\n', 0, len(mm) - 1) + 1

    # Get the line (with any newline stripped)
    line = mm[startofline:].rstrip(b'\r\n')

    # Do whatever calculates the new line, decoding/encoding to use str
    # in do_something to simplify; this is an XML file, so I'm assuming UTF-8
    new_line = do_something(line.decode('utf-8')).encode('utf-8')

    # Resize to accommodate the new line (or to strip data beyond the new line)
    mm.resize(startofline + len(new_line))  # + 1 if you need to add a trailing newline
    mm[startofline:] = new_line  # Replace contents; add a b"\n" if needed

Видимо в некоторых системах (например, OSX) без mremap, mm.resize не будет работать, поэтому для поддержки этих систем, вы, вероятно, разделите with (Итак mmap закрывается перед объектом файла) и использует поиск, запись и усечение файла на основе объекта файла для исправления файла. Следующий пример включает в себя мой ранее упомянутый Python 3.1 и ранее определенные настройки для использования contextlib.closing для полноты:

import mmap
from contextlib import closing

with open("large.XML", 'r+b') as myfile:
    with closing(mmap.mmap(myfile.fileno(), 0, access=mmap.ACCESS_WRITE)) as mm:
        startofline = mm.rfind(b'\n', 0, len(mm) - 1) + 1
        line = mm[startofline:].rstrip(b'\r\n')
        new_line = do_something(line.decode('utf-8')).encode('utf-8')

    myfile.seek(startofline)  # Move to where old line began
    myfile.write(new_line)  # Overwrite existing line with new line
    myfile.truncate()  # If existing line longer than new line, get rid of the excess

Преимущества для mmap над любым другим подходом являются:

  1. Больше не нужно читать файл за пределами самой строки (то есть 1-2 страницы файла, остальное никогда не будет прочитано или записано)
  2. С помощью rfind означает, что вы можете позволить Python быстро найти новую строку на уровне C (в CPython); явный seek с и read файлового объекта может соответствовать "только чтение страницы или около того", но вам придется вручную выполнить поиск новой строки

Предостережение: этот подход не будет работать (по крайней мере, не без изменений, чтобы избежать сопоставления более 2 ГБ и обрабатывать изменение размера, когда весь файл может не отображаться), если вы работаете в 32-разрядной системе и файл слишком велик отобразить в память. В большинстве 32-битных систем, даже в недавно появившемся процессе, у вас есть только 1-2 ГБ непрерывного адресного пространства; в некоторых особых случаях у вас может быть до 3-3,5 ГБ виртуальных адресов пользователей (хотя вы потеряете часть смежного пространства для кучи, стека, отображения исполняемых файлов и т. д.). mmap не требует большого объема физической оперативной памяти, но требует непрерывного адресного пространства; Одним из огромных преимуществ 64-битной ОС является то, что вы перестаете беспокоиться о виртуальном адресном пространстве во всех случаях, кроме самых нелепых, поэтому mmap может решить проблемы в общем случае, которые он не может решить без дополнительной сложности на 32-битной ОС. На данный момент большинство современных компьютеров являются 64-битными, но это определенно следует иметь в виду, если вы ориентируетесь на 32-битные системы (и в Windows, даже если ОС 64-битная, возможно, они установили 32-битную версию Python от ошибка, поэтому те же проблемы применяются). Вот еще один пример, который работает (при условии, что последняя строка не длиннее 100 МБ) на 32-битном Python (опуская closing и импорт для краткости) даже для огромных файлов:

with open("large.XML", 'r+b') as myfile:
    filesize = myfile.seek(0, 2)
    # Get an offset that only grabs the last 100 MB or so of the file aligned properly
    offset = max(0, filesize - 100 * 1024 ** 2) & ~(mmap.ALLOCATIONGRANULARITY - 1)
    with mmap.mmap(myfile.fileno(), 0, access=mmap.ACCESS_WRITE, offset=offset) as mm:
        startofline = mm.rfind(b'\n', 0, len(mm) - 1) + 1
        # If line might be > 100 MB long, probably want to check if startofline
        # follows a newline here
        line = mm[startofline:].rstrip(b'\r\n')
        new_line = do_something(line.decode('utf-8')).encode('utf-8')

    myfile.seek(startofline + offset)  # Move to where old line began, adjusted for offset
    myfile.write(new_line)  # Overwrite existing line with new line
    myfile.truncate()  # If existing line longer than new line, get rid of the excess
Другие вопросы по тегам