一次 KeyError 崩掉整个采集任务:SerpBase 响应字段的事故复盘

上周二早上 7:10,排名日报的告警把我吵醒。500 个关键词的采集任务,跑了 3 分钟,整体失败退出,只留下一行 traceback。这篇文章是事故复盘:现象、排查、根因、修复、以及我加进代码里的四条规矩。

现象

任务是每天早上的排名巡检,跑了大半年一直稳。那天日志里出现:

Traceback (most recent call last):
  ...
KeyError: 'snippet'

然后是「任务退出,退出码 1」。失败清单也是空的——异常抛在解析阶段,我原来的写法是一条失败整个循环挂掉,所以「哪些词失败了」根本没记。

排查过程

第一步,手动复现。 拿日志里第一个失败的词,用脚本单独发了一次请求——正常返回,status 是 0。先排除了「接口挂了」。

第二步,找规律。 把失败的那批词单独列出来,和成功的对比:清一色是长尾词,而成功的多是核心词。这个分野有点像「有没有摘要」,但不能靠猜。

第三步,看响应原文。 打印一个失败词的完整响应,一眼就看到了:organic 里那条结果没有 snippet 字段。而我解析代码里写的是:

# 事故版本:假设 snippet 一定存在
for item in data["organic"]:
    rows.append({
        "keyword": keyword,
        "rank": item["rank"],
        "snippet": item["snippet"],      # ← 这里炸的
    })

谷歌没给那些词渲染摘要,字段直接不存在。而 SerpBase 的文档里,snippet 明明是标着 optional 的——「When a snippet is parsed」,这句我写过,但写代码的时候没往心里去。

根因

两条线叠在一起:

  1. 把可选字段当必选字段——接口文档写得明明白白,是我读漏了;
  2. 错误粒度过粗——一条记录解析失败,应该跳过并记录,而不是让整个任务崩掉。

修复

解析层,全部改用 .get():

for item in data.get("organic", []):
    rows.append({
        "keyword": keyword,
        "rank": item.get("rank"),
        "snippet": item.get("snippet", ""),   # 没有摘要就是空串
    })

任务层,信封优先 + 单条隔离:

def collect(keyword: str):
    resp = requests.post(
        "https://api.serpbase.dev/google/search",
        headers={"Content-Type": "application/json",
                 "X-API-Key": os.environ["SERPBASE_API_KEY"]},
        json={"q": keyword, "gl": "cn", "hl": "zh-cn"},
        timeout=(5, 30),
    ).json()

    # 1. 先判信封,再读数据
    if resp.get("status") != 0:
        raise ApiError(f"status={resp.get('status')} req={resp.get('request_id')}")

    return resp.get("organic", [])

# 2. 单条失败不拖垮整批
failed = []
for kw in keywords:
    try:
        rows.extend(collect(kw))
    except Exception as e:
        log.error("collect failed kw=%s err=%s", kw, e)
        failed.append(kw)          # 失败清单,下轮补跑

两处改完,同类故障从「整体崩」变成「跳过 + 记录」,最坏情况也只是日报里少几个长尾词。

之后加进代码的四条规矩

  1. 信封优先:拿到响应先看 status,非 0 就抛异常,别急着读业务字段;
  2. 可选字段一律 .get():文档标 optional 的字段,不存在就当空值处理;
  3. 单条隔离:批量任务里任何一条失败,只跳过不蔓延;
  4. 失败必须留痕:失败的词、request_id、错误信息都落库,补跑和排障都有依据。

另外补了一组 fixture:存了一份「摘要缺失」的真实响应进测试样本,解析测试跑过它才算完。这个教训的成本是「一次失败的日报」,换回来的是以后每次接口抖动都不会再全线崩。

复盘

这次事故最冤的地方在于:文档早就把答案写在那儿了。 optional 字段的语义、信封的用法,SerpBase 的文档里逐字段标着。我栽的不是技术问题,是「我以为这个字段肯定有」。

现在我的代码里,所有对第三方接口的字段访问都走 .get()——宁可多写一个默认值,不赌一次运气。

接口的 optional 标注和信封字段说明在 官方文档 里写得很细,值得对着写一遍解析层。你因为「以为某个字段必有」栽过吗?评论区聊聊。

posted @ 2026-10-15 09:51  蜘蛛人  阅读(4)  评论(0)    收藏  举报