← 返回总目录

Day 199 — 技术写作

综合实战 764 行 4 个可运行脚本

Day 199 — 技术写作

阶段:Phase 12 — 综合实战(总复习第 4/5 天)
主题:信息结构 · 可复现实验 · Markdown 静态站 · 把真机实验写成可发表文章
代码:3 个可独立运行的脚本(全部带 --self-test,离线可跑),共 830 行
实测环境:Linux 7.0.0-34-generic / Python 3.12.3 / Intel Xeon Gold 6326 @2.90GHz / 4 核 / load1=5.35 / git 94165cf / 2026-10-29 06:00 CST


今天在 200 天里的位置

Day 196  知识体系全景复习   → 把学过的变成地图        ✅
Day 197  面试准备           → 把地图变成口头能力      ✅
Day 198  开源贡献           → 把能力放进别人的仓库    ✅
Day 199  技术写作           → 把经验变成可传抄的文字  ← 今天
Day 200  持续成长路径       → 把方法固化成长期系统

前 198 天,输出基本是给自己看的:代码能跑、笔记能查,就够。
今天换一个更苛刻的读者模型——一个不在你工位上、不知道你的机器、也不会问你问题的人。

他读你的文章时只有三个问题:

  1. 你说的这件事,凭什么成立?
  2. 换到我的机器上,能不能复现?
  3. 我要多花多少时间才能判断值不值得读?

技术写作的全部技巧,都可以还原成对这三个问题的回答。


概念速览

概念 一句话 今天的落点
读者模型 不在场、无上下文、可随时离开 概念 1.1
信息结构 结论先行,证据分层 概念 1.2
可复现性契约 环境 + 命令 + 样本量 + 统计量 + 输出哈希 概念 1.3 / 代码 02
断言分级 事实 / 推断 / 观点,各自需要不同证据 概念 1.4
代码围栏状态机 围栏内的字符不参与结构解析 原理 2.2
固定成本 解释器启动约 20 ms,小样本里占比巨大 原理 2.4
锚点一致性 TOC 的 href 必须等于标题的 id 原理 2.5
先转义后替换 否则用户内容会变成真标签 原理 2.6
静态站 构建产物即站点,无运行时依赖 概念 1.6

一、概念解释

1.1 技术写作的读者模型:一个不在场的人

写代码时,你和执行环境在同一个闭环里:跑一下就知道对不对。
写作打破了这个闭环——读者执行的是你的文字,不是你的程序。

所以技术写作的所有规范,本质上都在补偿"作者不在场"这件事:

一句话:技术文章是把一次私人实验,改写成别人可执行的指令。

1.2 信息结构:结论先行,证据分层

技术读者不是来听故事的,是来取结论的。所以结构上要满足"三层可退出":

第 1 层  标题 + 首段      → 30 秒判断:这篇和我有关吗
第 2 层  小标题 + 表格     → 3 分钟扫读:结论是什么
第 3 层  实验手册 + 代码   → 30 分钟精读:怎么复现

任何一层都应该是自洽的:只读第 2 层的人,也能拿走可用的结论。
这就是为什么"实验手册"要独立成章,而不是散落在正文叙述里。

1.3 可复现性契约:五个字段缺一不可

"我测过"不是证据,"这些条件下我测到这些数字"才是。最小契约:

字段 回答什么 缺失后果
环境 哪台机器、哪个版本 读者无法迁移结论
命令 到底跑了什么 读者只能猜
样本量 跑了几次、取哪个统计量 单次数字可能是噪声
原始输出 stdout/stderr 原文 无法判断是否被美化
输出指纹 哈希是否稳定 无法判断结果是否随机

02-repro-card.py 就是把这张表变成一条命令:它执行命令 N 次,记录环境指纹、
耗时分布、每次 stdout 的 sha256,并给出"输出是否可复现""计时是否可信"两个判定。

1.4 断言分级:不是所有句子都需要实验

写作最耗时的误区,是给每句话都找证据。实际上句子分三级,证据要求不同:

级别 例子 需要什么
事实 "Python 3.12 的 dict 保持插入序" 出处或一行可跑的验证
推断 "这个写法在热路径上更省" 说明推理链 + 至少一次测量
观点 "我不喜欢这个 API" 说清立场即可,不要伪装成事实

体检器拦不住你写观点,但它能拦住把观点写成事实——这正是"显然"这类词的危害:
它把一个未经证明的判断,伪装成了共识。

1.5 为什么用 Markdown 而不是 Word

不是因为好看,而是因为可计算:

今天的体检器、构建器都建立在"Markdown 是可解析的结构,而非排版格式"这个前提上。

1.6 静态站:把发布成本压到零

技术博客的访问曲线是长尾的:发布当天几十次,之后每月几次。
为这种负载上数据库和框架,是典型的过度工程。

静态站的取舍很清楚:

03-article-build.py 用 380 行做到:front matter 解析、Markdown 子集渲染、
TOC 锚点、阅读时长、首页、RSS、.nojekyll。

1.7 写作的反馈回路

写 → 体检(结构) → 实测(数字) → 发布 → 读者复现 → 修正
      ↑______________________________________________|

体检器和实验卡片只覆盖前两步。真正的质量来自第四步:有人照着你写的跑了一遍。
所以文章里要留下"怎么告诉我它跑不通"的入口(仓库 issue、邮箱),这是写作的一部分,不是客服。


二、原理解释

2.1 为什么"显然"是一种认知负担转移

"显然 X" 的信息量不是 X,而是"我判断 X 不需要解释"。读者收到的是两件事:

  1. 一个未证明的结论;
  2. 一个"你不该问"的暗示。

第二件更贵:它让读者要么接受一个黑箱,要么自己补证明。技术写作里,
任何让读者补证明的句子,都应该被删掉或改写。

改写模板:

2.2 代码围栏状态机:一个 continue 决定工具的死活

Markdown 的 # 既是标题标记,也是 Python/shell 的注释符。所以解析器必须是状态机:

       +--------------+  遇到围栏     +--------------+
       |  普通文本态   | -----------> |   代码块态    |
       |  解析标题/列表 | <----------- |  原样收集     |
       +--------------+  遇到围栏     +--------------+

实现只有六行,但漏掉 if in_fence: continue 就会让代码里的每个 # 注释 变成一级标题:

python
m = FENCE_RE.match(line)
if m:
    if not in_fence:
        in_fence = True
        if not m.group(2):
            add("E003", "error", i, "代码块没有标注语言")
    else:
        in_fence = False
    continue
if in_fence:
    continue   # 这一行是地基

体检器的自测里专门有一条反例:代码块内的 # 这是注释,不是标题 不允许触发 E001。

2.3 为什么数字必须带条件:噪声的三个来源

任何一次计时都混着三种成分:

来源 表现 对策
固定成本 解释器启动、import 报告时说明含不含启动
调度抖动 同机其他进程抢占 重复 N 次取中位数
真实差异 你要测的那部分 用相对差而不是绝对值

今天的实测里,30KB 输入的极差/中位是 22.8%,636KB 只有 12.6%。
如果只跑一次 30KB,完全可能把调度抖动当成"优化生效"。

2.4 实测:固定成本约 17 ms

三档输入的中位数:44.06 / 59.18 / 130.84 ms,字符数 30,483 / 156,684 / 636,684。

第一段明显"不线性",差值就是固定成本:设 t = a + b * chars,
第二段给出 b ≈ 0.155 微秒/字符,代回第一段得 a ≈ 17 ms(含解释器启动与 import)。
这个估算不精确,但它解释了为什么小文件上的百分比噪声更大——分母太小。

2.5 锚点一致性:TOC 和标题必须由同一个函数生成

第一版我用 slugify() 同时生成文件名和标题 id,结果中文标题被转成 post-a15a2f6c:
技术上能用,但读者拿到的 URL 是一串哈希,且自测里 TOC 与标题 id 对不上
(TOC 用原文、id 用哈希)。

修正:拆成两个函数,职责分开。

教训:一个函数服务两种用途,最后一定有一种被牺牲。

2.6 转义顺序:先 escape,再替换标记

行内渲染必须严格按这个顺序:

原文 → html.escape() → 识别行内代码/粗体/链接 → 输出 HTML

反过来(先替换再转义)会把自己的 <code> 标签转义掉;而只转义正文不转义代码块,
则代码里的 <script> 会真的执行。自测里有两行专门验证这件事:

python
check("&lt;script&gt;" in frag, "正文里的 <script> 被转义(不执行)")
check("x = 1 &lt; 2" in frag, "代码块里的 < 被转义")

2.7 阅读时长公式:把假设写出来

weighted = 正文字符数 + 代码行数 * 20,再除以 400 字/分钟。
代码行按 3 秒折算成 20 字,这个系数是拍的——但它写在代码里,读者可以改。

这比"约 5 分钟"这种黑箱数字诚实:读者能算出你凭什么说 5 分钟,也能按自己的速度调整。


三、定义与使用方法(速查)

3.1 三件套

bash
cd days/day-199-技术写作

# 1) 结构体检:单篇 / stdin / JSON / 严格模式
python3 code/01-article-lint.py article/post.md
cat article/post.md | python3 code/01-article-lint.py -
python3 code/01-article-lint.py article/post.md --json
python3 code/01-article-lint.py article/post.md --strict   # warning 也算失败

# 2) 实验卡片:跑 N 次,出可粘贴的 Markdown
python3 code/02-repro-card.py --repeat 5 -- python3 -c "print(sum(range(1000)))"

# 3) 静态站:构建 / 本地预览
python3 code/03-article-build.py article --out dist
python3 code/03-article-build.py article --out dist --serve 8899

三个脚本都支持 --self-test,不需要网络、不需要外部服务。

3.2 体检规则表

代码 级别 检查内容 为什么
E001 error 多个 H1 一篇文章一个主题
E002 error 标题跳级(H1→H3) 破坏目录层级语义
E003 error 代码块无语言 读者失去高亮与复制信心
E004 error 空标题 结构占位符会误导 TOC
E005 error 缺少 H1 无法确定文章标题
W101 warning 段落 > 400 字 移动端阅读负担
W102 warning 模糊断言词 举证责任转移
W103 warning 数字无条件 结论不可迁移
W104 warning 遗留 TODO: 半成品不该发布
W105 warning 图片无 alt 无障碍与弱网
W106 warning 表格单列 分隔符写错
W107 warning 链接无可读文字 屏幕阅读器只读 URL
W108 warning 全文无代码块 技术文章应有可执行内容

评分:100 - 10 * error - 3 * warning,下限 0。一个 error 抵三个 warning,
这是产品判断而不是科学:结构错误直接毁掉可读性。

3.3 实验卡片字段

字段 来源 用途
命令 shlex.join(cmd) 可复制
环境 platform + /proc/cpuinfo + os.getloadavg 可迁移
时间 UTC + git HEAD 可追溯
耗时 min / median / max / 极差比 可判断噪声
判定 输出哈希是否全等 可判断随机性
原始输出 最后一次 stdout/stderr 可核对

3.4 发布前检查(人工部分)

体检器能查大部分机械项,最后一条得自己读:


四、图解

4.1 从实验到文章的流水线

真实实验                写作            机械检查            发布
+----------+       +----------+     +------------+     +----------+
| 跑命令    | ----> | 结论先行  | --> | 结构体检    | --> | 静态站    |
| 抓环境    |       | 证据分层  |     | 误报修复    |     | RSS/锚点  |
| 存哈希    |       | 实验手册  |     | 评分 >= 90  |     | HTTP 200  |
+----------+       +----------+     +------------+     +----------+
     |                                                      |
     +--------- 读者复现失败 -> 回到第一步 ------------------+

4.2 体检器的两遍扫描

第一遍:行级状态机
  line --+-- 围栏? --> 切换 in_fence(内容不解析)
         +-- 标题? --> 记录层级、检查 H1/跳级
         +-- 表格? --> 检查列数
         +-- 其他  --> 累积成段

第二遍:段落级措辞
  段落 --> 剥离行内代码与引号 --> 查模糊词 / 数字条件 / 长度

4.3 锚点与目录的一致性

## 为什么要写
      |
      +-- anchor_id("为什么要写") --> id="为什么要写"
      +-- TOC 条目 href="#为什么要写"
                    ^
              同一个函数,必然一致

更多 Mermaid 图见 diagrams/README.md。


五、实战代码案例

5.1 01-article-lint.py:把写作规范写成可执行规则

245 行,核心是一个 for 循环 + 一个段落缓冲区。真实输出(对故意写坏的稿子):

📝 days/day-199-技术写作/article/draft-with-issues.md
   行数 19 / 字符 370 / 标题 4 / 代码块 1
   评分 65/100   错误 2   警告 5

  ❌ L3    E001  出现第二个 H1:一篇文章只能有一个标题
  ⚠️ L7    W102  模糊断言「显然」:给出证据或删掉
  ⚠️ L7    W102  模糊断言「众所周知」:给出证据或删掉
  ⚠️ L7    W102  模糊断言「大家都知道」:给出证据或删掉
  ❌ L9    E003  代码块没有标注语言(读者失去语法高亮)
  ⚠️ L15   W104  遗留 TODO/TBD:发布前处理掉或明确标注为邀请协作
  ⚠️ L19   W103  性能/数量断言缺少测量条件(样本量、环境、重复次数)

改完之后的终稿:

📝 days/day-199-技术写作/article/post.md
   行数 133 / 字符 2835 / 标题 10 / 代码块 6
   评分 100/100   错误 0   警告 0
   ✅ 没有发现问题

5.2 02-repro-card.py:把"我测过"变成可核对的卡片

205 行。关键设计是"两次判定":输出是否稳定、计时是否可信,分开判断。

python
stable_output = len(hashes) == 1          # 结果可复现
spread = (max(times) - min(times)) / median
noisy_timing = spread > 0.20              # 计时不可信

真实卡片(30KB 输入,5 次):

- 命令:python3 01-article-lint.py /tmp/day199/small.md
- 环境:Python 3.12.3 / Linux 7.0.0-34-generic / x86_64 / 4 核 / load1=5.35
- 时间:2026-10-28T22:01:45Z(UTC),commit 94165cf
- 重复:5 次,取 median
- 耗时:min 40.66 ms · median 44.06 ms · max 50.72 ms(极差/中位 22.8%)
- 判定:✅ 输出可复现;⚠️ 计时噪声大

注意"输出可复现"和"计时噪声大"可以同时成立——这正是它比单次计时诚实的地方。

5.3 03-article-build.py:380 行零依赖静态站

真实构建输出:

  post-a15a2f6c.html  1 分钟  370 字符
  verifiable-tech-writing.html  5 分钟  2664 字符
✅ 构建完成:2 篇 → /tmp/day199/dist/

产物 24KB:文章页、首页、feed.xml、.nojekyll。
本地起服务后 curl 验证:

HTTP=200
id="起因一篇自我感觉良好的稿子"
id="一技术文章的三种失败"
id="11-断言不可验证"
id="12-数字没有条件"
id="13-结构没有入口"

5.4 建议运行顺序

bash
python3 code/01-article-lint.py --self-test
python3 code/02-repro-card.py --self-test
python3 code/03-article-build.py --self-test

python3 code/01-article-lint.py article/draft-with-issues.md   # 感受 65 分
python3 code/01-article-lint.py article/post.md                # 100 分
python3 code/03-article-build.py article --out /tmp/day199/dist

六、实战实验手册

六个实验都在本机真实执行过,输出是本次运行的 stdout。实验之间不依赖彼此残留状态,
每个都写了清理方式。

实验 1:环境指纹(先固定机器,再谈数字)

目的:确认本机环境,后续所有数字都以它为基准
环境:本机 shell,无依赖
准备:无
执行:

bash
python3 -VV
uname -r
grep -m1 'model name' /proc/cpuinfo
nproc
cd ~/code/Learn-Python && git rev-parse --short HEAD

实际结果(2026-10-29 06:00 CST):

Python 3.12.3 (main, Aug 31 2026, 10:18:26) [GCC 13.3.0]
7.0.0-34-generic
model name	: Intel(R) Xeon(R) Gold 6326 CPU @ 2.90GHz
4
94165cf

结论:4 核共享机器,load1=5.35 说明它并不空闲——后面测出 22.8% 的极差就是它的
直接后果。记录 load1 不是形式,它解释了你为什么不能只跑一次。
清理:无。

实验 2:把坏稿子体检一遍

目的:验证规则能抓到真实写作问题,并且评分可复现
准备:仓库内已含 article/draft-with-issues.md(故意写坏:两个 H1、无语言代码块、
模糊断言、无条件数字、TODO、无 alt 图片)
执行:

bash
cd days/day-199-技术写作
python3 code/01-article-lint.py article/draft-with-issues.md; echo "exit=$?"

实际结果:

📝 days/day-199-技术写作/article/draft-with-issues.md
   行数 19 / 字符 370 / 标题 4 / 代码块 1
   评分 65/100   错误 2   警告 5

  ❌ L3    E001  出现第二个 H1:一篇文章只能有一个标题
  ⚠️ L7    W102  模糊断言「显然」:给出证据或删掉
  ⚠️ L7    W102  模糊断言「众所周知」:给出证据或删掉
  ⚠️ L7    W102  模糊断言「大家都知道」:给出证据或删掉
  ❌ L9    E003  代码块没有标注语言(读者失去语法高亮)
  ⚠️ L15   W104  遗留 TODO/TBD:发布前处理掉或明确标注为邀请协作
  ⚠️ L19   W103  性能/数量断言缺少测量条件(样本量、环境、重复次数)
exit=1

结论:两个结构错误被判 error,退出码 1,可以进 CI 卡住发布。同一段落里的三个
模糊词各报一条 warning,没有去重——这是刻意的,因为每个词都要被单独改掉。
清理:无。

实验 3:体检器压测(三档规模 × 5 次)

目的:拿到真实耗时,验证吞吐是否线性、噪声有多大
准备:用仓库内夹具生成三档语料(400 / 2000 / 8000 小节)
执行:

bash
cd days/day-199-技术写作
python3 code/04-make-corpus.py /tmp/day199
for f in small mid big; do
  python3 code/02-repro-card.py --repeat 5 -- python3 code/01-article-lint.py /tmp/day199/$f.md
done

实际结果(逐档的真实卡片,节选关键行):

small.md  400 节  53693 字节
  min 40.66 ms · median 44.06 ms · max 50.72 ms(极差/中位 22.8%)
  判定:✅ 输出可复现;⚠️ 计时噪声大
  stdout sha256 前缀 0344fd047196b83e

mid.md   2000 节  272694 字节
  min 56.75 ms · median 59.18 ms · max 69.46 ms(极差/中位 21.5%)
  判定:✅ 输出可复现;⚠️ 计时噪声大
  stdout sha256 前缀 9f31c928f545fdc5

big.md   8000 节  1100694 字节
  min 116.93 ms · median 130.84 ms · max 133.38 ms(极差/中位 12.6%)
  判定:✅ 输出可复现;✅ 计时稳定
  stdout sha256 前缀 f504041230f1c085

结论:字符数 ×20.9,中位耗时 ×2.97——存在明显固定成本;三次的 stdout 在 5 次
重复里哈希完全一致,说明输出(而不是时间)可复现。小文件极差 22.8% 超过 20% 阈值,
被判"计时噪声大",这正是 load1=5.35 的可测量后果。
清理:rm -rf /tmp/day199(实验 6 统一执行)。

实验 4:体检器初审自己的文章 → 7 条误报 → 修复 → 100 分

目的:验证"引用规则"不等于"违反规则"
执行:

bash
python3 code/01-article-lint.py article/post.md; echo "exit=$?"

实际结果(修复前,第一版规则):

   评分 79/100   错误 0   警告 7
  ⚠️ L1    W102  模糊断言「显然」:给出证据或删掉
  ⚠️ L31   W102  模糊断言「显然」:给出证据或删掉
  ⚠️ L31   W102  模糊断言「众所周知」:给出证据或删掉
  ⚠️ L31   W102  模糊断言「大家都知道」:给出证据或删掉
  ⚠️ L125  W102  模糊断言「显然」:给出证据或删掉
  ⚠️ L125  W102  模糊断言「众所周知」:给出证据或删掉
  ⚠️ L130  W104  遗留 TODO/TBD:发布前处理掉或明确标注为邀请协作
exit=0

7 条全是误报:L1 是 front matter 里的标题(文章就叫《别写「显然」》),L31/L125 是
用引号引用这些词来讨论它们,L130 是检查清单里"没有遗留 TODO"这一条。

修复三处:跳过 front matter;先剥离行内代码与引号片段再查措辞;TODO 规则要求后面跟冒号。
中间态(只剥离了「」引号、漏了 ASCII 双引号)是 85 分、2 条残留。

写这篇 README 时又暴露两条过宽规则:

改成用 ×/倍/快了 作为比较记号,并让 TODO/图片/链接检查同样先剥掉行内代码。

实际结果(修复后):

📝 days/day-199-技术写作/article/post.md
   行数 133 / 字符 2835 / 标题 10 / 代码块 6
   评分 100/100   错误 0   警告 0
   ✅ 没有发现问题

结论:规则的难点不是"能抓到",而是"只抓该抓的"。一条对讨论者开火的规则,
在真实写作里就是噪音源。修复后自测仍 8/8 通过,说明没有放过原有缺陷。
清理:无。

实验 5:构建静态站 + 真实 HTTP 验证

目的:确认发布链路真的可用,而不是"应该能用"
执行:

bash
python3 code/03-article-build.py article --out /tmp/day199/dist
python3 code/03-article-build.py article --out /tmp/day199/dist --serve 8899 &
sleep 2
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8899/verifiable-tech-writing.html
curl -s http://127.0.0.1:8899/verifiable-tech-writing.html | grep -o 'id="[^"]*"' | head -6
curl -s http://127.0.0.1:8899/feed.xml | head -3

实际结果:

  post-a15a2f6c.html  1 分钟  370 字符
  verifiable-tech-writing.html  5 分钟  2664 字符
✅ 构建完成:2 篇 → /tmp/day199/dist/
HTTP=200
id="起因一篇自我感觉良好的稿子"
id="一技术文章的三种失败"
id="11-断言不可验证"
id="12-数字没有条件"
id="13-结构没有入口"
id="二把检查写成代码"
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"><channel><title>技术写作实验室</title>

结论:锚点是中文原文而不是哈希,TOC 与 id 同源;RSS 含两条 item;产物 24KB。
注意构建把 draft-with-issues.md 也一起发布了——真实教训:构建目录里有什么,
站点上就有什么
,草稿要放单独的目录。
清理:停服务并验证端口,见实验 6。

实验 6:收尾卫生检查

目的:确认没有残留进程/端口/临时目录
执行:

bash
ss -ltnp 2>/dev/null | grep 8899 || echo 'port free'
pgrep -af 'serve 8899' || echo 'no server process'
rm -rf /tmp/day199
ls /tmp/day199 2>/dev/null || echo 'tmp clean'

实际结果:

port free
no server process
tmp clean

结论:端口 8899 已释放,临时目录已删。注意不要用 pkill -f 03-article-build.py
收尾——它会把执行这条命令的 shell 一起杀掉(见翻车 4)。
清理:本实验本身就是清理。


翻车记录(真实踩坑)

翻车 1:自测报 W105 失败,我以为是规则坏了

教训:自测失败时,先怀疑测试,再怀疑代码。这次如果直接改规则,会把一条正确规则改坏。

翻车 2:中文标题被 slug 成哈希,TOC 与 id 对不上

翻车 3:跨层引号把脚本改坏两次

教训:一次改动穿过三层以上引用时,不要拼字符串,直接重写整行/整块。

翻车 4:pkill -f 把自己的会话杀了

翻车 5:文章自称 168 行,实际 245 行


七、思考题

  1. 你的读者在什么场景下会关掉页面?如果只能保留文章的三段话,你保留哪三段?
  2. 体检器把"段落 > 400 字"设为警告。如果文章是 API 文档,这条规则还成立吗?该怎么改?
  3. 实验卡片记录 stdout 的 sha256。什么情况下"哈希不同"不代表实验失败?举两个例子。
  4. 为什么 noisy_timing 和 stable_output 必须分开判定?合成一个布尔值会丢什么信息?
  5. 你最近一次"我觉得这样更快"的判断,如果现在补一次 5 次重复的测量,最可能推翻什么?
  6. 静态站放弃了评论和搜索。如果你的文章主要靠讨论传播,这个取舍还合理吗?替代方案是什么?

八、验收与清理

读者在本机的验收命令

bash
cd days/day-199-技术写作

# 四个脚本离线自证(不需要网络,不需要外部服务)
python3 code/01-article-lint.py --self-test
python3 code/02-repro-card.py --self-test
python3 code/03-article-build.py --self-test
python3 code/04-make-corpus.py /tmp/corpus-check --self-test

# 两篇文章的分数应当分别是 65 和 100
python3 code/01-article-lint.py article/draft-with-issues.md
python3 code/01-article-lint.py article/post.md

# 构建并在本地看效果
python3 code/03-article-build.py article --out /tmp/day199-check
# 浏览器打开 /tmp/day199-check/index.html,或用 --serve 8899

本次实验清理了什么


九、参考资料