SERP 数据清洗实战:字段标准化、日期解析与去重键

抓下来的原始 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 &amp; 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 是解析后的别名,所以格式不固定,解析器必须容错。

三个实践建议

  1. 边采边清,但原始 JSON 也留一份。 清洗规则会变;把原始响应存到单独的列或文件,重新清洗随时可重放。
  2. 解析失败不要静默成空串。 返回 None 并把原文放进 date_raw,否则你分不清"没有日期"和"没解析出来"。
  3. 去重键要包含参数上下文。 同一个链接在不同 hl/gl 下可能是两回事,如果一份表里混着多地区数据,stable_id 里应拼上地区和语言。

成本

清洗本身不花钱;数据来自 search 端点,每次成功请求 1 credit,失败自动退款。清洗逻辑写好之后,采集多少、清洗多少,成本只跟请求数有关。

FAQ

要用第三方清洗库吗? 上面四个函数用标准库就够了;真需要更复杂的 HTML 处理再引入解析库。

sitelinks 这种嵌套结构怎么入库? 单独一列存 JSON 字符串,别硬拆成多行------它是附属信息,拆表会让主表膨胀。

日期要不要统一到 UTC? date 是文本日期,通常没有时区信息;存 ISO 日期即可,不要凭空补时间。

把 normalize_item 挂到你现有的采集循环里,入库质量会立刻上一个台阶。

相关推荐
阳光九叶草LXGZXJ5 小时前
达梦数据库-报错-15-列【XXX】长度超出定义
linux·运维·数据库·sql·学习
怕浪猫6 小时前
RAG 面试 6 连问,从原理到优化全部覆盖
python·算法·面试
高洁016 小时前
具身智能中的世界模型训练
人工智能·python·深度学习·机器学习·transformer
无线通信科研笔记6 小时前
IEEE TVT 2026 论文精读与完整复现|相位误差如何重塑近场 RIS 的幅相响应
论文阅读·人工智能·python·算法·论文笔记
字节渡客6 小时前
Redis键明明过期了,业务还在读到旧数据
数据库·redis·spring
朝朝辞暮i6 小时前
VLA 系统学习第 1 课:VLA 到底在干什么?
人工智能·python·计算机视觉·vla
2601_962885727 小时前
如何用 Python 自动识别股票的支撑位与压力位?
开发语言·python
爱吃奥利奥_wen7 小时前
面向对象三板斧:封装、继承、多态,到底在“装“什么、“承“什么、“变“什么?
开发语言·经验分享·c#
北冥有鱼被烹7 小时前
VCSEL全景解析:与光模块NPO CPO的关系、市场量化分析与产业链影响
python
我可能是个假开发7 小时前
FTP Unicode 文件名导致 `550` 的问题分析与解决方案
java·开发语言·spring