没有不值得去解决的问题,也没有不值得去学习的技术!

在用 AI 批量补全 WordPress 历史文章摘要之前,我先建立了一套只读格式审计管线

图3:100 篇历史文章的编辑器格式、代码格式和风险等级统计

旷野笔记:我的多线并行日志

极简远程求职看板:PHP / Go 双栈后端的 10 个网站 + 5 个 TG 频道 + 2 个发现源

(1) 极简远程求职看板:PHP / Go 双栈后端的 10 个网站 + 5 个 TG 频道 + 2 个发现源

39岁,不再硬扛独自求职:我决定加入电鸭远程人才库

(2) 39岁,不再硬扛独自求职:我决定加入电鸭远程人才库

让我意外的是。 第二天管理员就回复了。

(3) 发现电鸭简历无法修改技能标签?我的反馈第二天就被修复了

我的远程求职流程(V2)

(4) 我的远程求职流程(V2)

【图1:本地文件夹中按日期整理的工作机会目录】

(5) 从文件夹到自部署在线表格:我如何管理全职、兼职与项目机会

图3:100 篇历史文章的编辑器格式、代码格式和风险等级统计

(6) 在用 AI 批量补全 WordPress 历史文章摘要之前,我先建立了一套只读格式审计管线

我的 WordPress 博客已经积累了两千多篇中文文章,其中相当一部分历史文章没有填写摘要。

从表面上看,这个需求似乎并不复杂:查询所有没有摘要的文章,把标题和正文交给 AI,再将生成的摘要写回 WordPress。

但真正开始规划之后,我很快意识到,问题并不只是“选哪个模型生成摘要”。

这些文章跨越了十多年,使用过不同编辑器、代码高亮插件和内容结构。部分文章后来又经过 Gutenberg 转换、主题迁移或代码区块替换。如果在没有盘点格式的情况下,直接批量读取正文、调用 AI 并写回数据库,很难准确判断哪些文章可以自动处理,哪些文章需要人工检查。

因此,我没有立即开始批量生成摘要,而是先建立了一套只读的历史文章格式审计管线。

这套管线当前的任务不是生成摘要,也不会写回 WordPress。它只负责:

  • 从生产环境受控导出文章;
  • 识别历史文章的编辑器和代码格式;
  • 检测可能损坏或不确定的结构;
  • 对文章进行确定性的风险分类;
  • 为未来的 AI 摘要补全建立安全候选范围。
图1:wordpress-ai-excerpt-backfill 项目的中英文 README 与目录结构
图1:wordpress-ai-excerpt-backfill 项目的中英文 README 与目录结构

一、为什么不能直接批量生成摘要

我的历史文章并不是由一种统一格式构成的。

其中既有早期 Classic Editor 生成的普通 HTML,也有后来使用 Gutenberg 编辑或转换的文章。代码展示方式也经历过多次变化,包括:

  • 普通 <pre><code>
  • SyntaxHighlighter 短代码;
  • SyntaxHighlighter Gutenberg 区块;
  • Code Block Pro;
  • Gutenberg 内置代码区块;
  • 编辑器格式混合的文章;
  • 未知或已经停用的短代码。

有些文章虽然发布于 2014 年,但在 2026 年重新编辑过,正文中同时包含 Gutenberg 区块和没有转换的经典段落。

如果只根据文章发布时间判断格式,显然不可靠。

另外,代码内容中经常包含方括号、HTML 标签、正则表达式和模板语法。例如:

Plaintext
[if]
[endif]
[name]
[unknown-widget id="123"]

这些内容可能是代码示例,也可能是真正的 WordPress 短代码。检测器如果处理不当,就会把正常代码误判为未知短代码或损坏结构。

因此,摘要补全之前至少需要回答几个问题:

  1. 文章究竟属于 Gutenberg、Classic Editor,还是 mixed?
  2. 是否包含 SyntaxHighlighter、Code Block Pro 或经典代码结构?
  3. Gutenberg 区块是否完整配对?
  4. 短代码是否存在未闭合、孤立结束或错位嵌套?
  5. 代码区域中的方括号内容是否会被误判?
  6. 哪些文章可以自动处理,哪些必须排除或人工复核?

如果这些问题都没有答案,直接写回生产数据并不稳妥。

二、先把项目范围限制为“只读审计”

我为这个项目建立了一个独立目录:

Plaintext
/home/wangqiang/code/wordpress-ai-excerpt-backfill

生产环境中的工具则部署在 Web 根目录之外:

Plaintext
/root/tools/wordpress-ai-excerpt-backfill

WordPress 仍位于:

Plaintext
/data/wwwroot/www.shuijingwanwq.com

项目当前明确不包含以下能力:

  • AI 摘要生成;
  • WordPress 摘要写回;
  • 批量修改文章;
  • 修改分类、标签或其他文章字段;
  • 数据库写操作;
  • 缓存清理;
  • 翻译生成;
  • AI API 调用。

也就是说,即使检测器存在误判,目前最多只会影响本地分析结果,不会直接修改生产文章。

这是我为后续自动化设置的第一层安全边界。

三、生产导出器只负责读取

生产端使用 PHP 导出器读取 WordPress 数据。

导出器只处理满足条件的文章:

  • post_type=post
  • post_status=publish
  • Polylang 语言为中文;
  • 按文章 ID 递增读取。

导出结果采用 JSONL,每行对应一篇文章。字段中包含后续校验需要的信息,例如:

Plaintext
post_id
post_type
post_status
language
published_at
modified_at
title
excerpt
content
permalink
content_sha256

其中,content_sha256 用于确认正文在导出、下载和分析过程中没有发生变化。

导出器没有 WordPress 写入函数,也没有 $wpdb->insert()$wpdb->update()UPDATEDELETE 等 SQL 操作。

正式运行入口要求显式提供导出数量:

Bash
bin/run-readonly-export.sh \
  --limit 100 \
  --after-id 95 \
  --batch-size 100

目前单次导出数量被限制为:

Plaintext
1~100

最初,这个上限只存在于 Shell 运行脚本中。提交前审计时,我发现如果有人绕过 Shell,直接调用 PHP 导出器,就可能传入更大的数量。

因此,后来又在 PHP 导出器内部增加了独立硬上限:

PHP
const SWQ_MAX_EXPORT_LIMIT = 100;

这样,无论从哪个入口调用,超过 100 条都会在接触 Polylang 和数据库查询之前被拒绝。

这是一项典型的纵深防护:不能只依赖最外层入口正确。

四、部署和导出必须是两件独立的事

生产部署脚本和导出脚本被明确分开。

部署脚本默认不会执行任何网络操作:

Bash
bin/deploy-to-production.sh --dry-run

只有显式指定:

Bash
bin/deploy-to-production.sh --deploy

才会连接生产服务器。

部署过程包括:

  1. 将 PHP 文件上传为临时文件;
  2. 在服务器上运行 PHP lint;
  3. 核对文件哈希;
  4. 设置严格权限;
  5. 使用原子重命名替换正式文件。

部署不会自动运行导出。

同样,导出脚本只运行已经部署好的固定 PHP 文件,不会顺便上传或替换生产代码。

这样做虽然多了一步操作,但可以避免“部署完成后意外立即处理生产数据”这种耦合风险。

五、导出结果先校验,再变成正式文件

远程导出不会直接写入最终文件名。

运行脚本先生成临时 JSONL,然后使用固定 Python 解释器检查:

  • 文件是否存在;
  • 每一行是否为有效 JSON;
  • 记录数是否与预期一致;
  • 必需字段是否完整;
  • 是否存在重复文章 ID;
  • 语言和发布状态是否符合要求。

全部检查通过后,临时文件才会原子重命名为正式 JSONL。

如果导出或验证失败,临时文件会被清理,不会留下一个看起来正常、实际上只有部分记录的正式结果。

第一次测试导出了 3 篇文章。随后扩大到 20 篇,最后又完成了一批 100 篇的受控导出。

图2:生产环境完成 100 篇中文已发布文章的只读导出
图2:生产环境完成 100 篇中文已发布文章的只读导出

100 篇导出文件的结果为:

Plaintext
记录数:100
文件大小:558106 字节

服务器文件和本地下载文件的 SHA-256 完全一致,说明传输过程中没有发生改变。

六、本地分析器继续保持输入只读

生产 JSONL 下载到本地后,再由:

Plaintext
bin/analyze-export.py

进行分析。

分析器要求显式提供预期记录数:

Bash
bin/analyze-export.py \
  --expected-count 100 \
  data/raw/example.jsonl \
  data/analysis/example.analysis.jsonl

--expected-count 的允许范围同样是:

Plaintext
1~100

分析开始前会验证:

  • JSONL schema;
  • 精确记录数量;
  • 文章类型;
  • 发布状态;
  • Polylang 语言;
  • 重复文章 ID;
  • 正文 SHA-256。

正式分析结果也不是直接覆盖写入,而是先写临时文件,执行 flushfsync 后再原子替换。

提交前审计时,又发现了一个容易忽视的问题:如果用户把输入文件和输出文件写成同一个路径,分析完成后,原始导出文件可能会被分析结果替换。

例如:

Bash
bin/analyze-export.py \
  --expected-count 100 \
  data/raw/example.jsonl \
  data/raw/example.jsonl

后来我为分析器增加了同文件保护,覆盖以下情况:

  • 输入和输出路径字符串完全相同;
  • ./ 形成的等价路径;
  • .. 形成的等价路径;
  • 输出是输入的符号链接;
  • 输出与输入是同一 inode 的硬链接。

只要输入和输出最终指向同一个文件,分析器就会在读取输入和创建临时文件之前拒绝执行。

七、分析结果不包含正文等敏感字段

原始 JSONL 中包含正文、标题、摘要和 URL,因此被视为本地敏感数据。

分析结果只保留格式分类和风险判断所需的脱敏字段,例如:

Plaintext
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 明确排除了:

Plaintext
data/raw/
data/analysis/
__pycache__/
*.pyc

因此,公开 GitHub 仓库不会包含生产文章正文、原始导出文件或分析结果。

八、100 篇真实样本暴露出的历史格式差异

完成 100 篇样本分析后,得到的编辑器格式分布为:

Plaintext
classic:98
mixed:2

代码格式分布为:

Plaintext
none:95
classic-pre-code:4
syntaxhighlighter:1

风险等级为:

Plaintext
low:91
medium:7
high:2
manual-review:0

这一批文章的发布时间大致覆盖 2013 年至 2016 年。

结果说明,早期文章主体仍然是 Classic Editor 格式,但也已经出现:

  • 经典 <pre>/<code> 代码结构;
  • SyntaxHighlighter;
  • 后期编辑形成的 Gutenberg 与经典内容混合;
  • 多图片文章;
  • 代码中容易被误判为短代码的方括号内容。
图3:100 篇历史文章的编辑器格式、代码格式和风险等级统计
图3:100 篇历史文章的编辑器格式、代码格式和风险等级统计

九、真实样本帮助发现了多个检测误判

这次工作中,真正耗费时间的并不是导出 100 篇文章,而是逐步修正检测规则。

1. 普通方括号单词被误判为短代码

历史文章和代码示例中可能出现:

Plaintext
[length]
[method]

最初,检测器可能把这类未知方括号单词当作未闭合短代码。

后来规则被收紧:未知裸方括号单词必须具备更强证据,例如配对结构、属性赋值或显式自闭合,才会被当作短代码。

2. caption 没有被识别为已知配对短代码

WordPress 历史图片说明常使用:

Plaintext
[caption]...[/caption]

caption 加入已知和配对短代码后,历史图片结构才能得到正确分类。

3. 代码区域中的方括号被误判

最初的短代码检测器会扫描整个正文。

这意味着以下区域中的方括号文本,也可能被当作 WordPress 短代码:

  • SyntaxHighlighter Gutenberg 区块;
  • Code Block Pro;
  • <pre>
  • <code>
  • ...
    内部。

后来增加了代码区域保护:

  1. 计算受保护代码区间;
  2. 合并重叠区间;
  3. 普通短代码匹配如果落入代码区域就忽略;
  4. 外层开始和结束标记仍参与严格配对检测。

这样既避免了代码内容误判,又保留了未闭合、孤立结束和错位嵌套检测。

4. Gutenberg 区块外的空结构被误判为 mixed

最初,只要 Gutenberg 区块外还剩任何 HTML 标签,就会触发:

Plaintext
CLASSIC_SUBSTANTIAL_OUTSIDE_BLOCKS

这会把以下无语义残留也判为 mixed:

HTML
<p></p>
<p> </p>
<p><br></p>
<div><span></span></div>

后来增加了基于 HTMLParser 的结构判断。

以下内容可以忽略:

  • 普通空白和换行;
  • &nbsp;
  • &#160;
  • &#xA0;
  • 指定零宽字符;
  • 成对的空 pdivspansection
  • <br><br/>

但只要存在一个真实汉字、字母、数字或标点,仍然判为 mixed。

媒体、链接、功能性标签、未知标签和损坏 HTML,也继续保守判为实质内容。

我没有使用“少于 50 个字符就忽略”这类固定阈值,因为短文本也可能是一条重要提示、警告或命令说明。

5. 非 void 标签的自闭合写法

审计中还发现:

HTML
<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 导出上限一致;
  • 第一阶段资格判断。

完整测试命令为:

Bash
PYTHONDONTWRITEBYTECODE=1 \
python3 -m unittest discover -s tests -v

首次提交前,143 个测试全部通过。

图4:143 个自动化测试全部通过
图4:143 个自动化测试全部通过

十三、为项目增加中英文 README

这个项目最终采用了中文优先的双语文档结构:

Plaintext
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

首次提交为:

Plaintext
fa2d766 feat: 增加只读 WordPress 历史文章格式审计管线

仓库当前包含:

Plaintext
.gitignore
README.md
README.en.md
bin/
config/
docs/
src/
tests/

不包含:

Plaintext
data/raw/
data/analysis/
__pycache__/
*.pyc

十五、为什么这一步值得单独记录

从最终结果看,我还没有为任何历史文章生成摘要,也没有写回一条 WordPress 数据。

如果只看“摘要补全”的最终目标,这一阶段似乎没有直接产出。

但实际上,这一步解决了后续自动化中最基础的问题:

  • 我知道历史文章有哪些主要格式;
  • 我知道哪些格式可以确定性识别;
  • 我知道代码区域怎样避免短代码误判;
  • 我知道 mixed 应如何基于结构判断;
  • 我知道如何受控读取生产数据;
  • 我知道导出和分析失败时不会留下伪正式结果;
  • 我知道哪些敏感文件不能进入 Git;
  • 我已经拥有一套可以持续增加夹具和规则的测试框架。

如果没有这些基础,后续即使 AI 生成的摘要质量很好,整个生产流程仍然不够可信。

十六、下一步计划

接下来仍然不会立即启用批量写回。

更合理的顺序是:

  1. 继续按每批不超过 100 条扩大只读样本;
  2. 统计不同年代的编辑器和代码格式分布;
  3. 找出 unknown、mixed、SyntaxHighlighter 和 damaged 样本;
  4. 确认低风险摘要候选范围;
  5. 单独设计 AI 摘要生成流程;
  6. 对模型质量、处理速度和 API 成本进行比较;
  7. 最后再设计 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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理