净重载重这些参数 AI 搜索会读错:hasMeasurement 与 QuantitativeValue 的写法规范

2026-09-23 01:21:34 0 次浏览
GEOschema.orgJSON-LDPython结构化数据

做仓储设备的老周上周甩来一张截图:他在某个 AI 搜索里问「CBD-15 电动托盘车净重载重多少」,答案写着「约 1.5 公斤」。那台车实际净重载重 1.5 吨。我让他把页面源码发过来,规格表里明明印着「净重载重:1.5 t」,问题在于那个 t 是表头行的小字,值本身只是个裸数字 1.5,抓取的时候值和单位就散开了。

这种事在零售电商和工业品目录里特别集中。参数本来是页面里最容易结构化的部分,写法却常年沿用 HTML 表格的老习惯:数字和单位分开排版,区间写成「85-200」,多语言站点各写各的单位。人看得懂,机器读出来就是另一回事。

适用读者:负责商品页、规格页开发的前端与 SEO 同学,尤其是页面上已经写了 JSON-LD、想让 AI 搜索引用结果更准的团队。读懂这篇只要会 JSON-LD 基本语法就够。

参数读错,多半是单位没跟着值走

生成式引擎优化(Generative Engine Optimization, GEO)讨论的很多问题,落到参数这块其实就是一句话:机器拿到的是一个数字,不是一个量。

秤与尺子对产品参数做精确测量

「1.5」在数据库里只是一个标量,只有当它和单位绑在一起时才是物理量。HTML 表格把这件事交给排版——表头写单位,单元格写数字,视觉上没毛病,结构化数据里却很容易把这份约定丢掉。常见丢法有三种:

  • 值和单位拼成一个字符串,"value": "1.5t",机器只能拿到文本,没法参与换算;
  • 单位写在别的字段或纯文案里,JSON-LD 里压根没出现;
  • 区间值写成 "85-200",既不是单值也没有上下限字段。

AI 搜索拿到裸数字之后不会报错,它会猜。猜错的时候,回答看起来照样通顺,这就是为什么这类问题往往要等客户打电话过来才被发现。

schema.org 里这几个类型怎么摆位置

先把类型关系理清楚,避免把参数塞错地方。Product 上有 hasMeasurement,期望值是 QuantitativeValue;另外还有 additionalProperty,期望值是 PropertyValue。两者都能承载参数,区别在于前者强调「这是一个可被测量的量」,后者是通用键值对。

graph LR
  P[Product] -->|hasMeasurement| QV[QuantitativeValue]
  P -->|additionalProperty| PV[PropertyValue]
  P -->|hasVariant| PM[ProductModel]
  PM -->|hasMeasurement| QV2[QuantitativeValue]
  QV --> N[name 参数名]
  QV --> V[value 数值]
  QV --> R[minValue / maxValue]
  QV --> U[unitCode + unitText]
  QV --> REF[valueReference 限定条件]
  QV --> PID[propertyID 标准定义链接]

QuantitativeValue 自己继承自 StructuredValue,所以它不是「带单位的 PropertyValue」,而是一套完整的量纲描述:数值、单位、单位码、限定条件、外部标准引用都在里面。净重载重、起升高度、货叉长度、爬坡度这些真正带量纲的参数,用 hasMeasurement 挂上去更合适;颜色、货号、是否带侧移器这类没有量纲的属性,留在 additionalProperty 里。

判断标准其实挺省事:这个参数能不能换算单位,能换算就走 QuantitativeValue

AI 搜索读参数的机制:值、单位、单位码三件套

引擎处理一个商品页,大致分四步:抓取页面、抽取结构化数据、把实体和属性落成三元组、生成答案时再拼回自然语言。

结构化数据这一步产出的是三元组,形如:

(CBD-15, hasMeasurement, QuantValue#1)
(QuantValue#1, name, "净重载重")
(QuantValue#1, value, 1.5)
(QuantValue#1, unitCode, "TNE")

生成答案时,模型把三元组还原成句子,单位从 unitCodeunitText 里取。如果单位三元组不存在,模型手里只剩 (QuantValue#1, value, 1.5),它只能从上下文补一个单位进去,补的依据往往是同一页里出现频次最高的那个单位。老周那张页面里「整机自重 85 kg」「蓄电池 24 kg」这类小单位出现得多,1.5 就被顺手安上了公斤。

unitCodeunitText 的分工也在这里体现。unitCode 走 UN/CEFACT Recommendation 20 的代码表,是给机器比对和换算用的;unitText 是给人看的措辞,决定答案里写成「吨」还是「t」。两个都写,引擎换算有依据,生成话术也有依据。只写 unitText 时,「1.5 吨」和「1500 kg」在机器眼里是两条互相认不出来的字符串。

这就引出一个容易被忽略的点:同一个物理量在不同语言站写了不同单位却不给 unitCode,跨页聚合时必然算错。英文站写 3300 lb,中文站写 1.5 t,两边都缺单位码,聚合出来的结果像是两种产品。

区间值别塞进 value 字符串

净重载重是单值,但起升高度、工作温度、货叉宽度这些往往是区间。JSON-LD 里区间有专门的字段:minValuemaxValue,别写成 "85-200",更别写成 "85mm~200mm"

区间写法还牵出 valueReference。它表示这个量是在什么条件下测出来的,类型可以是 QualitativeValue、枚举或另一组量。比如「满载状态下起升高度 85-200 mm」,满载这个限定就挂在 valueReference 上;不限定的话,AI 在回答「空载能升多高」时会直接拿同一组数去答。

propertyID 也值得写。它指向一个统一的属性定义 URL,同一个属性跨几十个 SKU 都指向同一个地址,引擎做实体对齐时能把它们认成同一个东西。地址可以是站内一个专门的参数说明页,也可以是行业标准里的条目,关键是全站一致。

错误写法与正确写法对照

下面这段是老周站内改造前的版本,// 行是说明,实际贴到页面时要删掉:

// 错误示例:值和单位混写、区间塞进 value、单位缺失
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "电动托盘车 CBD-15",
  "sku": "CBD-15-E",
  // 错一:值里带单位,机器拿到的是字符串不是数字
  "hasMeasurement": {
    "@type": "QuantitativeValue",
    "name": "净重载重",
    "value": "1.5t"
  },
  // 错二:区间值塞进 value,minValue/maxValue 全丢
  // 错三:起升高度没有单位,200 到底是 mm 还是 cm 全靠猜
  "additionalProperty": [
    {
      "@type": "PropertyValue",
      "name": "起升高度",
      "value": "85-200"
    }
  ]
}

改成规范写法之后长这样:

// 正确写法:value 是数字,单位单独给 unitCode + unitText,区间拆成上下限
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "电动托盘车 CBD-15",
  "sku": "CBD-15-E",
  // 同一个量可以给两组单位,引擎自己会换算
  "hasMeasurement": [
    {
      "@type": "QuantitativeValue",
      "name": "净重载重",
      "value": 1500,
      // KGM 是 UN/CEFACT 里的千克代码
      "unitCode": "KGM",
      // unitText 决定答案里的措辞
      "unitText": "kg",
      // 指向站内统一的参数定义页,方便跨 SKU 对齐
      "propertyID": "https://example.com/spec/net-load-capacity"
    },
    {
      "@type": "QuantitativeValue",
      "name": "净重载重",
      "value": 1.5,
      // TNE 是公吨代码,和上面的 KGM 描述同一个物理量
      "unitCode": "TNE",
      "unitText": "t",
      "propertyID": "https://example.com/spec/net-load-capacity"
    }
  ],
  "additionalProperty": [
    {
      "@type": "PropertyValue",
      "name": "起升高度",
      // 区间必须拆成上下限,不能写 "85-200"
      "minValue": 85,
      "maxValue": 200,
      // MMT 是毫米代码,和 unitText 要对应
      "unitCode": "MMT",
      "unitText": "mm",
      // 满载这个测量条件单独挂出来
      "valueReference": {
        "@type": "QualitativeValue",
        "name": "满载状态"
      }
    }
  ]
}

把两类写法并排看,差别主要在单位是否独立成字段:

场景 常见错误写法 规范写法 AI 读出来的结果
单值带单位 "value": "1.5t" "value": 1.5 + "unitCode": "TNE" 错误写法只能当文本匹配,换算全丢
区间值 "value": "85-200" "minValue": 85 + "maxValue": 200 错误写法回答区间时会截断或取一半
只有中文单位 "unitText": "公斤" "unitCode": "KGM" 缺单位码时跨语言站无法对齐
测量条件 混在 name 里 "满载起升高度" "valueReference" 单独挂 错误写法回答空载场景会串值
参数名不统一 各 SKU 写「净载重」「额定载重」 统一 name + propertyID 不统一时同一属性被拆成多个三元组

单位码不用死记,常用的几个记住就够:

物理量 unitCode unitText 常见写法 说明
质量(千克) KGM kg / 千克 / 公斤 电商参数里出现频率最高
质量(公吨) TNE t / 吨 工业设备载重常用
长度(毫米) MMT mm / 毫米 尺寸类参数
长度(厘米) CMT cm / 厘米 包装尺寸常用
容积 LTR L / 升 液体、油箱类
功率 KWT kW / 千瓦 电机、整机功率

写个脚本把存量页面扫一遍

改造不能靠人肉翻页面。下面这个脚本把页面里所有 JSON-LD 抽出来,递归找 QuantitativeValue,把四类常见问题报出来。环境是 Python 3.8 以上,装 beautifulsoup4requests 两个包即可:

# -*- coding: utf-8 -*-
# 依赖:pip install beautifulsoup4 requests,Python 3.8+
# 用途:扫描商品页 JSON-LD,找出量测参数的单位与区间写法问题
import json
# json 用来解析页面里 ld+json 的脚本文本
import re
# re 用来识别 "1.5t"、"85-200" 这类值和单位混写
import sys
# sys 用来读命令行参数并把结果通过退出码交给 CI

from bs4 import BeautifulSoup
# BeautifulSoup 只用来取 script 标签,不做 DOM 分析,解析开销小
import requests
# requests 负责抓取页面,批量跑时建议换成 session 复用连接

# UN/CEFACT Recommendation 20 常用单位码,用来核对 unitCode 与 unitText 是否一致
UNIT_PAIRS = {
    "KGM": ("kg", "千克", "公斤"),   # 千克
    "TNE": ("t", "吨"),              # 公吨
    "GRM": ("g", "克"),              # 克
    "MMT": ("mm", "毫米"),           # 毫米
    "CMT": ("cm", "厘米"),           # 厘米
    "MTR": ("m", "米"),              # 米
    "LTR": ("L", "升"),              # 升
    "KWT": ("kW", "千瓦"),           # 千瓦
}
# 上面的键是 UN/CEFACT 代码,值是允许出现在 unitText 里的写法
# 站上新出现一种单位时,先往这张表里补,再改模板
# ---------------- 遍历工具 ----------------
# 为什么要自己写递归:JSON-LD 的嵌套层数不固定
# 有些模板把 hasMeasurement 挂在 hasVariant 的子对象里,只取顶层 key 会漏


def walk(node, path="$"):
    # 深度遍历 JSON-LD,把所有对象连同路径一起吐出来
    if isinstance(node, dict):
        # 字典:先把当前节点交出去,再对每个键值继续下钻
        yield path, node
        for key, val in node.items():
            yield from walk(val, f"{path}.{key}")
    elif isinstance(node, list):
        # 列表:按下标继续下钻
        # 数组逐个下钻,路径里带上序号方便定位
        for idx, val in enumerate(node):
            yield from walk(val, f"{path}[{idx}]")


# ---------------- 规则检查 ----------------
# 每条规则命中就往 problems 里塞一条带路径的说明
# 路径形如 $.hasMeasurement[0],照着它回模板里改字段很省事
def check_quantitative(node, path, problems):
    # 只处理 QuantitativeValue 类型的节点
    if node.get("@type") != "QuantitativeValue":
        return
    # 先把有没有区间字段记下来,后面判断 value 时要用到
    has_range = "minValue" in node or "maxValue" in node
    value = node.get("value")
    # 情况一:区间值塞进了 value 字符串,例如 "85-200"
    if not has_range and isinstance(value, str) and re.search(r"\d\s*[-~到]\s*\d", value):
        problems.append(f"{path}: 区间值写进了 value,应改用 minValue + maxValue")
    # 情况二:值和单位混写,例如 "1.5t"
    if isinstance(value, str) and re.match(r"^[\d.]+\s*[a-zA-Z\u4e00-\u9fa5]+$", value):
        problems.append(f"{path}: value 里混了单位,value 应只放数字")
    # 情况三:两个单位字段全空,AI 只能靠猜
    if not node.get("unitCode") and not node.get("unitText"):
        problems.append(f"{path}: 缺少 unitCode 与 unitText")
    # 情况四:unitCode 与 unitText 描述的不是同一个量纲
    # 典型例子是 unitText 写"公斤"、unitCode 却写成 TNE
    code = node.get("unitCode")
    text = str(node.get("unitText", ""))
    if code in UNIT_PAIRS and text and text.lower() not in [x.lower() for x in UNIT_PAIRS[code]]:
        problems.append(f"{path}: unitCode={code} 与 unitText={text} 不匹配")
    # 情况五:区间上下限写反
    if "minValue" in node and "maxValue" in node and node["minValue"] > node["maxValue"]:
        problems.append(f"{path}: minValue 大于 maxValue")


# ---------------- 页面抓取 ----------------
# 只认 type="application/ld+json" 的 script,微数据格式这次不处理
def audit(url):
    # 取页面 HTML,超时设 15 秒,慢页面直接跳过别卡住整批
    resp = requests.get(url, timeout=15, headers={"User-Agent": "Mozilla/5.0 geo-audit"})
    soup = BeautifulSoup(resp.text, "html.parser")
    problems = []
    blocks = soup.find_all("script", type="application/ld+json")
    # 一个页面常有多段 JSON-LD,坏段单独报错,不影响其他段继续跑
    for idx, tag in enumerate(blocks):
        try:
            data = json.loads(tag.string or "")
        except json.JSONDecodeError as exc:
            # 解析失败多半是模板拼接时多了一个逗号,或者文案里的引号没转义
            problems.append(f"ld+json#{idx}: 解析失败 {exc}")
            continue
        # 遍历到的每个对象都过一遍检查
        for path, node in walk(data):
            check_quantitative(node, path, problems)
    return problems


# ---------------- 入口 ----------------
# 批量扫描时把站点地图导出的 URL 列表逐行传进来即可
def main():
    # 命令行第一个参数是商品页地址,缺参数时直接提示用法
    if len(sys.argv) < 2:
        print("用法: python audit_measurement.py <商品页URL>")
        return 2
    url = sys.argv[1]
    # 一个 URL 对应一份问题清单,空清单表示这一页没问题
    problems = audit(url)
    if not problems:
        # 没发现问题就正常退出
        print(f"{url} 未发现量测写法问题")
        return 0
    # 有问题逐个打印,退出码 1 供 CI 拦截
    # 输出统一带 [问题] 前缀,CI 里直接 grep 这个标记就能出报告
    for item in problems:
        print("[问题]", item)
    return 1


# 退出码语义:0 通过、1 有问题、2 用法错误
if __name__ == "__main__":
    sys.exit(main())

跑起来就一行命令:

# 环境:Python 3.8+,已安装 beautifulsoup4、requests
# 用法:把商品页 URL 传进去,退出码 1 表示页面有问题
python audit_measurement.py "https://example.com/product/cbd-15"
# 只想本地看一段 JSON-LD 里有多少 QuantitativeValue,用 jq 快速过一遍
jq '.. | objects | select(."@type"=="QuantitativeValue")' product.jsonld

把脚本挂进 CI,每次商品页模板改动都跑一遍全量 SKU,比事后接客户电话划算。老周那边是每次发布前扫一遍站点地图里的一千多个商品 URL,扫出来的报告直接进发布卡点。

flowchart TD
  A[取商品页 URL 列表] --> B[抓取 HTML]
  B --> C[抽 ld+json 块]
  C --> D{JSON 能否解析}
  D -- 否 --> E[报语法错并跳过]
  D -- 是 --> F[递归找 QuantitativeValue]
  F --> G{value 是否混入单位}
  G -- 是 --> H[拆出数字并补 unitCode]
  G -- 否 --> I{是否有 minValue/maxValue}
  I -- 否且是区间 --> J[改写为上下限字段]
  I -- 是 --> K{unitCode 与 unitText 是否匹配}
  K -- 否 --> L[按 UN/CEFACT 校正]
  K -- 是 --> M[写入模板并发布]
  H --> M
  J --> M
  L --> M
  M --> N[跑官方校验工具复核]

官方校验工具怎么用

改完不能只看脚本。schema.org 官方的验证器和 Google 的富媒体结果测试都要过一遍,两者看的维度不一样:前者只校验类型和属性是否符合 schema.org 定义,后者额外看商品页富媒体结果要求的必填字段。

用法上有个顺序问题,先跑 validator.schema.org 确认没有类型错误,再跑 Google 的测试看缺哪些必填项。本地调试时把 JSON-LD 直接粘进「代码」页签比填 URL 快,改一行刷一次;线上复核再用 URL 方式,抓的是真实渲染结果。顺便提一句,商品富媒体结果要求 offersreviewaggregateRating 至少有一组,参数写对了但 offers 缺失,富媒体结果照样出不来,只是 AI 搜索的引用不受这条限制。

改造前后,AI 回答变了什么

老周那边改造完跑了三十天,抽了 12 个高频问法每天问一遍,统计口径是「单位正确且数值正确」算命中。数据是内部统计,样本不大,看趋势就够:

指标 改造前 改造后 30 天 说明
单位正确率 41% 93% 主要是补齐 unitCode 带来的
数值正确率 88% 97% 区间值拆分后少了一半截断
区间类问题命中 23% 79% minValue/maxValue 起的作用
跨型号对比类问题命中 12% 61% propertyID 统一后实体能对齐
客户来电纠错的次数 每周 6-8 通 每周 1 通 客服记录的口径

数值正确率本来就不低,因为数字大多是对的,错的只是单位。这也说明一个问题:参数类内容的 GEO 改造,收益集中在单位与区间这两块,不用一上来就重写整张参数表。先扫一遍,把缺单位的补齐,把区间拆开,性价比最高。

几个容易踩的坑

同页面多段 JSON-LD 互相打架是最常见的一个。模板里有一段 Product,运营又在富文本里塞了一段,两段的净重载重一个是 1500 kg 一个是 1.5 t,谁也没写错,但机器会看到两个冲突的三元组。做法是页面只保留一段权威 JSON-LD,其余位置的数据从同一个数据源渲染出来。

多语言站的换算也容易漏。英文站把 1.5 t 写成 3307 lb,数值换算了,单位码却还留着 TNE,unitCodeunitText 直接对不上。改单位得同时改三个字段,这件事在发布前用脚本卡住比人肉检查靠谱。

还有一种是表格和 JSON-LD 不同步。运营在后台改了参数,表格渲染跟着变了,JSON-LD 是模板里写死的旧值。这类问题脚本扫不出来,因为单看结构化数据它是合法的,只能靠把 JSON-LD 也接到同一个数据源上解决。

收尾

参数结构化的门槛不在语法,在于写的时候能不能忍住别把单位塞进值里。数字归数字,单位归单位,区间拆上下限,条件挂 valueReference,这四条做到了,AI 搜索读错参数的概率会掉一大截。至于要不要给每个参数都补 propertyID,看 SKU 规模,几十个以内手写也行,上千个就把参数定义页做成模板变量统一注入。

你们站上的参数是怎么写的,扫出来问题多不多,评论区可以贴几段 JSON-LD 一起看。

参考与延伸

GEO|AI搜索优化|schema.org|QuantitativeValue|JSON-LD|结构化数据|电商商品页

🤖
本内容由 AI 辅助生成,经人工校对审核;部分素材、资料来源于公开网络,仅作个人观点分享与交流使用,无任何商业侵权意图。若内容、图片、文字涉及您的合法著作权、版权权益,请联系本人,核实后将第一时间删除、修改相关内容。