抓下来的原始 JSON 不能直接进库,也不能直接做对比:标题里混着 HTML 实体、日期是各种各样的文本、链接带着锚点和尾斜杠、同一条结果在不同查询里反复出现。清洗层要做的就四件事:文本标准化、日期归一、URL 规范化、稳定去重键。这篇把四个函数和一个映射模板给全。
先盘点字段
搜索端点的 organic 条目里,rank(必填,返回列表内 1 起算的位次)、title、link 是必填;snippet、date、published_at、url、display_url、sitelinks 是可选字段,以实际返回为准。清洗前先对一批真实响应盘一遍:哪些字段有值、值长什么样,再决定清洗规则------比照着想象中的格式写解析器可靠。
四个清洗函数
python
import hashlib, html, re
from datetime import datetime
from urllib.parse import urlsplit
TAG_RE = re.compile(r"<[^>]+>")
SPACE_RE = re.compile(r"\s+")
def clean_text(value: str) -> str:
"""去标签、反转义、压缩空白。"""
if not value:
return ""
text = TAG_RE.sub("", value)
return SPACE_RE.sub(" ", html.unescape(text)).strip()
def parse_date(raw: str):
"""把日期文本尽量解析成 ISO 日期;解析不出来返回 None,原文另存。"""
if not raw:
return None
text = clean_text(raw)
for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%b %d, %Y", "%d %b %Y", "%Y年%m月%d日"):
try:
return datetime.strptime(text, fmt).date().isoformat()
except ValueError:
continue
return None
def normalize_url(url: str) -> str:
"""去掉锚点、统一尾斜杠,保留查询串。"""
base = url.split("#", 1)[0]
return base[:-1] if base.endswith("/") else base
def stable_id(link: str) -> str:
"""稳定去重键:注意不要用内置 hash()。"""
return hashlib.md5(normalize_url(link).encode("utf-8")).hexdigest()
这里有个容易踩的坑:Python 内置的 hash() 每个进程都会加随机盐,同一个链接两次运行会得到不同的值,拿它做去重键,今天去过的第二天还会当新数据。用 hashlib 里的摘要函数才能跨进程稳定。
映射模板
python
def normalize_item(item: dict) -> dict:
link = item.get("link", "")
return {
"id": stable_id(link),
"rank": item.get("rank"),
"title": clean_text(item.get("title", "")),
"link": normalize_url(link),
"domain": urlsplit(link).netloc,
"snippet": clean_text(item.get("snippet", "")),
"published_on": parse_date(item.get("date", "")),
"date_raw": item.get("date", ""), # 解析失败时留证据
"sitelinks": item.get("sitelinks", []), # 列表,入库时序列化为 JSON 字符串
}
清洗前后大致是这样(结构示例,不是实测值):
| 字段 | 原始值 | 清洗后 |
|---|---|---|
| title | Python & asyncio 入门 <b>教程</b> |
Python & asyncio 入门 教程 |
| date | 3 days ago |
None(date_raw 留原文) |
| link | https://example.com/post/#top |
https://example.com/post |
字段的准确定义以 SerpBase 文档里的 search 端点字段表 为准------比如 date 的说明是从 snippet 里抽出的日期文本、published_at 是解析后的别名,所以格式不固定,解析器必须容错。
三个实践建议
- 边采边清,但原始 JSON 也留一份。 清洗规则会变;把原始响应存到单独的列或文件,重新清洗随时可重放。
- 解析失败不要静默成空串。 返回
None并把原文放进date_raw,否则你分不清"没有日期"和"没解析出来"。 - 去重键要包含参数上下文。 同一个链接在不同
hl/gl下可能是两回事,如果一份表里混着多地区数据,stable_id里应拼上地区和语言。
成本
清洗本身不花钱;数据来自 search 端点,每次成功请求 1 credit,失败自动退款。清洗逻辑写好之后,采集多少、清洗多少,成本只跟请求数有关。
FAQ
要用第三方清洗库吗? 上面四个函数用标准库就够了;真需要更复杂的 HTML 处理再引入解析库。
sitelinks 这种嵌套结构怎么入库? 单独一列存 JSON 字符串,别硬拆成多行------它是附属信息,拆表会让主表膨胀。
日期要不要统一到 UTC? date 是文本日期,通常没有时区信息;存 ISO 日期即可,不要凭空补时间。
把 normalize_item 挂到你现有的采集循环里,入库质量会立刻上一个台阶。