内容抽取任务最容易出的事故不是报错而是静默少做。页面改版标签层级变了抽取脚本按旧路径找不到节点返回空值。任务照常跑完日志零错误入库表里那一批字段变成空。三天后有人发现某个参数在系统里查不到回查才发现从改版那天起就没抽到过。防范这类问题的思路是让抽取器在取到值的同时把从哪儿取到的也记下来。改版之后重跑比较记录与实际路径不匹配就是改版信号取不到值就是丢失信号。下面这套做法只用标准库html.parser适合在没有浏览器环境的抓取管道里运行。一、记录两个特征对每个被抽取的字段同时记录两样东西一个是结构位置。形如div.spec-table table tr[2] td[1]表示它出现在文档树的哪条链上。结构位置对布局改动敏感适合发现表格挪了位置。一个是锚文本特征。取该节点前面最近的一段有文字节点做一次归一化后计算短哈希。锚文本对措辞敏感、对布局不敏感适合发现这段说明被删了。两者组合的判据比单独任何一个稳。只信结构位置会把位置没变但内容整块换了漏掉只信锚文本改个标点就误报。import hashlib import re def normalize_text(s): s re.sub(r\s, , s or ) return s[:24] def anchor_hash(text): t normalize_text(text) if not t: return return hashlib.sha1(t.encode(utf-8)).hexdigest()[:8]二、一个可运行的遍历器继承HTMLParser在遍历过程中维护一条父节点栈遇到目标标签时输出结构路径与最近的前置文字。这里以最常用的两种字段容器为例定义列表dl/dt/dd与表格table/tr/td。from html.parser import HTMLParser class SpecParser(HTMLParser): def __init__(self, wanted): super().__init__() self.wanted set(wanted) # 要抓的标签名 self.stack [] self.last_text self.rows [] def handle_starttag(self, tag, attrs): self.stack.append(tag) def handle_endtag(self, tag): if tag in self.stack: self.stack.remove(tag) def handle_data(self, data): t data.strip() if t: self.last_text t上面是骨架入栈出栈维持路径。真正把字段落下来的是下面这段在取到值时把三项一起写出去。def capture(self, name, value): if name in self.wanted: self.rows.append({ field: name, value: value, path: .join(self.stack), anchor: anchor_hash(self.last_text), })三、比对与判定基线是改版前一次成功抽取的结果字段为(field, path, anchor)。重跑后逐字段比对四种情况对应四种处置。| 情况 | path | anchor | 判定 | 处置 ||---|---|---|---|---|| 一致 | 同 | 同 | 正常 | 无需动作 || 结构移动 | 不同 | 同 | 布局改版 | 更新映射抽取继续 || 文案改写 | 同 | 不同 | 措辞变化 | 人工看一眼多为正常 || 双双不匹配 | 不同 | 不同 | 内容整块换了 | 阻断入库报待确认 |def diff(baseline, current): cur {r[field]: r for r in current} alerts [] for b in baseline: c cur.get(b[field]) if not c or not c.get(value): alerts.append((b[field], missing)) continue path_ok c[path] b[path] anchor_ok c[anchor] b[anchor] if not path_ok and not anchor_ok: alerts.append((b[field], review)) elif not path_ok: alerts.append((b[field], moved)) elif not anchor_ok: alerts.append((b[field], reworded)) return alerts四、把判定接进任务管道三类判定建议走不同的严格度。missing直接阻断该页面的写入。字段取不到而不写比写一个空值安全得多——空值会覆盖掉上一版正确数据。moved只发通知。布局改版是常态阻断会让运维人员习惯性忽略所有告警。review建议积累到一定比例再触发人工。经验值是同一页面连续两次review或者全站单日review数超过页面总数的 5%说明发生了大范围改版此时应当暂停覆盖写入。还有一点容易漏基线本身要随确认过的改版更新否则每次改版之后所有页面都会长期报moved噪声会淹没真实故障。五、小结这套校验没有语义理解只做一件事把从哪儿抽到的变成一份可比对的记录。它的收益集中在改版密集的阶段——布局换了能发现内容整块没了能拦住。至于抽取本身准不准那仍然要靠字段级的规则与抽检两者不互相替代。本文由一支长期做抓取管道与数据质量的技术团队整理欢迎同行交流指正。