Tarfile在第一个常规文件后停止
我有一些 .tar.bz2
文件,里面装了很多小的 json
文件。每个压缩包里可能有几千个这样的文件,而且这些 json
文件都很小(通常小于10KB,有时候甚至小于1KB)。所以,压缩后的单个文件大小不会超过100KB。
根据文档,下面这个函数应该能返回一个迭代器,这个迭代器可以遍历压缩包里的所有常规文件,并返回它们的tarinfo结构和数据。
import tarfile
def tariter(filename):
with tarfile.open(filename) as archive:
while True:
tarinfo = archive.next()
if tarinfo is None:
break
if tarinfo.isreg():
handle = archive.extractfile(tarinfo.name)
data = handle.read()
handle.close()
yield tarinfo, data
但是实际上,它只返回一个迭代器,这个迭代器只返回第一个文件(连同内容),然后就停止了。显然,archive.next()
在读取第二个文件后返回了None,尽管压缩包里还有很多文件。
我这个代码里是不是有bug呢?
3 个回答
出于好奇,把原来的提问者代码改成下面这样也能正常工作,虽然@upside的代码更有道理。
import tarfile
def tariter(filename):
with tarfile.open(filename) as archive:
it = archive.__iter__() # CHANGE
while True:
tarinfo = it.next() # CHANGE
if tarinfo is None:
break
if tarinfo.isreg():
handle = archive.extractfile(tarinfo.name)
data = handle.read()
handle.close()
yield tarinfo, data
我不知道为什么 next()
会出错(我在本地也遇到过这个问题),不过这个方法可以正常工作(而且看起来更简洁):
import tarfile
def tariter(filename):
with tarfile.open(filename) as archive:
for tarinfo in archive:
if tarinfo.isreg():
handle = archive.extractfile(tarinfo.name)
data = handle.read()
handle.close()
yield tarinfo, data
一个解决方法是直接用 extractfile
和 tarinfo,而不是用文件名。这样做是有效的:
def tariter(filename):
with tarfile.open(filename) as archive:
while True:
tarinfo = archive.next()
if tarinfo is None:
break
if tarinfo.isreg():
handle = archive.extractfile(tarinfo) # LINE CHANGED
data = handle.read()
handle.close()
yield tarinfo, data
至于 为什么 会这样:TarFile.next()
并没有按照迭代器的规则来工作,因为它返回的是 None
,而不是抛出 StopIteration
。
迭代器的规则分为两个部分:一个是容器的“外部”部分,它返回一个迭代器;另一个是“内部”部分,就是迭代器本身。
容器必须实现 __iter__()
,这个方法返回一个 新的 对象,也就是迭代器。TarFile.__iter__()
返回一个新的 TarIter
对象。
迭代器本身(TarIter
)实现了 __iter__()
(这个方法总是返回 self
)和 next()
。它还必须有自己独立的索引,用来指向原始容器中的项目。这样,你就可以在同一个容器上生成多个不同的迭代器,而不会互相干扰。
然而,TarFile.next()
并没有使用独立的索引来进行迭代,所以如果其他人使用 TarFile
提供的伪迭代协议,就会搞乱迭代的顺序。
这似乎就是这里发生的事情。TarFile.extractfile(filename)
在当前的 TarFile
中查找匹配的文件时,使用的是 TarFile.next()
,而不是你使用的 TarFile.__iter__()
。这会破坏“下一个项目”的索引,导致 archive.next()
在第一次调用 extractfile()
后返回 None
。
不过,如果你使用 extractfile(tarinfo)
,那么 tarinfo
对象中有足够的元数据,TarFile
就可以直接提取内容,而不需要在 archive
对象中查找匹配的文件名。因此,archive.extractfile(tarinfo)
可能比 archive.extractfile(tarinfo.name)
更快。
一般来说,集合对象(像 TarFile
)不应该自己进行迭代,而是应该生成一个新的对象来进行迭代。仅仅存在 TarFile.next()
就让人觉得设计得不好。也许这样做有它的理由,但 你 不必使用它!
可以这样做:
def tariter(filename):
with tarfile.open(filename) as archive:
# use TarIter object for iteration over archive
for tarinfo in archive:
if tarinfo.isreg():
handle = archive.extractfile(tarinfo)
data = handle.read()
handle.close()
yield tarinfo, data
这样更清晰,我敢打赌也会快一点。