Python - 如何在不出现MemoryError的情况下压缩大型文本文件?

14

我使用以下简单的Python脚本,在EC2 m3.large实例上压缩一个大文本文件(比如10GB)。然而,我总是遇到一个MemoryError错误:

import gzip

with open('test_large.csv', 'rb') as f_in:
    with gzip.open('test_out.csv.gz', 'wb') as f_out:
        f_out.writelines(f_in)
        # or the following:
        # for line in f_in:
        #     f_out.write(line)

我得到的回溯信息是:

Traceback (most recent call last):
  File "test.py", line 8, in <module>
    f_out.writelines(f_in)
MemoryError

我已经阅读了一些关于这个问题的讨论,但还不是很清楚如何处理。能否给我一个更易懂的答案来解决这个问题?


马克的解决方案出了什么精确的错误?它不能是在f_out.writelines,因为你使用的是write - Serge Ballesta
错误信息如下: Traceback (most recent call last): File "test.py", line 8, in <module> for line in f_in: MemoryError - shihpeng
3个回答

19

这里的问题与gzip无关,而是与从一个没有换行符的10GB文件逐行读取有关:

另外,我用于测试Python gzip功能的文件是由fallocate -l 10G bigfile_file生成的。

这会给您提供一个完全由0字节组成的10GB稀疏文件。这意味着没有换行符字节。这意味着第一行长度为10GB。这意味着读取第一行需要10GB的空间。(如果您使用的是Python 3.3之前版本并且尝试将其作为Unicode读取,则可能需要20或40GB内存。)

如果您想复制二进制数据,请不要逐行复制。无论是普通文件、即时解压缩的GzipFilesocket.makefile()还是其他任何东西,都会遇到同样的问题。

解决方案是分块复制。或者只需使用copyfileobj,它会自动为您完成。

import gzip
import shutil

with open('test_large.csv', 'rb') as f_in:
    with gzip.open('test_out.csv.gz', 'wb') as f_out:
        shutil.copyfileobj(f_in, f_out)

默认情况下,copyfileobj 使用的块大小已经经过优化,通常表现良好,而且不会表现得很差。在某些情况下,你可能需要更小或更大的块大小;但是很难预测哪个更好。因此,可以使用不同的 bufsize 参数(例如,从1KB到8MB的4的幂)对 copyfileobj 进行测试,使用 timeit 测量时间。但如果不是做这方面的工作,通常使用默认的16KB就足够了。

* 如果缓冲区太大,则可能会交替进行长时间的I/O和处理。如果太小,则可能需要多次读取才能填充单个gzip块。


11

这很奇怪。如果您试图压缩一个没有包含许多换行符的大型二进制文件,因为这样的文件可能包含一个太大而无法适应您的RAM的“行”,那么我会期望出现这个错误,但是在一个按行结构化的 .csv 文件上不应该发生这种情况。

但是无论如何,逐行压缩文件并不是非常有效的。尽管操作系统缓冲磁盘I / O,但通常读写更大的数据块(例如64 kB)要快得多。

我在这台机器上有2GB的RAM,并且我刚刚成功使用下面的程序压缩了一个2.8GB的tar存档。

#! /usr/bin/env python

import gzip
import sys

blocksize = 1 << 16     #64kB

def gzipfile(iname, oname, level):
    with open(iname, 'rb') as f_in:
        f_out = gzip.open(oname, 'wb', level)
        while True:
            block = f_in.read(blocksize)
            if block == '':
                break
            f_out.write(block)
        f_out.close()
    return


def main():
    if len(sys.argv) < 3:
        print "gzip compress in_file to out_file"
        print "Usage:\n%s in_file out_file [compression_level]" % sys.argv[0]
        exit(1)

    iname = sys.argv[1]
    oname = sys.argv[2]
    level = int(sys.argv[3]) if len(sys.argv) > 3 else 6

    gzipfile(iname, oname, level)


if __name__ == '__main__':  
    main()

我正在使用Python 2.6.6,gzip.open()不支持with语句。


正如Andrew Bay在评论中指出的那样,在Python 3中,if block == '':将无法正确工作,因为block包含的是字节而不是字符串,并且空字节对象与空文本字符串不相等。我们可以检查块大小或将其与b''进行比较(这也适用于Python 2.6+),但简单的方法是if not block:


1
@shihpeng:我不熟悉fallocate,所以这只是一个猜测,但也许Python的gzip不喜欢这样的文件,因为它们不包含任何实际数据。我无法测试它,因为我仍在使用不支持fallocate的ext3系统。然而,我的程序使用使用truncate创建的大文件可以正常工作,该命令创建稀疏文件。 - PM 2Ring
尽管操作系统缓冲磁盘I/O,但通常读写较大的数据块(例如64 kB)会更快。GzipFile使用缓冲I/O(在Python 2.x中使用C stdio缓冲区,在3.x中使用Python io缓冲区)。只有当它尝试解压另一个zlib块并且该块超出缓冲区时,才从磁盘读取。因此,它已经在做你在这里尝试做的一切。唯一的区别是您正在使用更大的块大小;如果实际上有帮助,您可以手动打开文件并从中构建GzipFile,而不是使用gzip.open - abarnert
@abarnert:感谢您提供有关缓冲的信息。是的,当我讨论“fallocate”时,我暂时忘记了我所说的关于换行符的事情。 :) :oops: - PM 2Ring
1
在Python 3.x中,不再使用if block == ''来判断块是否为空,而是使用块的长度来确定。这是因为该字符串是unicode格式,无法与该块进行比较。 - AndrewBay
@AndrewBay 感谢提醒。我已经在我的答案中添加了一些信息。我不会为Python 3重新编写这段代码。我想它仍然是一个有用的例子,但是Andrew Barnert的答案中的技术更加优越。 - PM 2Ring
显示剩余3条评论

4

即使按行读取文件,仍然出现内存错误是很奇怪的。我猜测这可能是由于可用内存非常少,而行数非常大导致的。此时应该使用二进制读取:

import gzip

#adapt size value : small values will take more time, high value could cause memory errors
size = 8096

with open('test_large.csv', 'rb') as f_in:
    with gzip.open('test_out.csv.gz', 'wb') as f_out:
        while True:
            data = f_in.read(size)
            if data == '' : break
            f_out.write(data)

您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - shihpeng
这只复制了前8KB。 - abarnert
现在它将无限循环,在EOF之后永远写空字符串。 - abarnert
现在将其与 copyfileobj(f_in,f_out,size) 进行比较,后者不需要进行3次修复即可正确执行(因为 copyfileobj 已经编写、测试和优化,并在过去几十年中用于数千个项目),并且更易于阅读... - abarnert
1
@SergeBallesta:你应该看看我写的一些180行的怪物,我花了几个小时调试,才意识到我复制了stdlib中免费提供的东西。 :) - abarnert
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接