如果这确实是基于行的(不需要真正的 XML 解析器是最佳解决方案),mmap https://docs.python.org/3/library/mmap.html可以在这里提供帮助。
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 页,其余部分永远不会被读取或写入)
- Using
rfind
意味着你可以让Python在C层(在CPython中)快速完成查找换行符的工作;明确的seek
s and read
文件对象的 s 可以匹配“仅读取一页左右”,但您必须手动实现换行符的搜索
Caveat: 这种方法行不通(至少,不是没有修改以避免映射超过 2 GB,并在可能无法映射整个文件时处理调整大小)如果您使用的是 32 位系统并且文件太大而无法映射到内存中。在大多数 32 位系统上,即使在新生成的进程中,也只有 1-2 GB 的可用连续地址空间;在某些特殊情况下,您可能拥有多达 3-3.5 GB 的用户虚拟地址(尽管您会丢失一些用于堆、堆栈、可执行映射等的连续空间)。mmap
不需要太多的物理RAM,但需要连续的地址空间; 64 位操作系统的巨大好处之一是,除了最荒谬的情况外,您不再担心虚拟地址空间,因此mmap
可以解决一般情况下在 32 位操作系统上如果不增加复杂性就无法处理的问题。目前大多数现代计算机都是 64 位的,但如果您的目标是 32 位系统,则绝对需要记住这一点(在 Windows 上,即使操作系统是 64 位,它们也可能已经安装了 32 位版本的 Python)错误,所以同样的问题也适用)。这里还有一个可以在 32 位 Python 上运行的示例(假设最后一行不是 100+ MB 长)(省略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