搜索数据定时批量采集的工程化:从脚本到能跑一个月的任务

后台接第三方搜索数据接口这件事,写通一条请求很容易,难的是让它稳定地、按计划地、批量地跑起来,还不烧冤枉钱。这篇记录我把一个「每天批量查几百个关键词」的采集任务做成工程化的过程:调度、批量、去重、失败恢复、成本控制,几个要点一次讲清。

先定位:这篇解决什么问题

  • 输入:一个关键词清单(几百到几千个)
  • 任务:每天按计划批量查这些词的谷歌搜索结果
  • 约束:成本可控、断点能续、不重复扣费、结果能审计

它和「写个 SDK 封装」是两个层面的问题:封装解决「怎么发请求」,这篇解决「怎么把请求拼成一个可靠的生产任务」。

一、批量:并发与控制节奏

几百个关键词串行跑太慢,全并发又容易撞 QPS 限制。我用的服务是 SerpBase(官网 serpbase.dev,/google/search 端点,POST JSON),它的 QPS 有限流设计,所以批量要「有节制的并发」。

用 Python 的 ThreadPoolExecutor 控制并发数:

import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
import time

API_KEY = "你的Key"
URL = "https://api.serpbase.dev/google/search"
HEADERS = {"Content-Type": "application/json", "X-API-Key": API_KEY}

KEYWORDS = ["serp api", "google search api", "cheap serp api", ...]  # 几百个

def fetch(kw):
    resp = requests.post(
        URL, headers=HEADERS,
        json={"q": kw, "hl": "en", "gl": "us"},
        timeout=30,
    )
    data = resp.json()
    return {
        "keyword": kw,
        "status": data.get("status"),
        "elapsed_ms": data.get("elapsed_ms"),
        "credits_charged": data.get("credits_charged"),
        "organic": data.get("organic", []),
    }

results = []
with ThreadPoolExecutor(max_workers=5) as ex:   # 控制并发,别打满 QPS
    futures = {ex.submit(fetch, kw): kw for kw in KEYWORDS}
    for fut in as_completed(futures):
        kw = futures[fut]
        try:
            results.append(fut.result())
        except Exception as e:
            results.append({"keyword": kw, "status": "error", "error": str(e)})

print(f"完成 {len(results)} 条,成功 {sum(1 for r in results if r.get('status') == 0)} 条")

max_workers=5 是起步值,实际根据你的 QPS 额度调整——这个值决定了「快」和「稳」的平衡点。

二、去重与幂等:跑挂了不重复

批量任务最怕「跑一半断了,重跑一遍,已成功的又扣一次费」。解决办法是把「任务状态」和「结果」分开存,用关键词做主键做幂等:

import sqlite3

con = sqlite3.connect("crawl.db")
con.execute("""CREATE TABLE IF NOT EXISTS tasks (
    keyword TEXT PRIMARY KEY,
    status TEXT NOT NULL DEFAULT 'pending',   -- pending/running/done/failed
    result TEXT,
    updated_at TEXT
)""")
con.execute("""CREATE TABLE IF NOT EXISTS billing (
    keyword TEXT, request_id TEXT, credits REAL, elapsed_ms INT, ts TEXT
)""")

def mark_done(con, kw, result):
    con.execute("UPDATE tasks SET status='done', result=? WHERE keyword=?",
                (result, kw))
    con.commit()

关键点:

  • 任务表用 keyword 做主键,天然去重
  • 每条响应里的 request_idcredits_charged 写进 billing 表——这就是你的审计账本
  • 重跑时跳过 status='done' 的词,只补跑 failed/pending

这样断点续跑,已成功的不会再发请求,也就不会重复扣费。

三、失败处理:重试与续跑

SerpBase 请求失败和上游超时会自动退 credits,所以「重试」本身不会额外烧钱——这是能放心做重试的前提。失败处理我分两层:

单条重试(API 层,最多 3 次指数退避):

def fetch_with_retry(kw, retries=3):
    for attempt in range(retries):
        try:
            return fetch(kw)
        except Exception:
            if attempt < retries - 1:
                time.sleep(0.5 * (2 ** attempt))   # 0.5s, 1s, 2s
    raise RuntimeError(f"{kw} failed")

任务级续跑(调度层):把失败的词留在 failed 状态,下次定时任务只补跑这批。

两层配合,单条失败不会拖垮整批,整批失败能安全续跑。

四、调度:谁在什么时间跑

任务无状态(读写库 + 调接口),用系统调度最省心,不用额外引入框架。

  • Linux:cron
  • Windows:任务计划程序
# 每天早上 8 点跑一次全量
0 8 * * *  cd /opt/crawl && python3 run_full.py >> /var/log/crawl.log 2>&1

# 每小时补跑失败项
15 * * * *  cd /opt/crawl && python3 rerun_failed.py >> /var/log/crawl.log 2>&1

两个任务:一个做全量,一个做失败补跑。日志统一落文件,配合 request_id 排查。

五、成本:批量任务最容易被忽略的两点

  1. 端点费率不同。SerpBase 的 /google/search/google/news/google/videos 是 1 credits/次,/google/images 和两个 Maps 端点是 2 credits/次。批量前确认你调的是哪个端点,别按 1 credits 估预算。
  2. 记账靠响应字段。每条响应都带 credits_charged,写进 billing 表,月底对账直接查表,不靠猜。

一个量级参考:300 个关键词每天一次,一个月 9000 次,按 $0.50/千次($10 标准包,2 万次、永不过期)算,月成本约 $4.5——这个量级下成本几乎可以忽略,重点是别浪费在重复请求上。

六、这套方案的沉淀

跑了一个月,最值得分享的经验就三条:

  1. 并发别贪多。5 路并发的稳定性和成本都比 50 路好,QPS 限制是有道理的。
  2. 幂等是省钱核心。没有去重表,一次断线重跑就是双倍扣费。
  3. 把计费当数据对待request_id + credits_charged 进表,排查和审计都有据可查。

如果你也想把「批量查谷歌数据」做成能长期跑的任务,接口细节在 SerpBase 官方文档。先把批量、去重、失败续跑三块搭起来,这个任务就从「脚本」升级成「能跑一个月的工程」了。

有在批量采集上踩过坑的同学,欢迎交流你的并发和重试策略。

posted @ 2026-08-21 14:45  蜘蛛人  阅读(4)  评论(0)    收藏  举报