抓取正文时踩到的三个坑:字符编码、图片地址拼接和空文本

上周给 HaoNews 的抓取流程做了一次正文抽取,目标是把抓回来的原始 HTML 变成干净的纯文本,写入本地一份约 12 万条资讯记录的索引。源站是海外站,编码混杂,有 UTF-8 也有少量 GBK 残留,单页正文长度从几百字到两三万字都有。跑完整批之后,发现三个问题必须单独处理。

第一个是编码。只要源站的 HTML 里没有声明 charset,或者声明了但和实际字节不符,直接 decode 出来就是一堆问号。我的做法是先拿原始字节,按优先级探测:

import chardet

def decode_bytes(raw: bytes) -> str:
    for enc in ("utf-8", "gb18030"):
        try:
            return raw.decode(enc)
        except UnicodeDecodeError:
            continue
    guess = chardet.detect(raw)["encoding"] or "utf-8"
    return raw.decode(guess, errors="replace")

先试 UTF-8 和 GB18030 两个高概率项,失败再用 chardet 兜底。代价是 chardet 对大文件偏慢,所以我把探测结果按域名缓存,同一个站不重复探测。

第二个是相对地址。正文里的图片 src 大量是 ../images/xxx.jpg 这种,直接存进索引后链接是断的。我的处理是用 urllib.parse 按页面 URL 做绝对化:

from urllib.parse import urljoin

src_abs = urljoin(page_url, src)

这里踩过的坑是:如果页面里有 <base href>,得先读它,urljoin 要用 base 而不是 page_url,否则路径会错一层。

第三个是空文本。抽取器对某些正文包裹在多层 div 里的页面返回空字符串,但页面其实有内容。我的判断是:如果抽出来少于 50 个字符,就退回到整页可见文本,再用正则切掉导航和页脚。这个兜底有代价,会混入少量无关文本,但比整条记录丢失好。

边缘情况里最容易翻车的是 GBK 页面里夹着 UTF-8 的片段,逐字节解码会直接失败,errors=“replace” 会把那一段变成问号,事后很难补。另一个坑是相对地址以 // 开头(协议相对),urljoin 能处理,但如果你自己拼字符串就会漏掉协议。

现在还在纠结的是:正文少于 50 字符就退回整页,这个阈值对短资讯(比如一句话快讯)会误判,把导航也带进去。你们抓短资讯时是单独给一个阈值,还是按来源站配置?

0
赞同度 100% · 0 条评论 · 1 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。