花拾录
← 返回知识库

Python 标准库抓网页拿回一堆“乱码”,其实是 gzip 没解压

编程语言导入2026/09/220 阅读0 评论

用 Python 标准库去抓一个网站,read() 回来一看:满屏 \x1f\x8b\x08... 之类的字节,复制到编辑器里也是一堆乱码。第一反应是"对方是不是返回了异常内容",甚至怀疑自己请求错了地址。其实对方返回的完全正常,只是你少解了一道压。

现象

典型代码长这样:

import urllib.request

req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
resp = urllib.request.urlopen(req)
raw = resp.read()
print(raw[:64])

输出类似:

b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\xed]\xdb\x8e\xdc\xb8\x11...'

开头那几个字节 \x1f\x8b 非常关键——这不是什么随机乱码,而是一个明确的信号。

根因

\x1f\x8b 是 gzip 格式的固定魔数(magic number)。也就是说,服务器返回的响应体是一段 gzip 压缩数据,而不是纯文本。

为什么会这样?因为现代网站为了省带宽,普遍会在响应头里带上 Content-Encoding: gzip,把正文压缩后再发。这是 HTTP 协议的正常行为,客户端本该自动解压。

而问题的根源在于:你用的标准库 HTTP 客户端不会自动解压。requests 这类高层封装库会替你处理 gzip / deflate / br,所以平时用 requests 抓网页从没遇到过这个问题;一旦换回 urllib 这种更底层的方式,自动解压就没了,拿到的自然是压缩后的原始字节。于是"没解压"就被误读成了"内容是乱的"。

顺带一提:很多网站只在请求头里带了 Accept-Encoding: gzip 时才压缩。requests 默认会带上这个头,而手写的 urllib 请求往往没带——但有些服务器无论如何都压缩,所以无论怎样都可能撞上。

要区分"真乱码"和"压缩数据"其实不难:真乱码是字符编码对不上,字节流里通常还能看出一些接近可读的片段;而压缩数据的特征非常明显——开头是固定的 \x1f\x8b,后面跟着一串高熵的随机字节,几乎看不到任何像文字的连续片段。下次再遇到"一坨看不懂的字节",先瞄一眼头两个字节是不是 \x1f\x8b,是的话问题就已经解了九成。

解决

读取响应后,先看响应头,判断压缩类型,再调用对应的解压函数:

import gzip
import zlib
import urllib.request

req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0",
                                           "Accept-Encoding": "gzip, deflate"})
resp = urllib.request.urlopen(req)
raw = resp.read()

encoding = resp.headers.get("Content-Encoding", "").lower()
if encoding == "gzip":
    raw = gzip.decompress(raw)
elif encoding == "deflate":
    try:
        raw = zlib.decompress(raw)
    except zlib.error:
        raw = zlib.decompress(raw, -zlib.MAX_WBITS)   # 有些服务器发的是裸 deflate

text = raw.decode("utf-8", errors="replace")
print(text[:200])

几处细节值得说明:

  • 按响应头选解压方式,不要只认 gzip——deflate、br(Brotli)也常见,br 需要额外装 brotli 库才能解。
  • deflate 有两种封装:带 zlib 头的和不带的,zlib.decompress 对后者要传负数窗口位,所以上面加了兜底。
  • 解码时给 errors="replace",避免个别非法字节直接把整个脚本打崩。

由于多处脚本都踩过这一点,更省事的做法是把它封装成一个统一的"取文本"函数,全项目复用:

def fetch_text(url, headers=None):
    req = urllib.request.Request(url, headers=headers or {"User-Agent": "Mozilla/5.0"})
    with urllib.request.urlopen(req) as resp:
        raw = resp.read()
        enc = resp.headers.get("Content-Encoding", "").lower()
        if enc == "gzip":
            raw = gzip.decompress(raw)
        elif enc == "deflate":
            raw = zlib.decompress(raw)
        return raw.decode("utf-8", errors="replace")

延伸与预防

这条教训可以总结成一句话:先怀疑格式,再怀疑内容。 当抓回来的东西"看起来像乱码"时,先别急着找对方麻烦,按顺序排除三件事:

  1. 是不是压缩?看响应头的 Content-Encoding,看响应体开头有没有 \x1f\x8b 这类魔数。
  2. 是不是编码问题?确认 charset,中文站常见 GBK / GB18030 而不是 UTF-8。
  3. 是不是真错了?排除了前两条,再考虑对方返回了错误页、验证码页。

另外,能确定目标就是 HTTP 抓取时,直接用 requests 会省掉很多这类底层细节;如果出于依赖最小化的考虑必须用标准库,那就把"解压 + 解码"这一步固化进公共函数里,别在每个脚本里手写一遍——手写的地方越多,漏掉解压的概率就越大。

评论(0)

  • 还没有评论,来抢沙发~

相关文章