Самый эффективный способ изменить последнюю строку большого текстового файла в 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-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