一、先分清三个响应头在说什么
拿到一段"看不懂"的响应体,第一步不是猜编码,而是看响应头。最容易混淆的是这三个头,它们管的是完全不同的事:
|---------------------|--------------------|-----------------------------------------------------------|---------------|
| 响应头 | 管什么 | 值的含义 | 谁负责处理 |
| Content-Type | 内容的媒体类型 | application/json、image/png、application/octet-stream | 决定如何解析内容本身 |
| Content-Encoding | 内容被压缩了几次、用什么算法 | gzip、br、zstd、gzip, br | 需要先解压才能看到真实内容 |
| Transfer-Encoding | 传输层的分帧方式 | chunked | 需要先重组分块 |
三个头的处理顺序是:先按 Transfer-Encoding 重组字节流 → 再按 Content-Encoding 逐层解压 → 最后按 Content-Type 解析内容。顺序错了就全是乱码。
还有一个必须说清楚的前提:你手里的字节是"原始抓包流"还是"客户端已处理过的结果",决定了要不要手动解压。
绝大多数"响应体是乱码"的问题,其实是在已解压的数据上又做了一次解压 ,或者反过来:拿到了原始流却直接当文本处理。判断依据很简单------Content-Encoding 头存在,而 r.content 已经是可读 JSON,说明客户端已经处理过了。
Content-Encoding 也可以多值:
含义是先 gzip 压缩、再 brotli 压缩。解压时必须逆序进行:先解 brotli,再解 gzip。
二、Base64
它到底做了什么
Base64 不是加密,是把二进制数据映射成可打印字符:每 3 个字节(24 bit)拆成 4 组、每组 6 bit,每个 6 bit 值(0--63)查表得到一个字符。
所以编码后体积会膨胀约 33% (3 字节变 4 字符)。这个代价是它存在的意义:让二进制数据能安全穿过那些只处理文本的环节------JSON 字段、URL 参数、邮件正文、HTML 内联资源。
三种变体
|--------|-------------|-------|----------------|
| 变体 | 第 63、64 个字符 | 填充 | 典型场景 |
| 标准 | + / | 有 = | 通用、JSON 中嵌入二进制 |
| URL 安全 | - _ | 通常省略 | JWT、URL 参数、文件名 |
| 无填充 | --- | 无 | 部分紧凑实现、JWT |
识别特征
- 字符集严格落在
A-Z a-z 0-9 + / =(标准)或A-Z a-z 0-9 - _(URL 安全)内;
- 长度是 4 的倍数,结尾 0~2 个
=;
- 常见前缀:
data:image/png;base64,、Basic(HTTP Basic 认证)、JWT 的三段式xxx.yyy.zzz。
解码实现
注意 validate 参数的行为:base64.b64decode 默认 validate=False,会静默丢弃 所有不在码表里的字符。这在清洗不干净时能"蒙对",但也会掩盖真正的数据错误------比如截断了、混入了别的字符,你不会收到任何提示。调试阶段建议显式传 validate=True,让 binascii.Error 暴露问题。
常见坑
1)自定义码表。有些实现会打乱整张码表(把标准表 shuffled 成另一套 64 字符序列)。特征很明确:标准工具解出来是乱码,但字符集和长度分布完全符合 Base64 的统计特征。处理方式只能是从客户端逻辑里还原码表,然后建映射:
2)"看起来像 Base64 但不是"。以下几类字符串容易误判:
- JWT:是三段 Base64URL,但第三段是签名,不是数据;
- UUID :
550e8400-e29b-41d4-a716-446655440000,含-和 hex 字符,但长度不是 4 的倍数;
- 纯 Hex 字符串 :如果串里只有
0-9a-f,先按 Hex 处理更合理;
- 加密密文 :Base64 只是外层包装,解开后仍是二进制密文,需要密钥才能继续。这一点必须分清:解开 Base64 不等于解开加密。
3)多层嵌套 。Base64 → 解压 → 再 Base64 这类链路很常见,不要指望一次解开。判断依据:解开一层后如果又是符合 Base64 特征的可打印串,继续剥。
4)Base64 之后是二进制而非文本 。b64decode 返回的是 bytes,直接 .decode('utf-8') 可能抛 UnicodeDecodeError------如果原始内容是图片、protobuf、密文,本来就不是 UTF-8。
三、Hex 十六进制
识别与解码
每字节用两个十六进制字符表示,体积膨胀 100%(1 字节变 2 字符),比 Base64 大一倍。常见于哈希值、IV、签名、调试日志、抓包工具的字节视图。
先把非 hex 字符全部剔除,比逐个 replace 更稳:能同时处理 aa:bb:cc、aa bb cc、0xaa, 0xbb 这类写法。
命令行等价操作:
常见坑
1)奇偶长度 。合法 Hex 一定是偶数。奇数长度基本意味着数据被截断或前置/后置了额外字符------这是很有价值的信号,不要直接补 0 强行解码。
2)字节序(endianness)。这是原文完全没提、但实际最容易出错的点。多字节数值在内存中的字节排列有两种方式:
如果 Hex 串表示的是数值而不是字节序列(比如长度字段、序列号、偏移量),必须确认字节序。网络协议基本都是大端;但从内存 dump、某些二进制文件格式里取出的可能是小端。
3)Hex dump 的附加列。工具输出的 hex 视图通常带偏移量列和 ASCII 预览列:
直接把整行喂给 bytes.fromhex 会因为偏移量 00000000 和 ASCII 列而失败或得出错误结果。要只截取中间的 hex 列。
4)大小写 。bytes.fromhex 兼容大小写混合,但部分自研解析函数只认小写,遇到异常先统一 lower()。
四、压缩流
魔数对照表
Content-Encoding 头缺失或被改写时,用前几个字节判断:
|-------------|---------------------------------------|-----------|------------------------------|
| 格式 | 开头字节 | HTTP 头标识 | 说明 |
| gzip | 1F 8B 08 | gzip | 最常见,第三字节 08 是 deflate 方法标识 |
| zlib | 78 01 / 78 5E / 78 9C / 78 DA | deflate | 带 2 字节头和 4 字节 Adler-32 校验 |
| raw deflate | 无固定头 | deflate | 裸 deflate 数据,只能靠试探 |
| brotli | 无统一魔数 | br | 压缩率高于 gzip,需实际尝试解压 |
| zstd | 28 B5 2F FD | zstd | 高速,新兴格式 |
| PNG | 89 50 4E 47 | --- | 遇到它说明你拿到的是图片本体,不是压缩流 |
| ZIP | 50 4B 03 04 | --- | 同上 |
78 xx 的第二字节是 zlib 的 FLEVEL 标志位组合,9C 是默认压缩、DA 是最高压缩、01 是不压缩/最快。识别时看到 78 开头基本可以锁定 zlib。
解码实现
关于多成员 gzip:gzip 格式允许多个 member 拼接成一个流。gzip.decompress 在不同 Python 版本上对多成员的处理行为不一致,用 GzipFile 包 BytesIO 读取是最稳的写法,两种情况下都能得到完整结果。
常见坑
1)deflate 的历史歧义 。这是最经典的坑。HTTP 规范里 Content-Encoding: deflate 指的是 zlib 格式 (RFC 1950),但历史上大量服务器发送的是裸 deflate(RFC 1951)。客户端要两种都试:
-zlib.MAX_WBITS(即 -15)表示"不带 zlib 头"。这个负数是 zlib 库约定的窗口位取反写法。
2)chunked 分块未重组 。Transfer-Encoding: chunked 的响应体格式是:
直接对原始字节解压必然失败。重组逻辑:
好消息是:绝大多数 HTTP 客户端库已经处理了这一步。只有当你直接处理抓包文件、原始 socket 数据,或服务端自己实现了分块响应时,才需要手动做。
3)解压炸弹(decompression bomb)。一段几百 KB 的 gzip 数据可能解压出几个 GB。处理不可信来源的压缩数据时必须限制输出大小:
decompressobj(data, max_length) 会在达到上限时停止,unconsumed_tail 非空即表示被截断。这个写法在解析外部数据时是必要的防护。
4)多层压缩 。Content-Encoding: gzip, br 要逆序解两次。也有业务层自己再压一层的情况------判断依据同样是"解完还是不可读"。
5)截断数据 。响应不完整时解压会报 EOFError 或 CRC 校验失败。先核对 Content-Length 与实际收到的字节数是否一致,再判断是数据问题还是传输问题。
五、通用排查流程与自动化
五步流程
第 2 步有个快速判断技巧:统计字符分布 。Base64 会用到大小写字母 + 数字(约 62 种以上字符),Hex 只用 16 种字符。如果串里出现了 g-z 或 G-Z,就不可能是纯 Hex。
自动化识别脚本
工具清单
|---------|-----------------------------------------------------|-------------------------------|
| 场景 | 工具 | 特点 |
| 命令行快速验证 | base64、xxd、gzip -d、python3 -c | 无需安装,适合单次验证 |
| 交互式逐层试探 | CyberChef | "Magic" 自动识别,可视化剥洋葱,调试多层嵌套最高效 |
| 抓包集成 | Burp Suite Decoder / mitmproxy | 直接在代理流程里转换,不用复制粘贴 |
| 二进制分析 | 010 Editor、ImHex | 模板化解析,看魔数与结构最直观 |
| 代码库 | base64 / zlib / gzip / brotli / zstandard | 集成进自动化流程 |
六、结语
这三类处理手法要解决的核心问题只有一个:如何让二进制数据穿过只处理文本的通道 。Base64 和 Hex 解决"字符集不兼容",压缩解决"体积太大"。它们都不是安全措施------编码不等于加密,这一点在评估接口安全性时要分清楚:看到 Base64 就以为是加密、看到长串无意义字符就以为不可读,是两类典型的误判。
实际排查时的顺序比技巧更重要:先看响应头确定处理层次,再用魔数兜底,最后小样本试探、逐层剥离。养成这个习惯,遇到任何编码过的响应体都能在几分钟内定位。
最后提醒一句工程上的事:处理来自外部的不确定数据时,解压前限制输出大小、解码前严格校验字符集。这两个动作能挡掉解压炸弹和脏数据引发的内存问题,成本不到十行代码。
作者备注:本文所述方法适用于接口调试、抓包排查、自有系统的数据处理与协议分析等对自有或已授权流量的分析场景。编码与压缩是中性技术,文中所有示例均以标准格式与公开规范为依据。