2 min read · 408 words
实用技巧 / 博客运营 / Python · 自动化
约 2,300 字
当运营超过 200 篇博客文章时,即使人工进行内容审核,也难免会有所遗漏。比如 Markdown 残留(加粗直接暴露)、违反表情符号(Emoji)白名单、缺失来源、空表格、残留的盒子样式等。因此,我们专门在文章提交给博客 API 之前,设计了一个自动检查和修正的步骤。
本文将详细介绍这一自动 QC 系统的设计初衷、工作原理、实际效果以及验证方法。我们提炼了最核心的内容,以便面临同样困扰的运营人员只需一页代码就能轻松复刻。
开发初衷
在最初的第一年里,我们经常遇到两类事故。
第一类是模型输出残留。使用 LLM 生成正文时,加粗、## 小标题、--- 等 Markdown 标记经常在未转换为 HTML 的情况下残留下来。在上线后的页面上,星号等符号会直接暴露。
第二类是刚写完时完好无损,但在发布前被某个 hook 搞砸的情况。例如,某个函数在正文中多打开了一个 检查站由两个阶段组成。 第一阶段 sanitize —— 无条件修正 接收 HTML 并统一应用以下规则: 这一阶段是无需人工判断的机械化操作。我们确保无论什么文章,处理后的结果都是一致的。 第二阶段 quality gate —— 未通过则拦截发布 自动检查那些人工一眼就能看出遗漏的问题。如果未通过,将直接拒绝发布。 引入该系统 6 个月后的成果: 被拦截的 38 篇文章并没有丢失。作者只是提前知晓了问题,在润色修改后重新尝试,最终全部正常发布。拦截原因的分布为:缺失来源 41% / 字数不足 26% / 零图片 21% / 其他 12%。 这是我们在建立检查站后进行的验证工作。 黄金数据集(Golden Set)回归测试 —— 我们收集了过去曾发生过事故的 41 篇文章的原稿,做成了黄金数据集。在让它们通过 sanitize + quality gate 时,自动对比 41 篇文章的事故模式是否全部消失。最初通过率为 39/41。针对失败的 2 篇分析后,我们强化了正则表达式,最终将通过率提升至 41/41。 线上抽样检查(Live spot-check) —— 在应用全新 sanitize 的第一周,我们从发布的 18 篇文章中随机抽取了 8 篇,直接抓取(fetch)了线上页面。我们检查了在桌面端 1280px 和移动端 360px 两种宽度下,是否会出现水平滚动条、文字是否超出容器、图片是否破损。结果 8/8 全部正常。 双重过滤幂等性(Double pass idempotency) —— 验证将已经通过一次 sanitize 的输出结果再次进行 sanitize 时,输出是否保持一致。这是为了防止发布钩子链(publish hook chain)重复运行而进行的验证。结果 100/100 完全一致。 与其生搬硬套全部代码,不如根据您自己的环境,借鉴其中最核心的一两个点。 只需在发布前的最后一个环节调用这两个函数即可。如果 总而言之,就是“在发布前设立一个检查站,自动拦截所有事故”。这样一来,过去人工每次审核所花费的时间将被彻底省去。 Category Coverage Notice This article follows our label-specific editorial criteria. Details:
工作原理
width:800px、margin-left:-30px、position:absolute 等) 标签的固定 width/height 属性 → 保持响应式布局X → X,任意 --- → )border、box-shadow、padding>20px 的 max-width:100%、overflow-wrap:anywhere)
少于 3 个 → fail(指南/对比类文章)
实际效果
验证方法
如何动手实现
import re
def sanitize_pre_publish(html: str) -> tuple[str, list[str]]:
fixes = []
# 移除危险的行内宽度
html, n = re.subn(r'width\s*:\s*(?:[4-9]\d{2}|[1-9]\d{3,})px\s*;?', '', html)
if n: fixes.append('strip_wide_width')
# Markdown 残留 → HTML
html, n = re.subn(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', html)
if n: fixes.append('md_bold')
# 清除表情符号(必要时)
html, n = re.subn(r'[\U0001F300-\U0001FAFF]', '', html)
if n: fixes.append('strip_emoji')
return html, fixes
def quality_gate(html: str, post_type: str) -> tuple[bool, list[str]]:
fails = []
text = re.sub(r'<[^>]+>', '', html)
if len(text.replace(' ', '')) < 600: fails.append('too_short')
if html.count('<h2') < 3 and post_type in ('howto', 'compare'): fails.append('few_h2')
if '<img' not in html: fails.append('no_image')
if 'TODO' in html or 'REDACTED' in html: fails.append('placeholder')
return (len(fails) == 0), fails
quality_gate 返回 fail,则拦截发布并将原因反馈给相关人员。而 sanitize 处理后的 HTML 输出,直接传递给发布 API 即可。