首页 > Python > Python 3 标准库实例教程 > 数据压缩和归档

9.3. gzip — GNU zip 文件的读与写

目的:读写 gzip 文件。

gzip 模块提供了类文件的接口用于处理 GNU zip 文件,使用  zlib 压缩或者解压数据。

写压缩文件

模块级别的函数 open()  创建一个类文件的 GzipFile 的实例。这个常用的方法用于读写提供的字节。

gzip_write.py

import gzip
import io
import os

outfilename = 'example.txt.gz'
with gzip.open(outfilename, 'wb') as output:
    with io.TextIOWrapper(output, encoding='utf-8') as enc:
        enc.write('Contents of the example file go here.\n')

print(outfilename, 'contains', os.stat(outfilename).st_size,
      'bytes')
os.system('file -b --mime {}'.format(outfilename))

为了向压缩文件写入数据,以 wb 模式打开文件。这个例子使用来自 io 模块的 TextIOWrapper 包装了 GzipFile,能够将 Unicode 文本编码成适用压缩的字节。

$ python3 gzip_write.py

application/x-gzip; charset=binary
example.txt.gz contains 75 bytes

通过 compresslevel  参数,可以使用不同的压缩级别。compresslevel  有效值范围是0到9(包括0和9)。较小的压缩级别将带来更快的压缩速度,但导致压缩量较小。值越大,压缩越慢,但是压缩比更高。

gzip_compresslevel.py

import gzip
import io
import os
import hashlib

def get_hash(data):
    return hashlib.md5(data).hexdigest()

data = open('lorem.txt', 'r').read() * 1024
cksum = get_hash(data.encode('utf-8'))

print('Level  Size        Checksum')
print('-----  ----------  ---------------------------------')
print('data   {:>10}  {}'.format(len(data), cksum))

for i in range(0, 10):
    filename = 'compress-level-{}.gz'.format(i)
    with gzip.open(filename, 'wb', compresslevel=i) as output:
        with io.TextIOWrapper(output, encoding='utf-8') as enc:
            enc.write(data)
    size = os.stat(filename).st_size
    cksum = get_hash(open(filename, 'rb').read())
    print('{:>5d}  {:>10d}  {}'.format(i, size, cksum))

输出中中间一列的数字表示压缩输入生成的文件中包含字节的多少。对于这个输入数据,较高的压缩值并没有带来理想的压缩效果。结果是变化的,取决于输入。

$ python3 gzip_compresslevel.py

Level  Size        Checksum
-----  ----------  ---------------------------------
data       754688  e4c0f9433723971563f08a458715119c
    0      754793  ced7189c324eb73a8388492a9024d391
    1        9846  5356d357f23e0d5b6d85e920929f0e43
    2        8267  8ce46bce238edc095e47e941cebad93d
    3        8227  91662517459db94a744671a6b4295b67
    4        4167  ad304e3aec585640de9f14306fb32083
    5        4167  4381a5d6dff4dd2746387f20411dcfcd
    6        4167  ef3a05112ea382abb53bc4a5bee3a52a
    7        4167  4723a253d1dc8ddecd4ff7b7adf0bc0b
    8        4167  0e1aeba7bdc39f0007039f130d9a28b2
    9        4167  eccf47c4c4f1cca3274e57a1b9b9ddd2

GzipFile 实例也包含了一个 writelines() 方法,能够被用于写入字符串序列。

gzip_writelines.py

import gzip
import io
import itertools
import os

with gzip.open('example_lines.txt.gz', 'wb') as output:
    with io.TextIOWrapper(output, encoding='utf-8') as enc:
        enc.writelines(
            itertools.repeat('The same line, over and over.\n',
                             10)
        )

os.system('gzcat example_lines.txt.gz')

同常规文件一样,输入的行需要包含一个新行字符,这里是:\n

$ python3 gzip_writelines.py

The same line, over and over.
The same line, over and over.
The same line, over and over.
The same line, over and over.
The same line, over and over.
The same line, over and over.
The same line, over and over.
The same line, over and over.
The same line, over and over.
The same line, over and over.

读取压缩数据

要从先前压缩的文件中读取数据,以二进制模式('rb')打开文件,这样就不会执行基于文本的行尾转换或执行 Unicode 解码。

gzip_read.py

import gzip
import io

with gzip.open('example.txt.gz', 'rb') as input_file:
    with io.TextIOWrapper(input_file, encoding='utf-8') as dec:
        print(dec.read())

这个例子读取上一节中 gzip_write.py 写的文件,在解压缩后使用TextIOWrapper 解码文本。

$ python3 gzip_read.py

Contents of the example file go here.

在读取文件时,也可以仅查找和读取部分数据。

gzip_seek.py

import gzip

with gzip.open('example.txt.gz', 'rb') as input_file:
    print('Entire file:')
    all_data = input_file.read()
    print(all_data)

    expected = all_data[5:15]

    # 回到开头
    input_file.seek(0)

    # 向前移动 5 个字节
    input_file.seek(5)
    print('Starting at position 5 for 10 bytes:')
    partial = input_file.read(10)
    print(partial)

    print()
    print(expected == partial)

seek() 相对于 未压缩的 数据的位置,因此调用者不需要知道数据文件是否被压缩。

$ python3 gzip_seek.py

Entire file:
b'Contents of the example file go here.\n'
Starting at position 5 for 10 bytes:
b'nts of the'

True

处理流

GzipFile  类可以用于去包装其他类型的数据流,因此他们也可以使用压缩。这在通过套接字或者现有的(已打开)文件传输数据时很有用。 BytesIO 缓冲池也可以这么用。

gzip_BytesIO.py

import gzip
from io import BytesIO
import binascii

uncompressed_data = b'The same line, over and over.\n' * 10
print('UNCOMPRESSED:', len(uncompressed_data))
print(uncompressed_data)

buf = BytesIO()
with gzip.GzipFile(mode='wb', fileobj=buf) as f:
    f.write(uncompressed_data)

compressed_data = buf.getvalue()
print('COMPRESSED:', len(compressed_data))
print(binascii.hexlify(compressed_data))

inbuffer = BytesIO(compressed_data)
with gzip.GzipFile(mode='rb', fileobj=inbuffer) as f:
    reread_data = f.read(len(uncompressed_data))

print('\nREREAD:', len(reread_data))
print(reread_data)

使用 GzipFile  而不是 zlib 的一个好处是它支持文件 API。然而,当重新读取先前压缩的数据时,需要给 read() 方法显式传入一个长度。去掉长度将导致一个 CRC 错误,可能是因为 BytesIO 在遇到 EOF 之前反悔了空字符串。处理压缩数据流的时候,要么使用表示要读取的实际数据量的整数作为数据前缀,要么使用zlib中的增量解压缩API。

$ python3 gzip_BytesIO.py

UNCOMPRESSED: 300
b'The same line, over and over.\nThe same line, over and over.\nT
he same line, over and over.\nThe same line, over and over.\nThe
same line, over and over.\nThe same line, over and over.\nThe sam
e line, over and over.\nThe same line, over and over.\nThe same l
ine, over and over.\nThe same line, over and over.\n'
COMPRESSED: 51
b'1f8b080022caae5a02ff0bc94855284ecc4d55c8c9cc4bd551c82f4b2d5248c
c4b0133f4b8424665916401d3e717802c010000'

REREAD: 300
b'The same line, over and over.\nThe same line, over and over.\nT
he same line, over and over.\nThe same line, over and over.\nThe
same line, over and over.\nThe same line, over and over.\nThe sam
e line, over and over.\nThe same line, over and over.\nThe same l
ine, over and over.\nThe same line, over and over.\n'

推荐阅读

  • gzip 标准库文档
  • zlib -- zlib 模块是一个用于 gzip 压缩的低级接口。
  • zipfile -- zipfile 模块用于访问 Zip 文件。
  • bz2 -- bz2  使用了 bz2 压缩格式。
  • tarfile --  tarfile 模块内建支持读取压缩的 tar 文件。
  • io -- 用于创建输入和输出管道的构建快。