gzip 里面到底是什么?这篇自己写一个 inflate 解码器,拆开真实的 gzip 文件:头和尾、deflate 的块、动态 Huffman 块头里存了什么、压缩器如何选块类型,以及 gzip 头里的时间戳坑。
总长 41935 字节 = 头 10 + deflate 41917 + 尾 8;头:魔数 1f8b、方法 8(8 = deflate)、标志 0x0、mtime 1760000000、xfl 2、操作系统 255 尾:CRC32 0xecc54463(我自己用 binascii.crc32 算 0xecc54463:一致)、原始长度 200000(等于输入长度 200000) 同样数据,zlib 格式(2 字节头 + 4 字节 Adler-32 尾):41923 字节;raw deflate:41917 字节;三种封装差 18 / 6 字节
10 字节头(魔数 1f 8b、方法 8、标志、mtime、xfl、系统)、中间是 deflate 数据、8 字节尾(CRC32 和原始长度)。CRC32 保护解压后的数据;zlib 格式则是 2 字节头加 4 字节 Adler-32。
压缩后 214172 字节,解出 1048576 字节,与原文一致(纯 Python 解码用时 0.3 秒);共 8 个块
块 类型 输出字节 块占压缩字节 字面量 匹配 匹配覆盖 块头位数
0 动态 133878 28590 7314 9069 94.5% 558
1 动态 149154 30094 6538 9845 95.6% 414
2 动态 148447 29985 6611 9772 95.5% 418
3 动态 147924 30147 6489 9894 95.6% 407
4 动态 147113 29924 6633 9750 95.5% 411
5 动态 148216 30106 6537 9846 95.6% 409
6 动态 148579 30080 6543 9840 95.6% 414
7 动态 25265 5245 1178 1693 95.3% 388
总计:字面量 47843 个,匹配 69709 个,匹配覆盖 95.4% 的输出deflate 流由若干个块组成,每个块自带 Huffman 树。1 MiB 的 products 压成 8 个动态块,每块输出约 14.8 万字节、压缩后约 3 万字节。各块互相独立地建树,可以适应数据的变化。
HLIT = 279(字面量/长度码 279 个,257 起)、HDIST = 30、HCLEN = 17(码长码的 19 个中存了前 17 个,顺序 [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]) 块头共 558 位 = 70 字节,占这个块压缩后 28590 字节的 0.2%;用到 82 个字面量/长度符号、30 个距离符号;字面量/长度码最长 14 位 所有动态块的块头合计 427 字节,占全部压缩数据的 0.2%
动态块的块头先存“码长码”的码长,再用它存字面量/长度树和距离树的码长(第 10 篇讲的规范 Huffman 只需要码长)。第一个块的块头 70 字节,占块的 0.2%,可以忽略;块很短时才会明显。
zlib 级别 9:1 个块,字面量 7186 个,匹配 8882 个,匹配覆盖 94.5%,平均匹配长度 13.9 字节,压缩后 28019 字节 (第 11 篇我的懒匹配 chain 64 是 14994 个 token:字面量 5351 个,匹配 9643 个,平均长度 13.0 字节,估算 28234 字节)
zlib 对同样的 128 KiB 选了 7186 个字面量和 8882 个匹配,我的懒匹配选了 5351 个和 9643 个,压缩后差不到 1%。LZ77 + Huffman 的框架里有很多合理的取舍。
输入 原字节 deflate 后 块类型序列(0 存储 / 1 固定 / 2 动态) 空输入 0 2 1(共 1 块) 1 个字节 1 3 1(共 1 块) "hello world" 11 13 1(共 1 块) 50 字节的短文本 50 48 1(共 1 块) 300 字节的 JSON 300 225 2(共 1 块) 2000 字节的 JSON 2000 791 2(共 1 块) 随机 5000 字节 5000 5005 0(共 1 块) 随机 1 MiB 1048576 1048896 000000000000000000000000000000…(共 64 块) 全零 1 MiB 1048576 1033 2(共 1 块)
压缩器会为每个块选三种方式里最小的:存储、固定 Huffman、动态 Huffman。随机数据一块只多 5 字节;很短的输入用固定码,连 hello world 都会变长。
gzip.compress 默认参数,间隔 1.1 秒压缩两次:字节完全相同(头里的 mtime 是 00000000 和 00000000) gzip.GzipFile 默认 mtime,间隔 1.1 秒写两次:字节不同,不同的位置 [4](都在头里 4–7 字节的 mtime:9b8bcc6a 对 9c8bcc6a);mtime=0 写两次:字节完全相同 两个 gzip 文件直接拼在一起仍是合法的 gzip:gzip.decompress 得到 b'first second'(共 52 字节,每个成员各有 10 字节头 + 8 字节尾)
gzip 头里有 mtime,gzip.GzipFile 默认写当前时间,同一份数据不同时刻压缩的字节不同。需要稳定的字节(校验和、缓存、内容寻址)时显式传 mtime=0。gzip.compress 在 Python 3.14 里默认 mtime 为 0。