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

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

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

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

Ответ 1

Обновление: используйте ответ 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()

Ответ 2

Если это действительно что-то линейное (где истинный синтаксический анализатор 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-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