给 SerpBase 写一个 Python 封装:接入实录与踩坑笔记
后端接第三方 API 这件事,难的不是请求本身,而是把「请求、重试、计费日志、异常处理」整理成一块能直接用的东西。这篇记录我把 SerpBase 接进项目的过程:怎么封、踩了哪些坑、最后长什么样。
先说接入的基本事实
SerpBase 的接口是全 POST JSON,Base URL 是 https://api.serpbase.dev,认证就一个请求头 X-API-Key。6 个端点:
| 端点 | 用途 | 每次扣 credits |
|---|---|---|
/google/search |
网页搜索 organic 结果 | 1 |
/google/images |
图片结果 | 2 |
/google/news |
新闻结果 | 1 |
/google/videos |
视频结果 | 1 |
/google/maps/search |
地图地点搜索 | 2 |
/google/maps/detail |
地点详情 | 2 |
关键点:失败请求和上游超时会自动退款,credits_charged 字段会出现在每个响应里。这就意味着封装层可以放心做重试——重试的成本兜底已经由服务端处理了,我这边只需要记录真实扣费。
封装的第一版(30 行)
import requests
import time
import logging
logger = logging.getLogger("serpbase")
class SerpBase:
BASE = "https://api.serpbase.dev"
ENDPOINTS = {
"search": "/google/search",
"images": "/google/images",
"news": "/google/news",
"videos": "/google/videos",
"maps_search": "/google/maps/search",
"maps_detail": "/google/maps/detail",
}
def __init__(self, api_key, max_retries=2):
self.session = requests.Session()
self.session.headers.update(
{"Content-Type": "application/json", "X-API-Key": api_key}
)
self.max_retries = max_retries
def call(self, endpoint, **params):
for attempt in range(self.max_retries + 1):
resp = self.session.post(f"{self.BASE}{self.ENDPOINTS[endpoint]}",
json=params, timeout=30)
data = resp.json()
if data.get("status") == 0:
logger.info(
"ok type=%s credits=%s elapsed_ms=%s request_id=%s",
data.get("search_type"),
data.get("credits_charged"),
data.get("elapsed_ms"),
data.get("request_id"),
)
return data
if attempt < self.max_retries:
time.sleep(0.5 * (attempt + 1))
logger.warning("failed type=%s status=%s", endpoint, data.get("status"))
raise RuntimeError(f"{endpoint} failed: {data}")
用法:
api = SerpBase("your_key")
result = api.call("search", q="python asyncio", hl="en", gl="us", page=1)
for item in result["organic"]:
print(item["rank"], item["title"])
踩坑记录(每一条都是真的)
坑 1:把失败当 HTTP 错误处理。 它家成功失败都是 HTTP 200 + JSON body,靠 status 字段区分。我第一版用 raise_for_status(),死活拿不到错误详情。改成检查 status == 0 后,错误信息(含 request_id)都能直接打进日志,定位问题快多了。
坑 2:Maps 端点的坐标参数必须成对。 lat 和 lng 必须一起传,zoom 只有传了坐标才生效(默认 14,范围 1–21)。我一开始只传了 lat,返回的 places 是空的,看了文档才发现是参数组合问题,不是接口问题。
坑 3:图片和视频结果的字段是可选的。 文档明确说 position、display_url、thumbnail 这类归一化别名「optional」。封装层里我一律用 .get(),不做硬索引,否则线上偶发 KeyError。
坑 4:credits_charged 在退款后才准确。 每个响应里的这个数字是「退款逻辑应用之后」的真实扣费。我把每条请求的扣费写进了结构化日志,月底对账直接查日志,不用猜。
重试策略的取舍
我的做法是:默认 2 次重试,指数退避 0.5s 起步。因为失败自动退款,重试不会白白烧 credits;同时 request_id 贯穿响应,工单追踪有据可查。跑了一个月,成功率和成本都能从日志里直接看。
最后
封装这种东西没有银弹,把「认证、端点映射、重试、日志」四个点做扎实,接哪家都顺手。SerpBase 完整端点参数在官方文档,它还有官方 MCP server 和 agent skill,想直接给 Claude / Codex / Cursor 这类 agent 用的话,连封装都可以省了。
有别的接入细节想讨论的,评论区聊。

浙公网安备 33010602011771号