事情是这样的。
10 月底,我给站上的 9 篇文章批量补 SEO,写了个 Python 脚本,一次生成 TDK、canonical 和 JSON-LD。
脚本跑完,页面上线,我自己点开看了两眼,标题在、描述在、canonical 在,觉得成了。
过了三天,我想验证一下结构化数据,跑了个解析:
import json, re
html = open('post/26-docker-family.html').read()
m = re.search(r'<script type="application/ld\+json">(.*?)</script>', html, re.S)
data = json.loads(m.group(1))
报错:
json.decoder.JSONDecodeError: Expecting ',' delimiter: line 4 column 68 (char 124)
我第一反应是脚本写错了。打开那篇文章的源码,找到那段 JSON-LD:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Mac mini 当服务器第 2 弹:Docker 全家桶一次拉满",
看到这里我明白了。
标题里有中文冒号":"和中文引号"",我脚本里是这么拼的:
json_ld = f'''
{{
"headline": "{title}",
"description": "{desc}"
}}
'''
title 变量里带着中文引号和括号,直接塞进 f-string,生成出来的 JSON 就是坏的。
为什么中文引号会毁掉 JSON
JSON 规定字符串必须用英文双引号 " 包裹。中文引号 " " 在 JSON 里是普通字符,可以出现,但中文引号长得太像英文引号,很多解析器会混淆。
更麻烦的是另一个问题:我的标题里有英文引号的时候,直接拼接会提前闭合字符串。
比如标题是 Mac mini 当服务器第 2 弹:"全家桶"一次拉满,拼出来是:
"headline": "Mac mini 当服务器第 2 弹:"全家桶"一次拉满",
那个英文引号直接把 headline 的值截断了,后面的 全家桶 变成非法字符,JSON 报错。
我的情况是中文引号版的同一类问题——中文书名号、中文冒号、中文引号,全都可能让解析器出问题,尤其是当内容里同时出现英文符号的时候。
正确的写法
一句话:别拼字符串,用 json.dumps。
import json
obj = {
"@context": "https://schema.org",
"@type": "Article",
"headline": title,
"description": desc,
"datePublished": published,
"dateModified": modified,
"author": {"@type": "Person", "name": "陌笑技术分享"},
"mainEntityOfPage": {"@type": "WebPage", "@id": canonical_url},
}
json_ld = json.dumps(obj, ensure_ascii=False, indent=2)
三个参数的意义:
ensure_ascii=False:中文原样输出,不转成\u4e2d\u6587,可读性好indent=2:格式化,方便自己排查- 自动处理转义:引号、反斜杠、换行,
json.dumps全部搞定
还有一个细节:如果内容里有 </script>,需要转义,否则会提前闭合脚本标签:
json_ld = json.dumps(obj, ensure_ascii=False).replace("</", "<\\/")
顺手发现的另外两个问题
排查 JSON-LD 的时候,我把整个 head 都看了一遍,又发现两个。
问题一:keywords 全是"分类名,站名"
<meta name="keywords" content="Docker,陌笑技术分享">
9 篇文章都是这个格式,等于没写。
修法:按每篇内容定制长尾词。我在脚本里加了个映射表:
kw_map = {
"26-docker-family": "Docker全家桶,Mac mini服务器,Docker Compose,家庭服务器搭建",
"27-docker-basics": "Docker入门,Docker教程,容器,docker compose",
"28-alist-mount": "Alist教程,网盘聚合,百度网盘挂载,自建网盘",
# ...
}
问题二:canonical 不带 www,推送带 www
<link rel="canonical" href="https://moxiaoa.com/post/26-docker-family.html">
但 sitemap 和主动推送里都是 https://www.moxiaoa.com/...。
搜索引擎看到两个地址指向同一内容,不知道该认哪个。
修法:全站统一带 www,包括服务器上做 301。
修复之后怎么验证
批量改 9 篇,写完不能只看一眼就算。我做了三层验证:
第一层:脚本解析
import json, re, glob
for f in glob.glob('post/*.html'):
html = open(f).read()
m = re.search(r'<script type="application/ld\+json">(.*?)</script>', html, re.S)
if not m:
print(f, '缺少 JSON-LD')
continue
try:
obj = json.loads(m.group(1))
print(f, 'OK', obj['headline'][:20])
except Exception as e:
print(f, '解析失败', e)
9 篇全部 OK 才算过。
第二层:线上抓取
本地文件对了,不代表线上对。要真的请求一次:
curl -s https://www.moxiaoa.com/post/26-docker-family.html | \
python3 -c "import sys,re,json; h=sys.stdin.read(); \
m=re.search(r'application/ld+json\">(.*?)</script>', h, re.S); \
print(json.loads(m.group(1))['headline'])"
第三层:看实际渲染
浏览器打开页面,按 F12,看 head 里这几行有没有出现,有没有被其他代码覆盖。
有些站会动态改 head,静态文件对了,运行时又被脚本改了,这种最难查。
教训
这三件事花了我一个晚上,但都是可以避免的:
1. 生成 JSON 永远用序列化函数
手拼字符串的坑,不止引号,还有换行、反斜杠、控制字符。用 json.dumps 一次解决。
2. 批量改完必须机器验证
9 篇文章,我肉眼看了两篇觉得没问题。实际上第一篇就是坏的。批量操作一定要写验证脚本,跑一遍才有底。
3. 一致性比完美更重要
canonical、sitemap、推送地址,只要有一个不一致,前面都白做。定一个规则(比如永远带 www),然后全站贯彻。
现在的状态
9 篇文章的 TDK、canonical、JSON-LD 全部重生成,验证通过,线上抓取确认无误。
索引量开始动了,从 1 涨到 7。
我这次算是把 SEO 的地基重新夯了一遍。剩下的就是等——等搜索引擎重新评估,等内容积累。
SEO 这件事,做对的事比做多的事重要。
有在改站点 SEO 的朋友,记得跑一遍 json.loads 验证,能省很多时间。
如果你也在折腾这套东西,碰到卡住的地方,可以加我微信 lige0552 聊聊。白天要上班,消息回得慢,看到都会回。