如何在 python 中列出 gz 文件的内容而不解压缩它?

How do I list contents of a gz file without extracting it in python?

我有一个 .gz 文件,我需要使用 python.

获取其中的文件名

这个问题和this one一样

唯一的区别是我的文件是 .gz 而不是 .tar.gz 所以 tarfile 库在这里没有帮助我

我正在使用 requests 库来请求 URL。响应是一个压缩文件。

这是我用来下载文件的代码

response = requests.get(line.rstrip(), stream=True)
        if response.status_code == 200:
            with open(str(base_output_dir)+"/"+str(current_dir)+"/"+str(count)+".gz", 'wb') as out_file:
                shutil.copyfileobj(response.raw, out_file)
            del response

例如,此代码下载名称为 1.gz 的文件。现在,如果我用存档管理器打开文件,文件将包含类似 my_latest_data.json

的内容

我需要提取文件,输出为 my_latest_data.json

这是我用来提取文件的代码

inF = gzip.open(f, 'rb')
outfilename = f.split(".")[0]
outF = open(outfilename, 'wb')
outF.write(inF.read())
inF.close()
outF.close()

outputfilename变量是我在脚本中提供的字符串,但我需要真实的文件名(my_latest_data.json)

你不能,因为 Gzip 不是一种存档格式。

这本身就是一个废话解释,所以让我比我在评论中做的更详细一点...

只是压缩

"just a compression system" 意味着 Gzip 对输入字节(通常来自文件)进行操作并输出压缩字节。您无法知道里面的字节是代表多个文件还是只代表一个文件——它只是一个已被压缩的字节流。例如,这就是您可以通过网络接受 gzip 压缩数据的原因。它的 bytes_in -> bytes_out.

什么是清单?

清单是存档中的 header,充当存档内容的 table。请注意,现在我使用的是 "archive" 而不是 "compressed stream of bytes"。存档意味着它是清单所引用的 collection 文件或段 -- 压缩的字节流 只是 字节流。

Gzip 里面到底有什么?

对 .gz 文件内容的稍微简化的描述是:

  1. 一个 header 带有一个特殊数字来表示它的 gzip、版本和时间戳(10 字节)
  2. 可选headers;通常包括原始文件名(如果压缩 target 是一个文件)
  3. body -- 一些压缩的负载
  4. 最后一个 CRC-32 校验和(8 字节)

就是这样。没有清单。

另一方面,存档格式内部会有一个清单。这就是 tar 库的用武之地。 Tar 只是一种将一堆位推到一个文件中的方法,并在前面放置一个清单,让您知道原始文件的名称文件以及它们在连接到存档中之前的大小。因此,.tar.gz 如此普遍。

有些实用程序允许您一次解压缩 gzip 文件的一部分,或者只在内存中解压缩它,然后让您检查清单或其中可能包含的任何内容。但是任何清单的详细信息都特定于其中包含的存档格式。

请注意,这与 zip 存档不同。 Zip 一种存档格式,因此包含清单。 Gzip 是一个压缩 库,就像 bzip2 和朋友一样。

如另一个答案所述,只有我去掉复数形式,你的问题才有意义:“我有一个 .gz 文件,我需要获取 文件的名称 在其中使用 python."

gzip header 中可能有也可能没有文件名。 gzip 实用程序通常会忽略 header 中的名称,并解压缩到与 .gz 文件同名的文件,但删除 .gz。例如。您的 1.gz 将解压缩为名为 1 的文件,即使 header 中的文件名为 my_latest_data.json。 gzip 的 -N 选项将使用 header 中的文件名(以及 header 中的时间戳),如果有的话。所以 gzip -dN 1.gz 会创建文件 my_latest_data.json,而不是 1.

您可以通过手动处理header在Python中找到header中的文件名。您可以在 gzip specification.

中找到详细信息
  1. 验证前三个字节是1f 8b 08
  2. 保存第四个字节。称之为 flags。如果 flags & 8 为零,则放弃 -- header.
  3. 中没有文件名
  4. 跳过接下来的六个字节。
  5. 如果 flags & 2 不为零,则跳过两个字节。
  6. 如果flags & 4不为零,则读取接下来的两个字节。考虑到它们是小端顺序,从这两个字节中生成一个整数,称之为 xlen。然后跳过 xlen 字节。
  7. 我们已经知道 flags & 8 不为零,所以您现在位于文件名处。读取字节直到达到零字节。直到但不包括零字节的那些字节是文件名。

注意:此答案自 Python 3 起已过时。


使用 Mark Adler 回复中的提示和对 gzip 模块的一些检查,我设置了从 gzip 文件中提取内部文件名的函数。我注意到 GzipFile 对象有一个名为 _read_gzip_header() 的私有方法,它几乎可以获取文件名,所以我基于那个

import gzip

def get_gzip_filename(filepath):
    f = gzip.open(filepath)
    f._read_gzip_header()
    f.fileobj.seek(0)
    f.fileobj.read(3)
    flag = ord(f.fileobj.read(1))
    mtime = gzip.read32(f.fileobj)
    f.fileobj.read(2)
    if flag & gzip.FEXTRA:
        # Read & discard the extra field, if present
        xlen = ord(f.fileobj.read(1))
        xlen = xlen + 256*ord(f.fileobj.read(1))
        f.fileobj.read(xlen)
    filename = ''
    if flag & gzip.FNAME:
        while True:
            s = f.fileobj.read(1)
            if not s or s=='[=10=]0':
                break
            else:
                filename += s
    return filename or None

The Python 3 gzip library discards this information 但您可以采用 link 周围的代码来做其他事情。

如本页其他答案所述,此信息无论如何都是可选的。但如果你需要查看它是否存在,也不是不可能检索。

import struct


def gzinfo(filename):
    # Copy+paste from gzip.py line 16
    FTEXT, FHCRC, FEXTRA, FNAME, FCOMMENT = 1, 2, 4, 8, 16
    
    with open(filename, 'rb') as fp:
        # Basically copy+paste from GzipFile module line 429f
        magic = fp.read(2)
        if magic == b'':
            return False

        if magic != b'73':
            raise ValueError('Not a gzipped file (%r)' % magic)

        method, flag, _last_mtime = struct.unpack("<BBIxx", fp.read(8))

        if method != 8:
            raise ValueError('Unknown compression method')

        if flag & FEXTRA:
            # Read & discard the extra field, if present
            extra_len, = struct.unpack("<H", fp.read(2))
            fp.read(extra_len)
        if flag & FNAME:
            fname = []
            while True:
                s = fp.read(1)
                if not s or s==b'[=10=]0':
                    break
                fname.append(s.decode('latin-1'))
            return ''.join(fname)
        
def main():
    from sys import argv
    for filename in argv[1:]:
        print(filename, gzinfo(filename))

if __name__ == '__main__':
    main()

这用一个模糊的 ValueError 异常替换了原始代码中的异常(如果你打算更广泛地使用它,你可能想要修复它,并将它变成一个合适的模块,你可以 import) 并使用通用的 read() 函数而不是特定的 _read_exact() 方法,该方法会遇到一些麻烦以确保它得到它请求的字节数(如果你想要的话,这也可以取消到).