给外贸独立站做内容改版,最容易卡在验收:产品页的参数补全了没有、文章页能不能答上标题那个问题、问答页有几条。这些判断过去都靠人眼看,改完一批就没人再回头看一遍。
这篇把三类页面的判断标准写成可计算的指标,配一段能跑的脚本。
先说清定位。这些指标是启发式的自检工具,用来发现问题与看趋势,不是任何平台的判定标准,也不能拿它的分数去对外说效果。
一、为什么要把写法变成指标
三类页面的写法要求本身很清楚:产品页的参数与认证要能被提取,文章页要有一句直接回答标题的话,问答页要有成条的问答与对应的标记。
问题在于不可持续。一次性改完可以,但产品线在增、工艺在变,半年之后没人知道哪些页面又退回去了。
变成指标之后,改版前跑一次、改完跑一次,看的是变化量而不是绝对值。这一层与外部观测是两套数据:站内的合格度是自己可控的,站外被 AI 引用与由此带来的点击都是被动接收的。
二、三个指标的定义与合格线
| 指标 | 定义 | 计算方式 | 合格线 |
|---|---|---|---|
| 参数可提取率 | 页面上的参数有多少写在文字与表格里 | 文字与表格中的参数条数 ÷ 全部参数条数(含图片里的) | 由站点自定,建议先取当前值再看提升 |
| 标题答案匹配度 | 标题问句能不能在正文里找到直答句 | 标题关键词命中率 × 前 300 字内是否出现同义判断句 | 启发式打分,只用于排序 |
| 问答覆盖度 | 问答条目数与结构化数据标记是否对应 | 问句型小标题条数、问答标记条数,两者取小 | 标记数不少于条目数的八成 |
三张合格线的量纲都是自己定的。跨站比较没有意义,因为不同品类的参数密度差得很远,同一套阈值没法通用。
三、抓取与打分脚本
脚本做三件事:取回页面的 HTML、按三个指标打分、把结果写成表。抓取用 requests 加解析库就够,页面数量在几百这个量级不需要上异步。
# page_score.py
# pip install requests beautifulsoup4
import csv, json, re
import requests
from bs4 import BeautifulSoup
# 带单位的参数写法:数值 + 单位
PARAM_WITH_UNIT = re.compile(r'\d+(?:\.\d+)?\s*(?:mm|cm|kg|g|MPa|N|h|%|\u00b0C|小时|天)')
# 问句型小标题的粗判:以疑问词开头或结尾带问号
QUESTION_HEAD = re.compile(r'^(.{2,40}?)(?:\uff1f|\?)$')
TITLE_WORD = re.compile(r'[\u4e00-\u9fa5A-Za-z]{2,}')
def fetch(url, timeout=20):
r = requests.get(url, headers={'User-Agent':
'Mozilla/5.0 (compatible; site-audit/1.0)'}, timeout=timeout)
r.raise_for_status()
return BeautifulSoup(r.text, 'html.parser')
def score_param(soup):
"""参数可提取率:文字与表格里的参数 占 全部参数(含图片替代文字)的比例"""
text = ' '.join(t.get_text(' ', strip=True)
for t in soup.find_all(['p', 'li', 'td', 'th', 'h2', 'h3']))
in_text = len(PARAM_WITH_UNIT.findall(text))
in_alt = 0
for img in soup.find_all('img'):
alt = img.get('alt') or ''
in_alt += len(PARAM_WITH_UNIT.findall(alt))
total = in_text + in_alt
return round(100.0 * in_text / total, 1) if total else None
def score_answer(soup, title):
"""标题答案匹配度:标题关键词在正文前 300 字里的命中率"""
words = set(TITLE_WORD.findall(title))
body = ' '.join(p.get_text(' ', strip=True) for p in soup.find_all('p'))
head = body[:300]
if not words:
return None
hit = sum(1 for w in words if w in head)
return round(100.0 * hit / len(words), 1)
def score_faq(soup):
"""问答覆盖度:问句型小标题条数 与 问答标记条数"""
qs = [h for h in soup.find_all(['h2', 'h3'])
if QUESTION_HEAD.match(h.get_text(strip=True))]
marked = len(soup.select('[itemtype*="Question"]'))
return {'questions': len(qs), 'marked': marked}
def audit(urls):
rows = []
for kind, url in urls:
soup = fetch(url)
title = (soup.title.get_text(strip=True) if soup.title else '')
rows.append({
'kind': kind,
'url': url,
'param_rate': score_param(soup),
'answer_match': score_answer(soup, title),
'faq': score_faq(soup),
})
return rows
if __name__ == '__main__':
targets = [(r[0], r[1]) for r in csv.reader(open('urls.csv', encoding='utf-8'))]
rows = audit(targets)
with open('score.csv', 'w', newline='', encoding='utf-8') as f:
w = csv.DictWriter(f, fieldnames=['kind', 'url', 'param_rate', 'answer_match', 'faq'])
w.writeheader()
w.writerows(rows)
print(json.dumps(rows[:3], ensure_ascii=False, indent=2))
输入是一张两张列的清单:页面类型与地址。输出是一张分数表,可以直接和上一次的结果并排比。
几个实现上的注意点。参数正则里的单位表要按自己的品类补,机械类要加公差与硬度,纺织类要加克重与幅宽。问句型小标题的粗判只看是否以问号结尾,长标题需要再截断一次。问答标记那一段按实际使用的语法调整,用 JSON-LD 的站要改成解析脚本内容。
四、把三项指标接进改版流程
接法有两种,成本都不高。
轻量做法是放在发版清单里。每次产品页或文章页改动之后跑一次,分数掉的页面回到修改列表。这套做法不需要额外的服务。我是先在自己站上挑了二十来个页面跑了一遍,把分数表存下来,隔周再跑一次,才看清哪些页面是真的在退化。
稍重一点的做法是接进构建流程,把脚本挂在部署的下一步,输出结果存成带日期的文件。这样就有了时间序列,能看出哪些页面在缓慢退化。
建议把站内分数与站外观测分开存。站内分数在代码库里跟着版本走,站外的曝光与引用另存一份,两张表靠 URL 关联,不要混成一张表,否则口径会乱。
顺带说一句元数据的处理。页面标题、meta 描述、结构化数据、图片替代文字这四类字段,脚本里都能顺手取到,建议在分数表里各占一列。它们不影响三类页面的主体分数,但退化的速度往往比正文快。
五、判读时容易踩的三个坑
第一个坑是把参数可提取率当成越高越好。把无意义的数字堆进正文也能刷高这个分数,评出来的页面反而更差。这个指标要看的是"降下来又涨回去"的变化,不是绝对值。
第二个坑是拿标题答案匹配度当结论。它是关键词级别的粗判,同义词和改写句都会漏掉,只适合用来排序,挑出最不像的几页去看一眼。
第三个坑是只跑一次就下判断。单次分数的作用有限,同一批页面连着跑四周,趋势才有信息量。这与独立站SEO 里看排名的做法一样,单日数据基本没有意义。
六、这套做法的边界
三个指标都是代理量。它们能指出"这里可能有问题",不能证明"改完就会变好"。任何把它当成绩效指标的做法都会失真。
它也不覆盖内容质量本身。一段参数写得对不对、一个工艺结论是不是过时,脚本判不了,这部分还是得有人过。脚本的作用是让人的注意力落在少数几页上,不是替代判断。
独立站搭建阶段把这三类页面的字段位置留出来,后期的分数维护成本会低很多;我们这边的站用的是 NeoGress,产品参数位与问答段在后台是对应字段,改版时脚本能直接取到,不用再去猜结构。适不适合你的项目,看团队里有没有人能固定每周跑一次。
最后一个问题:你们现在是用什么方式记录页面改版前后的变化?评论区说一下做法,我看看有没有更适合接进自动化流程的形式。