把搜索数据采集封装成内部服务:从散落脚本到统一入口
项目一开始,搜索数据是「各业务各自调 API」:排名功能发一次请求、报表功能发一次、AI 接地又发一次。每个地方都要管 API key、重试、失败处理,谁改了参数别人都不知道。这篇记录我怎么把这些散落调用收拢成一个内部数据服务——统一入口、统一鉴权、统一限流,业务方只调一个接口。
先看问题:散落调用的四个症状
- API key 到处放:每个服务一份,轮换 key 时要逐个改
- 重试逻辑各写各的:A 服务重试 2 次,B 服务不重试,行为不一致
- 扣费重复:同一关键词不同模块各查一遍,没人做缓存
- 换供应商是灾难:每个调用点都要改
这些都是「把采集逻辑内联在各业务里」的必然结果。解法是抽一个内部服务:所有对搜索数据 API 的调用都走它。
设计目标
┌────────────┐ ┌─────────────────────────────┐ ┌────────────┐
│ 业务A │──>│ │──>│ SerpBase │
│ 业务B │──>│ 搜索数据内部服务(网关) │──>│ (外部API) │
│ 业务C │──>│ 统一鉴权/限流/缓存/重试/审计 │──>│ │
└────────────┘ └─────────────────────────────┘ └────────────┘
关键决策:内部服务只暴露业务语义接口,不暴露供应商细节。业务方说「我要查这个关键词的排名」,不说「我要调 /google/search 带什么参数」。
第一步:统一入参接口
内部服务的接口设计成「业务意图优先」:
# 内部服务:POST /api/v1/search
# 入参:{ query, market="us", lang="en", device="default", type="web" }
# type 支持 web / news / images / videos / maps
def handle_search(params):
endpoint = {
"web": "/google/search",
"news": "/google/news",
"images": "/google/images",
"videos": "/google/videos",
"maps": "/google/maps/search",
}[params["type"]]
# 统一走:鉴权 -> 缓存 -> 限流 -> 调用 -> 审计
return serve(endpoint, {
"q": params["query"],
"gl": params.get("market", "us"),
"hl": params.get("lang", "en"),
"device": params.get("device", "default"),
})
业务方不用知道端点叫什么、参数怎么拼,只表达「要什么类型的数据」。
第二步:把公共能力收进服务
之前散在各处的逻辑,全部收进来:
def serve(endpoint, params):
# 1. 缓存优先(复用之前的缓存层)
key = cache_key(endpoint, params)
hit = cache_get(key)
if hit:
return hit, "cache"
# 2. 限流:按内部账号限流,防止一个业务打爆额度
if not rate_allow(params.get("account", "default")):
raise RateLimited("账号额度超限")
# 3. 调用外部 API(失败自动退 credits,重试零成本)
data = call_serpbase(endpoint, params)
# 4. 缓存成功结果
if data.get("status") == 0:
cache_set(key, data, ttl_for(params["type"]))
return data, "remote"
- 缓存:复用之前做的缓存层,内部服务天然让多个业务共享缓存,重复扣费直接消失
- 限流:按内部账号维度限流,防止某个业务打爆整体 QPS
- 重试:统一重试策略,失败/超时自动退 credits 的设计让重试零成本
第三步:审计与计费归属
内部服务要解决「钱是谁花的」——每笔调用打上业务账号:
def audit(account, endpoint, data):
log(
account=account,
endpoint=endpoint,
request_id=data.get("request_id"),
credits=data.get("credits_charged"),
elapsed_ms=data.get("elapsed_ms"),
status=data.get("status"),
)
落一张审计表,月底按账号聚合,谁用了多少 credits 一目了然——这比「各业务自己记」靠谱得多。
第四步:客户端一个薄封装
业务方接入时,给一个极薄的客户端(读内部的,不是外部 API):
# 业务方用
import requests
def search_rank(account, query, market="us", lang="en"):
resp = requests.post(
"http://search-svc.internal/api/v1/search",
headers={"X-Account": account, "X-Key": INTERNAL_KEY},
json={"query": query, "market": market, "lang": lang, "type": "web"},
timeout=30,
)
return resp.json()
内部 key 和服务地址统一管理,业务方永远不用接触外部 API key。
换供应商?改一处
内部服务最大的红利:供应商隔离。如果哪天要换或增加备用源,只改 call_serpbase() 这一个函数,业务方无感知。这也为「多供应商容灾」铺好了路——切换是网关内部的事。
踩坑记录
坑 1:接口参数设计得太「供应商化」。 第一版内部接口直接透传 /google/search 的参数(q、hl、gl...),结果业务方还是得懂外部 API。改成「业务意图优先」后(query/market/lang/type),接入门槛才真正降下来。
坑 2:缓存命中率低。 各业务传的参数格式不统一(有的传 en,有的传 en-US),缓存键对不上。解决:在服务层做参数归一化,统一再算缓存键。
坑 3:没做内部限流。 一个报表任务把并发拉满,其他业务跟着变慢。加上按账号限流后,才互不干扰。
效果沉淀
收拢成内部服务后:
- API key 只存在于一处,轮换零成本
- 缓存让重复扣费消失(多个业务共享)
- 审计表让 credits 归属清清楚楚
- 换供应商改一个函数
服务化不是银弹,但对「多个业务都要搜索数据」的场景,它是性价比最高的收敛方式。外部 API 的端点、参数、计费规则细节在 SerpBase 官方文档,做服务封装时先把它读透。
有把外部数据源收拢成内部服务的同学,欢迎交流你的接口设计。

浙公网安备 33010602011771号