我的 WordPress 博客已经积累了两千多篇中文文章,其中相当一部分历史文章没有填写摘要。
从表面上看,这个需求似乎并不复杂:查询所有没有摘要的文章,把标题和正文交给 AI,再将生成的摘要写回 WordPress。
但真正开始规划之后,我很快意识到,问题并不只是“选哪个模型生成摘要”。
这些文章跨越了十多年,使用过不同编辑器、代码高亮插件和内容结构。部分文章后来又经过 Gutenberg 转换、主题迁移或代码区块替换。如果在没有盘点格式的情况下,直接批量读取正文、调用 AI 并写回数据库,很难准确判断哪些文章可以自动处理,哪些文章需要人工检查。
因此,我没有立即开始批量生成摘要,而是先建立了一套只读的历史文章格式审计管线。
这套管线当前的任务不是生成摘要,也不会写回 WordPress。它只负责:
- 从生产环境受控导出文章;
- 识别历史文章的编辑器和代码格式;
- 检测可能损坏或不确定的结构;
- 对文章进行确定性的风险分类;
- 为未来的 AI 摘要补全建立安全候选范围。

wordpress-ai-excerpt-backfill 项目的中英文 README 与目录结构一、为什么不能直接批量生成摘要
我的历史文章并不是由一种统一格式构成的。
其中既有早期 Classic Editor 生成的普通 HTML,也有后来使用 Gutenberg 编辑或转换的文章。代码展示方式也经历过多次变化,包括:
- 普通
<pre>和<code>; - SyntaxHighlighter 短代码;
- SyntaxHighlighter Gutenberg 区块;
- Code Block Pro;
- Gutenberg 内置代码区块;
- 编辑器格式混合的文章;
- 未知或已经停用的短代码。
有些文章虽然发布于 2014 年,但在 2026 年重新编辑过,正文中同时包含 Gutenberg 区块和没有转换的经典段落。
如果只根据文章发布时间判断格式,显然不可靠。
另外,代码内容中经常包含方括号、HTML 标签、正则表达式和模板语法。例如:
[if]
[endif]
[name]
[unknown-widget id="123"]
这些内容可能是代码示例,也可能是真正的 WordPress 短代码。检测器如果处理不当,就会把正常代码误判为未知短代码或损坏结构。
因此,摘要补全之前至少需要回答几个问题:
- 文章究竟属于 Gutenberg、Classic Editor,还是 mixed?
- 是否包含 SyntaxHighlighter、Code Block Pro 或经典代码结构?
- Gutenberg 区块是否完整配对?
- 短代码是否存在未闭合、孤立结束或错位嵌套?
- 代码区域中的方括号内容是否会被误判?
- 哪些文章可以自动处理,哪些必须排除或人工复核?
如果这些问题都没有答案,直接写回生产数据并不稳妥。
二、先把项目范围限制为“只读审计”
我为这个项目建立了一个独立目录:
/home/wangqiang/code/wordpress-ai-excerpt-backfill
生产环境中的工具则部署在 Web 根目录之外:
/root/tools/wordpress-ai-excerpt-backfill
WordPress 仍位于:
/data/wwwroot/www.shuijingwanwq.com
项目当前明确不包含以下能力:
- AI 摘要生成;
- WordPress 摘要写回;
- 批量修改文章;
- 修改分类、标签或其他文章字段;
- 数据库写操作;
- 缓存清理;
- 翻译生成;
- AI API 调用。
也就是说,即使检测器存在误判,目前最多只会影响本地分析结果,不会直接修改生产文章。
这是我为后续自动化设置的第一层安全边界。
三、生产导出器只负责读取
生产端使用 PHP 导出器读取 WordPress 数据。
导出器只处理满足条件的文章:
post_type=post;post_status=publish;- Polylang 语言为中文;
- 按文章 ID 递增读取。
导出结果采用 JSONL,每行对应一篇文章。字段中包含后续校验需要的信息,例如:
post_id
post_type
post_status
language
published_at
modified_at
title
excerpt
content
permalink
content_sha256
其中,content_sha256 用于确认正文在导出、下载和分析过程中没有发生变化。
导出器没有 WordPress 写入函数,也没有 $wpdb->insert()、$wpdb->update() 或 UPDATE、DELETE 等 SQL 操作。
正式运行入口要求显式提供导出数量:
bin/run-readonly-export.sh \
--limit 100 \
--after-id 95 \
--batch-size 100
目前单次导出数量被限制为:
1~100
最初,这个上限只存在于 Shell 运行脚本中。提交前审计时,我发现如果有人绕过 Shell,直接调用 PHP 导出器,就可能传入更大的数量。
因此,后来又在 PHP 导出器内部增加了独立硬上限:
const SWQ_MAX_EXPORT_LIMIT = 100;
这样,无论从哪个入口调用,超过 100 条都会在接触 Polylang 和数据库查询之前被拒绝。
这是一项典型的纵深防护:不能只依赖最外层入口正确。
四、部署和导出必须是两件独立的事
生产部署脚本和导出脚本被明确分开。
部署脚本默认不会执行任何网络操作:
bin/deploy-to-production.sh --dry-run
只有显式指定:
bin/deploy-to-production.sh --deploy
才会连接生产服务器。
部署过程包括:
- 将 PHP 文件上传为临时文件;
- 在服务器上运行 PHP lint;
- 核对文件哈希;
- 设置严格权限;
- 使用原子重命名替换正式文件。
部署不会自动运行导出。
同样,导出脚本只运行已经部署好的固定 PHP 文件,不会顺便上传或替换生产代码。
这样做虽然多了一步操作,但可以避免“部署完成后意外立即处理生产数据”这种耦合风险。
五、导出结果先校验,再变成正式文件
远程导出不会直接写入最终文件名。
运行脚本先生成临时 JSONL,然后使用固定 Python 解释器检查:
- 文件是否存在;
- 每一行是否为有效 JSON;
- 记录数是否与预期一致;
- 必需字段是否完整;
- 是否存在重复文章 ID;
- 语言和发布状态是否符合要求。
全部检查通过后,临时文件才会原子重命名为正式 JSONL。
如果导出或验证失败,临时文件会被清理,不会留下一个看起来正常、实际上只有部分记录的正式结果。
第一次测试导出了 3 篇文章。随后扩大到 20 篇,最后又完成了一批 100 篇的受控导出。

100 篇导出文件的结果为:
记录数:100
文件大小:558106 字节
服务器文件和本地下载文件的 SHA-256 完全一致,说明传输过程中没有发生改变。
六、本地分析器继续保持输入只读
生产 JSONL 下载到本地后,再由:
bin/analyze-export.py
进行分析。
分析器要求显式提供预期记录数:
bin/analyze-export.py \
--expected-count 100 \
data/raw/example.jsonl \
data/analysis/example.analysis.jsonl
--expected-count 的允许范围同样是:
1~100
分析开始前会验证:
- JSONL schema;
- 精确记录数量;
- 文章类型;
- 发布状态;
- Polylang 语言;
- 重复文章 ID;
- 正文 SHA-256。
正式分析结果也不是直接覆盖写入,而是先写临时文件,执行 flush 和 fsync 后再原子替换。
提交前审计时,又发现了一个容易忽视的问题:如果用户把输入文件和输出文件写成同一个路径,分析完成后,原始导出文件可能会被分析结果替换。
例如:
bin/analyze-export.py \
--expected-count 100 \
data/raw/example.jsonl \
data/raw/example.jsonl
后来我为分析器增加了同文件保护,覆盖以下情况:
- 输入和输出路径字符串完全相同;
./形成的等价路径;..形成的等价路径;- 输出是输入的符号链接;
- 输出与输入是同一 inode 的硬链接。
只要输入和输出最终指向同一个文件,分析器就会在读取输入和创建临时文件之前拒绝执行。
七、分析结果不包含正文等敏感字段
原始 JSONL 中包含正文、标题、摘要和 URL,因此被视为本地敏感数据。
分析结果只保留格式分类和风险判断所需的脱敏字段,例如:
post_id
published_at
editor_format
code_format
primary_format
matched_rule_ids
risk_level
risk_reasons
manual_review
phase1_status
phase1_exclusion_reasons
项目中的 .gitignore 明确排除了:
data/raw/
data/analysis/
__pycache__/
*.pyc
因此,公开 GitHub 仓库不会包含生产文章正文、原始导出文件或分析结果。
八、100 篇真实样本暴露出的历史格式差异
完成 100 篇样本分析后,得到的编辑器格式分布为:
classic:98
mixed:2
代码格式分布为:
none:95
classic-pre-code:4
syntaxhighlighter:1
风险等级为:
low:91
medium:7
high:2
manual-review:0
这一批文章的发布时间大致覆盖 2013 年至 2016 年。
结果说明,早期文章主体仍然是 Classic Editor 格式,但也已经出现:
- 经典
<pre>/<code>代码结构; - SyntaxHighlighter;
- 后期编辑形成的 Gutenberg 与经典内容混合;
- 多图片文章;
- 代码中容易被误判为短代码的方括号内容。

九、真实样本帮助发现了多个检测误判
这次工作中,真正耗费时间的并不是导出 100 篇文章,而是逐步修正检测规则。
1. 普通方括号单词被误判为短代码
历史文章和代码示例中可能出现:
[length]
[method]
最初,检测器可能把这类未知方括号单词当作未闭合短代码。
后来规则被收紧:未知裸方括号单词必须具备更强证据,例如配对结构、属性赋值或显式自闭合,才会被当作短代码。
2. caption 没有被识别为已知配对短代码
WordPress 历史图片说明常使用:
[caption]...[/caption]
将 caption 加入已知和配对短代码后,历史图片结构才能得到正确分类。
3. 代码区域中的方括号被误判
最初的短代码检测器会扫描整个正文。
这意味着以下区域中的方括号文本,也可能被当作 WordPress 短代码:
- SyntaxHighlighter Gutenberg 区块;
- Code Block Pro;
<pre>;<code>;内部。...
后来增加了代码区域保护:
- 计算受保护代码区间;
- 合并重叠区间;
- 普通短代码匹配如果落入代码区域就忽略;
外层开始和结束标记仍参与严格配对检测。
这样既避免了代码内容误判,又保留了未闭合、孤立结束和错位嵌套检测。
4. Gutenberg 区块外的空结构被误判为 mixed
最初,只要 Gutenberg 区块外还剩任何 HTML 标签,就会触发:
CLASSIC_SUBSTANTIAL_OUTSIDE_BLOCKS
这会把以下无语义残留也判为 mixed:
<p></p>
<p> </p>
<p><br></p>
<div><span></span></div>
后来增加了基于 HTMLParser 的结构判断。
以下内容可以忽略:
- 普通空白和换行;
; ; ;- 指定零宽字符;
- 成对的空
p、div、span、section; <br>和<br/>。
但只要存在一个真实汉字、字母、数字或标点,仍然判为 mixed。
媒体、链接、功能性标签、未知标签和损坏 HTML,也继续保守判为实质内容。
我没有使用“少于 50 个字符就忽略”这类固定阈值,因为短文本也可能是一条重要提示、警告或命令说明。
5. 非 void 标签的自闭合写法
审计中还发现:
<p/>
<div/>
<span/>
<section/>
最初会被当作安全空结构。
但这些并不是真正的 HTML void 元素,自闭合斜杠也不一定表示它们在 HTML 语义中已经正常闭合。
最终规则被收紧:
<br/>可以忽略;<p/>、<div/>、<span/>、<section/>保守判为实质或异常结构。
十、为什么两篇 mixed 文章最终仍然保留
真实样本中有两篇文章被判断为 mixed。
最初我怀疑检测器可能只删除了 Gutenberg 注释,而没有删除整个区块内容,导致区块内部 HTML 被误算为经典正文。
诊断后确认,Gutenberg span 计算没有问题。检测器删除的是从开始注释到结束注释之间的完整顶层区块。
其中一篇文章在 Gutenberg 区块外仍有大量经典正文,mixed 显然正确。
另一篇文章只有 56 个区块外可见字符,但它们并不是换行、空段落或不可见字符,而是两个完整的经典 <p> 段落,其中包含:
- 43 个汉字;
- 3 个 ASCII 字母;
- 4 个数字;
- 6 个标点。
这些段落位于两个 SyntaxHighlighter 区块之间,属于真实可见内容。
因此,即使它们只占全文很小比例,也不应因为“字符太少”而被忽略。
这次诊断让我最终确定:mixed 判断应优先基于结构语义,而不是字符数量或全文比例。
十一、第一阶段资格范围保持严格
当前第一阶段资格判断有意设置得很窄。
只有同时满足以下条件的文章,才可能进入未来的自动摘要候选范围:
- WordPress 已发布文章;
- Polylang 语言明确为中文;
- Gutenberg 结构完整;
- 包含完整 Code Block Pro 结构;
- 不包含 SyntaxHighlighter;
- 不属于 mixed;
- 不包含 unknown 格式;
- 不需要 manual-review。
gutenberg/plain、Classic Editor 和其他历史格式目前仍然只作为盘点或迁移候选,不直接进入自动摘要阶段。
这并不意味着它们永远不能生成摘要,而是说明它们需要不同的内容保护、迁移或审核策略。
十二、143 个自动化测试作为首个稳定基线
在逐步补充边界测试后,项目最终形成了 143 个自动化测试。
测试范围包括:
- JSONL 输入合约;
- 精确记录数检查;
- 原子写入;
- 输入文件不变;
- 输入输出同文件保护;
- Gutenberg 区块配对;
- Classic Editor 和 mixed 分类;
- Code Block Pro;
- SyntaxHighlighter;
<pre>/<code>;- 已知和未知短代码;
- 损坏结构;
- 空结构语义判断;
- 生产脚本只读约束;
- 部署 dry-run;
- PHP 与 Shell 导出上限一致;
- 第一阶段资格判断。
完整测试命令为:
PYTHONDONTWRITEBYTECODE=1 \
python3 -m unittest discover -s tests -v
首次提交前,143 个测试全部通过。

十三、为项目增加中英文 README
这个项目最终采用了中文优先的双语文档结构:
README.md
README.en.md
其中:
README.md是中文主版本;README.en.md是英文版本;- 两个文件顶部可以互相切换;
- 两个版本使用相同的状态、安全边界和目录说明。
README 明确区分:
已完成
- 只读导出;
- 本地格式分析;
- 风险分类;
- 资格判断;
- 自动化测试;
- 3、20 和 100 条生产样本验证。
正在进行
- 扩大历史样本;
- 验证格式边界;
- 确定低风险候选。
尚未实现
- AI 摘要生成;
- WordPress 摘要写回;
- 批量文章修改;
- 自动部署写入工具。
这可以避免仓库访问者误以为项目已经能够直接修改 WordPress。
十四、创建公开 GitHub 仓库
完成敏感信息审计后,我确认仓库中不包含:
- API Key;
- Token;
- 私钥;
- 密码;
- 服务器 IP;
- 用户邮箱;
- 数据库配置;
- 真实文章正文;
- 原始 JSONL;
- 本地分析结果。
因此,这次没有继续使用私有仓库,而是创建了公开 GitHub 仓库:
shuijingwan/wordpress-ai-excerpt-backfill
首次提交为:
fa2d766 feat: 增加只读 WordPress 历史文章格式审计管线
仓库当前包含:
.gitignore
README.md
README.en.md
bin/
config/
docs/
src/
tests/
不包含:
data/raw/
data/analysis/
__pycache__/
*.pyc
十五、为什么这一步值得单独记录
从最终结果看,我还没有为任何历史文章生成摘要,也没有写回一条 WordPress 数据。
如果只看“摘要补全”的最终目标,这一阶段似乎没有直接产出。
但实际上,这一步解决了后续自动化中最基础的问题:
- 我知道历史文章有哪些主要格式;
- 我知道哪些格式可以确定性识别;
- 我知道代码区域怎样避免短代码误判;
- 我知道 mixed 应如何基于结构判断;
- 我知道如何受控读取生产数据;
- 我知道导出和分析失败时不会留下伪正式结果;
- 我知道哪些敏感文件不能进入 Git;
- 我已经拥有一套可以持续增加夹具和规则的测试框架。
如果没有这些基础,后续即使 AI 生成的摘要质量很好,整个生产流程仍然不够可信。
十六、下一步计划
接下来仍然不会立即启用批量写回。
更合理的顺序是:
- 继续按每批不超过 100 条扩大只读样本;
- 统计不同年代的编辑器和代码格式分布;
- 找出 unknown、mixed、SyntaxHighlighter 和 damaged 样本;
- 确认低风险摘要候选范围;
- 单独设计 AI 摘要生成流程;
- 对模型质量、处理速度和 API 成本进行比较;
- 最后再设计 WordPress 写回工具。
真正进入写入阶段前,还必须增加:
- 文章级备份;
- dry-run;
- 幂等性;
- 冲突检测;
- 原摘要保护;
- 失败重试;
- 审计日志;
- 回滚能力。
摘要生成和摘要写回应继续作为两个独立阶段,而不是一次执行完成。
十七、总结
这次项目最重要的决定,不是选择了哪个 AI 模型,而是没有急着让 AI 修改 WordPress。
面对两千多篇跨越十多年的历史文章,我先建立了一个只读、限量、可校验、可测试的格式审计管线。
首批 3 条、20 条和 100 条生产样本已经完成验证,143 个自动化测试全部通过,项目也已经以中英文文档形式公开到 GitHub。
当前阶段仍然没有生成摘要,也没有写回生产数据。
但从长期生产流程来看,这并不是绕路,而是在真正开始自动化之前,先把格式边界、数据安全和失败保护弄清楚。
只有先知道哪些文章可以安全处理,后续的 AI 摘要补全才有可能从一次性脚本,逐步变成一套可以长期运行的生产流程。
需要长期技术维护或远程问题排查?
我是拥有 15+ 年经验的 PHP / Go 后端工程师,长期关注已有系统维护、Bug 修复、性能优化、服务器排查、WordPress 网站维护和小功能迭代。
如果你的项目遇到以下情况,可以先从一次小问题排查开始合作:
- ✅ PHP / Laravel / Yii2 老项目无人维护
- ✅ Go / Gin 后端接口需要排查或优化
- ✅ WordPress 网站访问慢、报错或插件冲突
- ✅ Nginx / MySQL / Redis / Linux 服务器异常
- ✅ CDN / Cloudflare / DNS / HTTPS 配置问题
- ✅ 需要长期远程技术支持或兼职维护
更多介绍请查看:关于我 & 合作
微信:13980074657
邮箱:shuijingwanwq@gmail.com
Telegram:@shuijingwan
GitHub:https://github.com/shuijingwan

发表回复