结构化标注缺失怎么查:JSON-LD抽取与每日巡检脚本实现

页面正文之外还有一层机器可读的声明:主体名称、地址、营业时间、产品参数、资质有效期。这一层通常由 JSON-LD、Microdata 与 Open Graph 三套写法承载,其中 JSON-LD 因为不改视觉结构、可以独立放在
里,成了实际落地中最常见的一种。

问题是它经常缺失,或者写了一半:只有 WebPage 没有 Organization;有 name 没有 address;地址写了但只写到市;@type 写成自造词。这类缺陷不会导致页面报错,浏览器与搜索引擎都不会提醒,只有抽取端在需要那个字段的时候才发现------那时已经体现为回答里缺一段话。

巡检这一层的成本很低,脚本不到一百行,难的地方在于判据设计:什么算缺、缺到哪一级要报警。

一、三套写法的读取顺序

先明确一个工程判断:读取顺序按 JSON-LD → Microdata → Open Graph 排,前一层拿到就用,拿不到才降级。原因是三者的语义强度不同。

· JSON-LD :显式声明类型与属性,最接近「页面自己说清楚这是什么」,支持多对象嵌套与图引用(通过 @id)。

· Microdata :属性挂在视觉 DOM 上(itemtype、itemprop),改动风险高,落地率明显低于 JSON-LD,但老站点常见。

· Open Graph:面向社交分享设计,字段集固定且很窄(标题、描述、图片、类型),不能当成结构化数据的替代。

一个页面只有 Open Graph 而无前两者,在抽取视角下等于没做标注------OG 提供的是摘要,不是字段。这一条在巡检报表里要单列,因为把它算成「已标注」会让整份结果失真。

二、解析与字段齐备度计算

主体逻辑是抽取所有 JSON-LD 块,合并成一个节点表,然后按类型检查必查字段。JSON 解析失败率不低(尾逗号、单引号、把 JSON 包在 HTML 注释里),必须给每一块单独 try,不能让一块坏数据毁掉整页判断。

python 复制代码
import json, re
from collections import defaultdict

LD_BLOCK = re.compile(
    r'<script[^>]+type=["\']application/ld\+json["\'][^>]*>(.*?)</script>',
    re.S | re.I)

def parse_blocks(html):
    nodes, broken = [], 0
    for raw in LD_BLOCK.findall(html):
        txt = raw.strip()
        if txt.startswith("<!--"):
            txt = txt.strip("<!-")
        try:
            data = json.loads(txt)
        except Exception:
            broken += 1
            continue
        nodes.extend(flatten(data))
    return nodes, broken

def flatten(data):
    """@graph 展开、列表摊平,返回带 @type 的节点列表。"""
    out = []
    if isinstance(data, list):
        for d in data:
            out.extend(flatten(d))
    elif isinstance(data, dict):
        if "@graph" in data:
            out.extend(flatten(data["@graph"]))
        if data.get("@type"):
            out.append(data)
    return out

broken 计数必须保留并进入报表。一块解析不了的 JSON-LD 比完全没有更糟------页面作者以为做了标注,读取端拿到的却是空。巡检报表里把这类标成「声明存在但不可解析」,与「未声明」分开列,处置方式完全不同:前者是修语法,后者是补内容。

三、必查字段表与判据分级

必查字段按页面类型定,不按站点定。一份可用的初始表:

| 页面类型 | @type | 必查字段 | 缺失级别 |

| --- | --- | --- | --- |

| 机构主页 | Organization | name、url、address、sameAs | 硬缺失 |

| 文章页 | Article / NewsArticle | headline、datePublished、dateModified、author | 硬缺失 |

| 产品页 | Product | name、description、brand、offers | 硬缺失 |

| 联系方式页 | Organization | telephone、email、contactPoint | 软缺失 |

| 面包屑 | BreadcrumbList | itemListElement(≥2 项) | 提示 |

分级不是为了好看。「硬缺失」= 该类型页面缺了关键字段,抽取端会退回正文猜测,报错概率高;「软缺失」= 有替代来源(正文里也写着),影响可控;「提示」= 不做也不影响主流程,做了改善展示。三档在日报里分别对应不同的处理时限,否则所有告警都变成「下周再看」。

地址字段还要额外做一层颗粒度检查:addressLocality 为空或只到省级,抽取端读出来的地址就不具备可核对性。这一项常见但不显眼,值得单独写一条断言。

四、同一主体多处声明的冲突检测

比缺失更麻烦的是冲突:机构主页声明的地址与联系页地址不一致;sameAs 列了三个外部主页,其中一个已经改版换名;文章页 dateModified 早于 datePublished。这类问题不会因为字段齐全而被发现,只能靠跨页比对。

python 复制代码
def conflicts_by_key(nodes, key):
    bucket = defaultdict(set)
    for n in nodes:
        v = n.get(key)
        if isinstance(v, str) and v.strip():
            bucket[n.get("@type", "?")].add(v.strip())
    return {t: sorted(vs) for t, vs in bucket.items() if len(vs) > 1}

# 用法:把整站抓回来的节点合成一份大表,按字段名分组看唯一值数
# 期望同一类型下同一字段只有一个值;多值即候选冲突。

冲突判据要留人工确认口子。同一 Organization 下出现两个 telephone 是合理的(售前与售后各一条),出现两个 address 就值得追一下。简单起见,脚本对电话只统计数量、对地址与 url 判唯一,后续按实际误报率再放宽或收紧。

五、每日任务怎么落地

一次全站扫描在几百到几千页的量级上,用两路并发加超时控制可以在十几分钟内完成,足够每天跑。任务设计上有三点值得坚持:

· 快照而不是覆盖 :每天的解析结果按 URL + 抓取日期 存一份,不覆盖历史。字段消失这件事只有对比历史才能发现,单看当天报表看不出来。

· 报表按类型分组,不按 URL 分组 :一个模板缺 dateModified,会同时影响几百篇文章页。按 URL 出报表会得到一个很大的数字,按模板分组只有几行。

· 变更项单列:与前一天相比新增、消失、改值的字段单独一节。内容改错往往在改错当天最容易判断,两天之后就没人记得当时为什么改。

调度侧不需要额外依赖,cron 或任务计划程序每天一次即可,抓取失败重试两次。真正要注意的是 UA 与限速------巡检脚本被站点当成攻击,是这类内部工具最常见的失效方式。

六、几类常见误判

第一类是模板占位符未替换。页面上留的是 {organization_name} 或空字符串,脚本读到一个「已声明但无意义」的值。对策是加一层最小可用性判断:长度低于 2 的字符串、含花括号的值,一律视为缺失。

第二类是把 Open Graph 当 JSON-LD。OG 的 og:title 覆盖不了 Article.headline 缺失,报表里如果按「页面有标题声明」就算通过,会漏掉整类问题。判据要按字段名而不是按是否有任何元信息。

第三类是嵌套对象被摊平后丢失归属。contactPoint 是数组,telephone 在每个元素里;摊平成一张大表之后,很难判断某个电话属于哪个联络方式。上面的 flatten 只处理 @graph 与顶层列表,属性内部的嵌套结构保留原样,就是为这一点。

第四类是缓存导致的旧声明仍在返回。改过 JSON-LD 之后,CDN 边缘节点上的 HTML 可能仍是旧版,脚本连续几天报同一个缺失。对策是在巡检结果里同时记录响应头里的缓存信息与页面 dateModified,两条一起看才判得准。

七、小结

结构化标注的巡检,价值不在「补上多少个字段」,在于让这一层变成一个可回归的对象。字段缺失、语法错误、跨页冲突这三类问题,一旦有每天跑一次的脚本盯着,就不会以「客户问到才发现」的形式暴露。判据要保守、报表要分组、历史要留快照,这三点比解析实现本身更决定这套脚本能用多久。

(本文由一支长期做网页结构化数据与检索工程的技术团队整理,欢迎同行交流指正。)

相关推荐
2601_962966641 小时前
数学与应用数学专业准备风险管理岗,2027届秋招金融知识和工具技能怎么补?
python·sql
码流子1 小时前
02-易迁信创兼容引擎
大数据·数据库·python
kimnoic1 小时前
Python常见模块及其用法示例详解
开发语言·python
databook1 小时前
Scikit-Learn实战:5步搞定PCA降维
python·机器学习·scikit-learn
溪语流沙2 小时前
【Web全栈进阶】JWT无状态认证:签发、校验、刷新
前端·git·python·github
reasonsummer2 小时前
【办公类-112-08】20261009园园通信息合并配学号拆班(数据更新,用年月日期区分版本)
python
阿狗童鞋2 小时前
Python爬虫进阶实战指南
开发语言·爬虫·python
中原第一高手2 小时前
fofatoto 1.8.0 发布:启动即知新版本、中文进度面板与更干净的 Web 日志
python·网络安全·开源·资产测绘·fofa
APIshop2 小时前
淘宝详情接口全解析:从官方开放平台到第三方数据服务
java·python·api