从「能跑就行」到「敢改就敢改」:采集脚本重构实录

接手过一个采集脚本:单文件 700 多行,一个函数从头写到尾,请求逻辑复制了四遍(对应四个端点),Key 硬编码在第 12 行,改一个字段要通读全文。每次需求变更我都手心冒汗。后来用一个周末重构,现在改起来轻松多了。这篇记录重构的顺序和手法。

先认「坏味道」

对照自查,中了三条以上就该排重构了:

  1. 一个函数干完所有事:请求 + 清洗 + 入库 + 报表
  2. 硬编码:Key、关键词列表、市场参数、文件路径
  3. 复制粘贴:几个端点的请求逻辑几乎一样,改一处漏一处
  4. 错误吞掉(except: pass)或一处报错全批中断
  5. 没有测试,改完靠「跑一遍看看」

重构的第一原则

先保行为不变,再改善结构。 重构和加功能分开做——混在一起,出 bug 就分不清是谁引入的。顺序固定:先重构(行为不变)→ 验证一致 → 再改功能。

第一步:写「保命测试」

重构最大的风险是改坏了不知道。先给现有代码写特征测试(characterization test):拿真实响应当 fixture,把当前输出固化为期望值:

def test_normalize_matches_fixture():
    raw = json.load(open("fixtures/search_python.json"))
    expected = json.load(open("fixtures/normalized_python.json"))
    assert normalize(raw) == expected

跑通「现状」,就有了安全网:重构后测试还绿,说明行为没变。

第二步:参数外提

# before
# api_key = "sk_xxx"        # 写死
# keywords = ["a", "b"]     # 写死

# after
API_KEY = os.environ["SERPBASE_API_KEY"]
cfg = yaml.safe_load(open("config.yaml"))

Key 外提到环境变量不只是整洁,还是必须的安全修复——硬编码的 Key 随时可能跟着 git 提交泄露。

第三步:消灭复制粘贴(表驱动)

四个端点的请求逻辑几乎一样,抽成一张表:

ENDPOINTS = {
    "search": "https://api.serpbase.dev/google/search",
    "images": "https://api.serpbase.dev/google/images",
    "news":   "https://api.serpbase.dev/google/news",
    "videos": "https://api.serpbase.dev/google/videos",
}

def fetch(endpoint: str, params: dict) -> dict:
    resp = session.post(ENDPOINTS[endpoint], json=params, timeout=30)
    resp.raise_for_status()
    return resp.json()

端点增加一个,改一行;超时策略调整,改一处,所有端点受益。

第四步:按步骤拆函数

巨型函数按职责拆开:fetch → normalize → validate → store。每个函数只做一件事,能单独测试,也能单独替换。

第五步:错误处理分级

把「全批中断」改成「单条隔离」:

for kw in keywords:
    try:
        process(kw)
    except Exception as e:
        log.error("failed kw=%s: %s", kw, e)
        failed.append(kw)          # 记录失败项,继续跑

一条失败不再拖垮整批,失败的收尾统一处理。

第六步:新旧影子比对

重构完成后,用同一批输入跑新旧两版,逐条 diff:

assert old_normalize(raw) == new_normalize(raw)

不一致的地方逐个确认,是重构引入的差异就修,是原有 bug 就记下来单独处理。

踩坑记录

坑 1:重构和改功能混在一个提交。 上线后出问题,回溯分不清来源。分成两次提交、两个 PR。

坑 2:没写保命测试就动手。 重构完只有「跑一遍好像没问题」的自信,线上炸了才发现。先写特征测试,再动结构。

坑 3:过度抽象。 有人抽了个「配置 DSL」,结果没人看得懂,比原脚本还难维护。抽象到「消除重复」即可,别造框架。

坑 4:憋一次性大重构。 攒了两周的改动一起上线,风险和收益一起失控。小步重构,每一步都可运行、可回滚。

工程清单

  1. 先写特征测试(真实响应做 fixture)
  2. 重构与改功能分开提交
  3. 参数外提,Key 走环境变量
  4. 复制粘贴抽成表驱动
  5. 按职责拆函数,错误分级隔离
  6. 新旧影子比对,不一致逐个确认

重构的价值不是「代码好看」,是「下次改需求不心虚」。能跑就行的代码是欠债,重构是还债——早还早轻松。

接口的六个端点共用一套响应结构(统一信封),SerpBase 官方文档 里每个端点都列了完整字段,重构时把请求层抽成表驱动会特别顺。你们的采集脚本重构过吗?评论区聊聊最痛的坏味道。接手过一个采集脚本:单文件 700 多行,一个函数从头写到尾,请求逻辑复制了四遍(对应四个端点),Key 硬编码在第 12 行,改一个字段要通读全文。每次需求变更我都手心冒汗。后来用一个周末重构,现在改起来轻松多了。这篇记录重构的顺序和手法。

先认「坏味道」

对照自查,中了三条以上就该排重构了:

  1. 一个函数干完所有事:请求 + 清洗 + 入库 + 报表
  2. 硬编码:Key、关键词列表、市场参数、文件路径
  3. 复制粘贴:几个端点的请求逻辑几乎一样,改一处漏一处
  4. 错误吞掉(except: pass)或一处报错全批中断
  5. 没有测试,改完靠「跑一遍看看」

重构的第一原则

先保行为不变,再改善结构。 重构和加功能分开做——混在一起,出 bug 就分不清是谁引入的。顺序固定:先重构(行为不变)→ 验证一致 → 再改功能。

第一步:写「保命测试」

重构最大的风险是改坏了不知道。先给现有代码写特征测试(characterization test):拿真实响应当 fixture,把当前输出固化为期望值:

def test_normalize_matches_fixture():
    raw = json.load(open("fixtures/search_python.json"))
    expected = json.load(open("fixtures/normalized_python.json"))
    assert normalize(raw) == expected

跑通「现状」,就有了安全网:重构后测试还绿,说明行为没变。

第二步:参数外提

# before
# api_key = "sk_xxx"        # 写死
# keywords = ["a", "b"]     # 写死

# after
API_KEY = os.environ["SERPBASE_API_KEY"]
cfg = yaml.safe_load(open("config.yaml"))

Key 外提到环境变量不只是整洁,还是必须的安全修复——硬编码的 Key 随时可能跟着 git 提交泄露。

第三步:消灭复制粘贴(表驱动)

四个端点的请求逻辑几乎一样,抽成一张表:

ENDPOINTS = {
    "search": "https://api.serpbase.dev/google/search",
    "images": "https://api.serpbase.dev/google/images",
    "news":   "https://api.serpbase.dev/google/news",
    "videos": "https://api.serpbase.dev/google/videos",
}

def fetch(endpoint: str, params: dict) -> dict:
    resp = session.post(ENDPOINTS[endpoint], json=params, timeout=30)
    resp.raise_for_status()
    return resp.json()

端点增加一个,改一行;超时策略调整,改一处,所有端点受益。

第四步:按步骤拆函数

巨型函数按职责拆开:fetch → normalize → validate → store。每个函数只做一件事,能单独测试,也能单独替换。

第五步:错误处理分级

把「全批中断」改成「单条隔离」:

for kw in keywords:
    try:
        process(kw)
    except Exception as e:
        log.error("failed kw=%s: %s", kw, e)
        failed.append(kw)          # 记录失败项,继续跑

一条失败不再拖垮整批,失败的收尾统一处理。

第六步:新旧影子比对

重构完成后,用同一批输入跑新旧两版,逐条 diff:

assert old_normalize(raw) == new_normalize(raw)

不一致的地方逐个确认,是重构引入的差异就修,是原有 bug 就记下来单独处理。

踩坑记录

坑 1:重构和改功能混在一个提交。 上线后出问题,回溯分不清来源。分成两次提交、两个 PR。

坑 2:没写保命测试就动手。 重构完只有「跑一遍好像没问题」的自信,线上炸了才发现。先写特征测试,再动结构。

坑 3:过度抽象。 有人抽了个「配置 DSL」,结果没人看得懂,比原脚本还难维护。抽象到「消除重复」即可,别造框架。

坑 4:憋一次性大重构。 攒了两周的改动一起上线,风险和收益一起失控。小步重构,每一步都可运行、可回滚。

工程清单

  1. 先写特征测试(真实响应做 fixture)
  2. 重构与改功能分开提交
  3. 参数外提,Key 走环境变量
  4. 复制粘贴抽成表驱动
  5. 按职责拆函数,错误分级隔离
  6. 新旧影子比对,不一致逐个确认

重构的价值不是「代码好看」,是「下次改需求不心虚」。能跑就行的代码是欠债,重构是还债——早还早轻松。

接口的六个端点共用一套响应结构(统一信封),SerpBase 官方文档 里每个端点都列了完整字段,重构时把请求层抽成表驱动会特别顺。你们的采集脚本重构过吗?评论区聊聊最痛的坏味道。

posted @ 2026-09-24 08:59  蜘蛛人  阅读(10)  评论(0)    收藏  举报