给 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 端点的坐标参数必须成对。 latlng 必须一起传,zoom 只有传了坐标才生效(默认 14,范围 1–21)。我一开始只传了 lat,返回的 places 是空的,看了文档才发现是参数组合问题,不是接口问题。

坑 3:图片和视频结果的字段是可选的。 文档明确说 positiondisplay_urlthumbnail 这类归一化别名「optional」。封装层里我一律用 .get(),不做硬索引,否则线上偶发 KeyError。

坑 4:credits_charged 在退款后才准确。 每个响应里的这个数字是「退款逻辑应用之后」的真实扣费。我把每条请求的扣费写进了结构化日志,月底对账直接查日志,不用猜。

重试策略的取舍

我的做法是:默认 2 次重试,指数退避 0.5s 起步。因为失败自动退款,重试不会白白烧 credits;同时 request_id 贯穿响应,工单追踪有据可查。跑了一个月,成功率和成本都能从日志里直接看。

最后

封装这种东西没有银弹,把「认证、端点映射、重试、日志」四个点做扎实,接哪家都顺手。SerpBase 完整端点参数在官方文档,它还有官方 MCP server 和 agent skill,想直接给 Claude / Codex / Cursor 这类 agent 用的话,连封装都可以省了。

有别的接入细节想讨论的,评论区聊。

posted @ 2026-08-18 16:50  蜘蛛人  阅读(4)  评论(0)    收藏  举报