快速将文件数据移至StringIO
在Python中,我有一个文件流,我想把其中的一部分复制到一个StringIO
对象里。我希望这个过程尽可能快,并且尽量减少复制的次数。
但是如果我这样做:
data = file.read(SIZE)
stream = StringIO(data)
我觉得这里进行了两次复制,对吧?一次是从文件复制到数据,另一次是从StringIO
复制到它内部的缓冲区。我能避免其中一次复制吗?我不需要临时的data
,所以我觉得只需要一次复制就够了。
4 个回答
也许你在寻找的是一个 缓冲区/内存视图:
>>> data = file.read(SIZE)
>>> buf = buffer(data, 0, len(data))
这样你就可以访问原始数据的一部分,而不需要复制它。不过,你必须只对以字节为单位的数据感兴趣,因为缓冲协议就是提供这种格式的数据。
你可以在这个相关的 问题中找到更多信息。
补充一下,在这篇我通过reddit找到的 博客文章中,提供了关于同样问题的更多信息:
>>> f = open.(filename, 'rb')
>>> data = bytearray(os.path.getsize(filename))
>>> f.readinto(data)
根据作者的说法,不会创建额外的复制,并且数据可以被修改,因为 bytearray
是可变的。
不,实际上并没有额外的副本被创建。用来存储数据的缓冲区是一样的。data
和通过StringIO.getvalue()
访问的内部属性其实是同一份数据的不同名称。
Python 2.7 (r27:82500, Jul 30 2010, 07:39:35)
[GCC 4.1.2 20080704 (Red Hat 4.1.2-48)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import StringIO
>>> data = open("/dev/zero").read(1024)
>>> hex(id(data))
'0xea516f0'
>>> stream = StringIO.StringIO(data)
>>> hex(id(stream.getvalue()))
'0xea516f0'
快速浏览一下源代码可以看到,cStringIO
在创建时也不会复制数据,但在调用cStringIO.getvalue()
时会创建一个副本,所以我不能重复上面的演示。
简单来说:使用StringIO时,你无法避免进行两次复制。
一些假设:
- 你在使用cStringIO,否则这样优化就没意义了。
- 你关注的是速度,而不是内存效率。如果不是,可以看看Jakob Bowyer的解决方案,或者如果你的文件是二进制的,可以使用
file.read(SOME_BYTE_COUNT)
的变体。 - 你已经在评论中提到过这一点,但为了完整性:你想实际编辑内容,而不仅仅是查看它。
详细回答:由于Python中的字符串是不可变的,而StringIO的缓冲区是可变的,因此迟早会需要进行一次复制;否则你就会在修改一个不可变的对象!为了实现你想要的功能,StringIO对象需要有一个专门的方法,可以直接从作为参数传入的文件对象中读取。但目前并没有这样的办法。
在StringIO之外,有一些解决方案可以避免额外的复制。随便想想,这个方法可以直接将文件读入一个可修改的字节数组,不需要额外的复制:
import numpy as np
a = np.fromfile("filename.ext", dtype="uint8")
根据你打算如何使用,这可能会有点麻烦,因为它是一个从0到255的值数组,而不是字符数组。但它在功能上等同于StringIO对象,使用np.fromstring
、np.tostring
、np.tofile
和切片表示法应该能满足你的需求。你可能还需要np.insert
、np.delete
和np.append
。
我相信还有其他模块可以实现类似的功能。
时间测试:
这一切到底有多重要呢?让我们看看。我创建了一个100MB的文件largefile.bin
。然后我用这两种方法读取文件并修改第一个字节。
$ python -m timeit -s "import numpy as np" "a = np.fromfile('largefile.bin', 'uint8'); a[0] = 1" 10 loops, best of 3: 132 msec per loop $ python -m timeit -s "from cStringIO import StringIO" "a = StringIO(); a.write(open('largefile.bin').read()); a.seek(0); a.write('1')" 10 loops, best of 3: 203 msec per loop
所以在我的例子中,使用StringIO比使用numpy慢了50%。
最后,为了比较,直接编辑文件:
$ python -m timeit "a = open('largefile.bin', 'r+b'); a.seek(0); a.write('1')" 10000 loops, best of 3: 29.5 usec per loop
所以,这几乎快了4500倍。当然,这非常依赖于你打算对文件做什么。修改第一个字节并不能代表全部。但使用这种方法,你在其他两种方法上有了先机,而且由于大多数操作系统对磁盘有良好的缓存,速度也可能非常不错。
(如果你不被允许编辑文件,因此想避免制作工作副本的成本,还有一些可能的方法可以提高速度。如果你可以选择文件系统,Btrfs有一个写时复制的文件复制操作——使得复制文件的过程几乎是瞬间完成的。使用任何文件系统的LVM快照也可以实现同样的效果。)