DEFLATE 解剖:LZ77 与霍夫曼如何压掉七成体积,gzip 头尾字段逐个讲
三份 RFC,一个 .gz
日常说的「gzip 压缩」其实是三层协议的叠罗汉:最外层是 RFC 1952 定义的 gzip 容器——文件头、可选字段、数据、校验尾;中间是 RFC 1951 定义的 DEFLATE 算法——真正的压缩发生地;再往里还有一层概念上的 LZ77 与霍夫曼编码的组合。zlib(RFC 1950)是兄弟格式,只是头尾不同、算法共用。一个 .gz 文件从第一字节到最后一字节,全部逻辑就落在这几十页规范里:
| |
头十字节是固定的:魔数 1F 8B 开头,CM 指明压缩方法(8 = DEFLATE,至今也是唯一值),FLG 是标志位,MTIME 是 Unix 秒级时间戳(可以为 0),XFL 暗示压缩级别,OS 标记生产平台。FLG 里五个位分别声明后面跟着哪些可选字段:文件名、注释、扩展段、头部 CRC、文本标志——命令行 gzip foo.txt 生成的文件里通常能看到内嵌的原始文件名,就是 FNAME 位的产物。
尾部八字节常被忽视却很有用:CRC32 是未压缩原始数据的校验和——这意味着解压完才能验证完整性,而不是下载时;ISIZE 是原始长度对 2³² 取模,超过 4 GB 的文件这里会回绕,解压器不能拿它当真实大小,只能当完整性线索。
DEFLATE:先找重复,再压字符
DEFLATE 的压缩分两步接力。第一步 LZ77:在一个 32 KB 的滑动窗口里找重复。编码器看到「www.util.cn」中第二个 w 时,窗口里已经有 3 个连续的 w,就可以不输出字符,改输出一个三元组的后两元——「往回看 3 个字节,抄 3 个字节」(距离 3、长度 3)。规范限定匹配长度 3 到 258 字节、距离 1 到 32768 字节,正好覆盖 32 KB 窗口的每个位置。
第二步霍夫曼:LZ77 的输出已经变成了两类符号——没被匹配到的字面字节(0–255),和「匹配」记号(257–285,附上长度与距离的额外比特)。霍夫曼编码给高频符号短码、低频符号长码,两类符号各建一棵码表,文本场景下仅这一步就能再省三成。
关键在于码表怎么传给解码器。DEFLATE 把数据切成块,每块开头两个位声明类型:00 存储块(不压缩,纯搬运,最多 65535 字节,适合已经压不动的数据);01 固定霍夫曼(双方使用规范里写死的码表,零开销建表);10 动态霍夫曼(块头携带为本块数据量身定制的码表)。动态块的头是整个格式最绕的部分:HLIT/HDIST/HCLEN 三个位数组出字面码表长度、距离码表长度、以及「码长码表」本身的长度;码长码表的读入顺序不是 0,1,2…而是 16,17,18,0,8,7,9…——高频的码长符号被规范作者人为排到前面,配合 3 比特定长读入尽量省位;随后真正的码长序列还要经过一轮游程编码(符号 16 复制前一码长 3–6 次、17/18 表示连续零)。为了传码表省几十个字节,规范设计者把两层编码套了三层元数据,这是「压缩比洁癖」的极致样本。
有个跨格式的彩蛋:DEFLATE 的位流按**字节内低位在前(LSB first)**打包,与 GIF 的 LZW 如出一辙(参见《GIF 格式解剖》)。九十年代前后的压缩格式在位序上出奇一致,读错方向解出的就是纯乱码。
浏览器原生的一行流式 API
站点 🔧 gzip 压缩解压
没有引任何压缩库,核心就一行:new Blob([bytes]).stream().pipeThrough(new CompressionStream('gzip'))。浏览器在 Chrome 80+、Firefox 113+、Safari 16.4+ 里原生实现了完整的 zlib 管线,流式接口意味着内存占用与文件大小解耦,大文件也不会一次性读入。
但原生 API 有两条边界要认。其一,没有压缩级别参数:命令行 gzip -6、-9 可以在速度与压缩率之间取舍,CompressionStream 只用实现的默认级别,所以工具压出的文件与 gzip -9 字节级不一致是正常的——格式兼容,参数不同。其二,多成员 gzip 只解第一个成员:gzip 容器允许把多个 DEFLATE 流首尾相接(cat a.gz b.gz > c.gz 是合法操作),解压器按成员逐个处理,而原生流在第一个成员的 ISIZE 之后便宣告完成,不会继续读后面粘着的成员。工具在实现里明确标注了这个行为,遇到多成员文件宁可提示用户也不要静默丢数据。
还有一条判断逻辑值得一讲:解压结果是文本还是二进制?工具用 TextDecoder('utf-8', { fatal: true }) 做严格解码——任何非法 UTF-8 序列都会抛异常,据此把结果分成「可预览文本」和「显示十六进制头 + 下载」两路。比魔数猜测更可靠:一个 .gz 里装的可能是 JSON 也可能是图片,解码器自己就是最准的探针。
为什么图片压不动了
「把 zip 再压一遍」「把 MP3 转成 zip 变小」是新手最常问的两个问题。答案在信息论那头:JPEG、MP3、zip 的产出已经是熵编码后的高熵数据,每个字节看起来都像随机数,LZ77 找不到重复,霍夫曼无频差可利用。实测口径也是如此:重复度高的文本通常压到原大小的 20%–40%,而已压缩格式再压一次,结果接近 100% 甚至反超(容器开销)。反过来,日志、CSV、JSON、代码这类人类可读文本冗余度极高,是 DEFLATE 的主场。
想判断一个来历不明的文件是不是压缩包,魔数比后缀可靠:1F 8B 认 gzip,PK\x03\x04 认 ZIP 家族(docx、epub、apk 都是它)。🔧 文件类型识别 做的就是逐字节比对签名这件事,把「改了后缀的 exe」当场揭穿。
压缩、编码与格式识别这条线,连同更多文件处理工具,收在文件处理流水线专题里,全部本地完成,文件不出浏览器。