Tarfile在第一个常规文件后停止

1 投票
3 回答
1091 浏览
提问于 2025-04-17 16:06

我有一些 .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 个回答

0

出于好奇,把原来的提问者代码改成下面这样也能正常工作,虽然@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
1

我不知道为什么 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
3

一个解决方法是直接用 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

这样更清晰,我敢打赌也会快一点。

撰写回答