SERP API 返回 JSON 会演进,新字段加、旧字段改,下游解析会崩。做好版本管理,升级不炸。
1. 为什么需要 Schema 管理
SERP API 返回 JSON 的结构,不同版本有差异:
- v1:
organic[].title/snippet - v2: 加
people_also_ask[](延伸问题) - v3: 加
ai_overview(AI 摘要) - v4:
organic[].snippet改名为organic[].description(破坏性)
下游解析器跟着版本走,不管理就会崩。
2. JSON Schema 定义
先定义 API 返回的 schema:
json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://serpbase.dev/schema/v1/search.json",
"type": "object",
"properties": {
"status": { "type": "integer" },
"request_id": { "type": "string" },
"organic": {
"type": "array",
"items": {
"type": "object",
"required": ["title", "link"],
"properties": {
"title": { "type": "string" },
"link": { "type": "string" },
"snippet": { "type": "string" },
"position": { "type": "integer" }
}
}
},
"people_also_ask": {
"type": "array",
"items": {
"type": "object",
"properties": {
"question": { "type": "string" },
"answer": { "type": "string" }
}
}
}
},
"required": ["status", "organic"]
}
3. Schema 版本管理
git 管理 + semver 版本号:
bash
schemas/
├── v1.0.0-search.json
├── v1.1.0-search.json # 加 people_also_ask
├── v2.0.0-search.json # 破坏性:snippet → description
└── v3.0.0-search.json # 加 ai_overview
- minor(1.1.0):加字段,不破坏
- major(2.0.0):改字段名,破坏
4. 校验库
Python 用 jsonschema:
python
import jsonschema
from jsonschema import validate, Draft202012Validator
def validate_serp(data, schema_path='schemas/v1.1.0-search.json'):
"""校验 SERP 数据是否符合 schema"""
schema = json.load(open(schema_path))
try:
validate(instance=data, schema=schema)
return True, None
except jsonschema.ValidationError as e:
return False, str(e)
# 测试
data, error = validate_serp(serp_response)
if not data:
print(f"Schema violation: {error}")
5. 自动适配不同版本
下游解析器适配多个版本:
python
def parse_serp(data):
"""自适应解析 SERP 数据,兼容多版本"""
# 检测版本
version = detect_version(data)
if version >= '2.0.0':
# 新版:description 替代 snippet
snippet_field = 'description'
else:
# 旧版:snippet
snippet_field = 'snippet'
results = []
for item in data.get('organic', []):
results.append({
'title': item['title'],
'snippet': item.get(snippet_field, ''), # 兼容
'link': item['link'],
'position': item.get('position', 0)
})
# ai_overview(v3+)
if 'ai_overview' in data:
results.append({
'type': 'ai_overview',
'text': data['ai_overview'].get('text', '')
})
return results
def detect_version(data):
"""检测数据版本"""
if 'ai_overview' in data:
return '3.0.0'
if 'people_also_ask' in data:
return '1.1.0'
return '1.0.0'
6. 兼容策略
3 种兼容策略:
python
class SchemaCompat:
"""Schema 兼容处理"""
def __init__(self):
self.versions = {
'1.0.0': self.parse_v1,
'1.1.0': self.parse_v1_1,
'2.0.0': self.parse_v2,
'3.0.0': self.parse_v3
}
def parse(self, data):
version = detect_version(data)
return self.versions[version](data)
def parse_v1(self, data):
return [{'title': i['title'], 'snippet': i['snippet']} for i in data['organic']]
def parse_v1_1(self, data):
items = self.parse_v1(data)
# 加 PAA
items.extend([
{'type': 'paa', 'q': p['question'], 'a': p['answer']}
for p in data.get('people_also_ask', [])
])
return items
def parse_v2(self, data):
items = []
for i in data['organic']:
items.append({
'title': i['title'],
'description': i['description'], # 新版字段
'link': i['link']
})
return items
def parse_v3(self, data):
items = self.parse_v2(data)
if 'ai_overview' in data:
items.append({'type': 'ai_overview', 'text': data['ai_overview']['text']})
return items
7. 下游影响分析
每次 Schema 升级,评估下游影响:
python
def analyze_schema_change(old_schema, new_schema):
"""分析 schema 变化的破坏性"""
breaking = []
additive = []
# 递归对比
def walk(old, new, path=''):
if isinstance(old, dict) and isinstance(new, dict):
# 删除字段 = 破坏
for k in old:
if k not in new:
breaking.append(f'{path}.{k} removed')
# 新增字段 = additive
for k in new:
if k not in old:
additive.append(f'{path}.{k} added')
# 递归
for k in old:
if k in new:
walk(old[k], new[k], f'{path}.{k}')
elif isinstance(old, dict) and isinstance(new, dict):
# 类型变化 = 破坏
if old.get('type') != new.get('type'):
breaking.append(f'{path} type changed')
walk(old_schema, new_schema)
return breaking, additive
8. 版本兼容测试
自动化测试保证兼容:
python
import pytest
# 所有历史版本的测试数据
HISTORICAL = {
'1.0.0': {...}, # 只有 organic
'1.1.0': {...}, # + paa
'2.0.0': {...}, # snippet → description
'3.0.0': {...} # + ai_overview
}
@pytest.mark.parametrize('version,data', HISTORICAL.items())
def test_parse_compat(version, data):
"""所有历史版本都能解析"""
parser = SchemaCompat()
result = parser.parse(data)
assert result is not None
assert isinstance(result, list)
9. 实战:30 天数据
跑 30 天 Schema 管理:
| 指标 | 数值 |
|---|---|
| Schema 版本 | 3 次(1.1 → 2.0 → 3.0) |
| 破坏性变更 | 1 次(snippet → description) |
| 下游解析失败 | 0 |
| 回滚次数 | 0 |
| 平均迁移时间 | 30 分钟 |
10. 最佳实践
几点经验:
- API 侧加版本参数 :
?v=3.0.0 - 保持 snippet 兼容:加 description 的同时保留 snippet(过渡期)
- schema 文档化:每个字段标注引入版本
- 提前通知:破坏性变更至少提前 30 天
- 灰度:先灰度 1% 流量验证
11. API 版本参数
serpbase 支持版本参数:
python
r = requests.post(
'https://api.serpbase.dev/google/search?v=3.0.0',
headers={'X-API-Key': key},
json={'q': query}
)
12. 总结
JSON Schema 版本管理 5 件事:
- 定义 schema(v1.0.0)
- git + semver 管理
- 校验(jsonschema)
- 兼容解析(多版本适配)
- 破坏性评估 + 灰度
代码 GitHub 公开,clone 跑起来。
参考文档
本文 API 示例参考 serpbase 文档,接口路径、参数和返回字段以官方文档为准。