🤖 第三章 应用实战:公众号创作智能助手
📄 一句话概括

公众号的正文编辑器既看不懂 Markdown,也吃不下网页 HTML——它只认一小片【内联样式的 HTML】。本节要做的事,就是把 3.4.2 拿到的 Markdown 稿,转换到这一小片范围里,并且用一段审计代码保证转换结果一定落在范围内。

📝 本节和前面几节的关系

这是本章唯一一节纯技术、且不依赖任何平台的内容。3.4.2 拿到了 body_md,本节把它变成 body_html(3.2 字段表里的两列)。这一节也是本章相对第二章最大的增量——第二章的 RPA 只搬字段,本章的 RPA 还要搬格式。

一、先搞清楚【公众号到底吃什么】

三样东西放一起对比,一眼就明白问题在哪:

你手上的东西 公众号编辑器怎么看它
Markdown(## 标题、**加粗**) 完全不认识,星号和井号会原样显示
网页 HTML(带 <style>、class="lead") 认识一部分,但会洗掉另一部分——样式表被丢掉,class 被丢掉
内联样式 HTML(<p style="font-size:15px">) 认识,且能保留——这是唯一能稳定存活的形式

所以本节的目标非常明确:把 Markdown 转换成【内联样式 HTML】。

二、公众号的清洗规则:六条铁律

下面六条是排版自动化的全部约束。每一条都对应一类【粘贴过去就完了】的事故:

# 铁律 违反的后果
1 不能有 <style> 标签 整段样式表被丢弃,全文变成无格式裸文本
2 不能有 class / id 属性 属性被剥掉 → 依赖它的样式全部失效
3 样式必须写在元素的 style 里 否则不生效(写在父容器上也常常不生效,因为父容器可能被重写)
4 不能用 <h1> 到 <h6> 会被转成 <p> 或直接删掉 → 样式白写。标题要用 <p> + 内联样式模拟
5 不支持伪元素、动画、定位、外部资源、脚本 :before/:after/animation/position/transform/外链 CSS 字体,全部无效
6 正文里的图片必须来自微信素材库 外链图片发布后会变成裂图(不是不显示,是发布后才坏)

能稳定使用的 CSS 属性其实不多,这一小片就够了:

font-size  line-height  letter-spacing  color  background-color
font-weight  font-family  text-align  margin  padding
border  border-left  border-bottom  border-radius  box-shadow
📝 记住一句话就够了

【能用单色、边框和留白表达的时候,就不要用渐变、阴影和复杂布局。】 因为那些复杂效果恰恰是最容易被清洗掉的,而且清洗是【悄悄发生】的——你在这边看着好好的,粘过去就没了。

三、一个关键机制:为什么【内联样式】能活下来

这一条很多人不理解,但它决定了 RPA 该怎么做:

公众号后台接受的不是【HTML 源码】,而是【剪贴板里的富文本】。

粘贴方式 结果
直接 Ctrl + V(剪贴板里带 HTML 格式) 内联样式保留,排版完整
把 HTML 源码当纯文本粘进去 标签原样显示,或者干脆被清掉

这就是为什么那些 Markdown 排版工具能用——它们做的事不是【给你一段 HTML 代码让你贴】,而是把渲染好的富文本放进剪贴板,你按一下粘贴,样式就带过来了。

⚠️ 这条机制对 RPA 的直接影响

RPA 要做的不是【往输入框里打字】,而是: 1. 把转换好的 HTML 放进剪贴板的【HTML 格式】槽位(不是纯文本槽位); 2. 在编辑器里执行一次粘贴(Ctrl + V); 3. 回读一次,确认样式真的进来了。

如果只是把 HTML 字符串当文本输进去,你会看到满屏的尖括号。 这是本节最实操、也最容易踩的一个坑。

四、转换规则表

把 Markdown 的每种元素,映射到一个带内联样式的 <p>:

Markdown 转换后 说明
## 大标题 <p style="字号19px;居中;加粗"> 只用于章内大标题(见下节的双标题坑)
### 小标题 <p style="字号16px;加粗;左边框4px"> 用左边框做视觉层级,比字号差更醒目
普通段落 <p style="15px;行高1.8;色#3f3f3f"> 主体
**加粗** <strong style="加粗;色#000"> <strong> 也必须带内联样式,否则被当默认样式
> 引用 <p style="浅底+左边框3px"> 用背景色和边框模拟引用块
- 列表项 <p>· 内容</p> 不用 <ul><li>——列表样式在微信里最容易错乱
1. 列表项 <p>1、内容</p> 数字自己拼进文本,可控
--- <p style="居中;浅灰;字间距4px">✦ ✦ ✦</p> 用符号做分隔,比 <hr> 稳
<table> 不要转,改成截图 表格样式在微信里最不稳定

排版参数(一套可以直接用的推荐值):

参数 推荐值 为什么
正文字号 15px 移动端阅读的舒适区(15~16px)
行高 1.8 公众号以手机阅读为主,行高比屏幕宽裕
字间距 0.5px 一点点空隙,读起来不挤
段间距 16px 段与段之间留白,替代首行缩进
段首缩进 不用 公众号的阅读习惯是不缩进
正文颜色 #3f3f3f 深灰,比纯黑柔和
小标题 16px 加粗 + 左边框 层级靠边框,不靠花哨字体

五、动手 Demo:写一个转换器,再用审计器验收

下面这段代码只用 Python 标准库(不需要装任何东西)。它做两件事:转,以及验。

# -*- coding: utf-8 -*-
"""3.4.3 Demo:Markdown → 公众号可用的内联样式 HTML(纯标准库)"""
import re
import html

# ---- 所有样式都内联写死,不用 class / <style> ----
S = {
    "p":      "font-size:15px;line-height:1.8;letter-spacing:0.5px;color:#3f3f3f;margin:0 0 16px 0;padding:0;",
    "h2":     "font-size:19px;line-height:1.6;font-weight:bold;color:#222222;margin:28px 0 16px 0;text-align:center;",
    "h3":     "font-size:16px;line-height:1.6;font-weight:bold;color:#222222;margin:24px 0 12px 0;border-left:4px solid #d0a848;padding-left:10px;",
    "strong": "font-weight:bold;color:#000000;",
    "quote":  "font-size:14px;line-height:1.8;color:#888888;margin:0 0 16px 0;border-left:3px solid #dddddd;padding:6px 0 6px 12px;background-color:#f7f7f7;",
    "li":     "font-size:15px;line-height:1.8;letter-spacing:0.5px;color:#3f3f3f;margin:0 0 8px 0;padding-left:8px;",
    "hr":     "text-align:center;color:#c9c9c9;font-size:13px;margin:24px 0;letter-spacing:4px;",
}


def inline(t):
    t = html.escape(t, quote=False)
    return re.sub(r"\*\*(.+?)\*\*",
                  lambda m: '<strong style="%s">%s</strong>' % (S["strong"], m.group(1)), t)


def convert(md):
    out = []
    for raw in md.split("\n"):
        ln = raw.rstrip()
        if not ln.strip():
            continue
        if re.match(r"^#{2,}\s+", ln):                      # ## / ### 一律转成 p
            level = len(ln) - len(ln.lstrip("#"))
            key = "h2" if level <= 2 else "h3"
            out.append('<p style="%s">%s</p>' % (S[key], inline(ln.lstrip("#").strip())))
        elif ln.startswith("> "):
            out.append('<p style="%s">%s</p>' % (S["quote"], inline(ln[2:].strip())))
        elif ln.strip() in ("---", "***", "___"):
            out.append('<p style="%s">&#10022; &#10022; &#10022;</p>' % S["hr"])
        elif re.match(r"^[-*]\s+", ln):
            out.append('<p style="%s">· %s</p>' % (S["li"], inline(re.sub(r"^[-*]\s+", "", ln))))
        elif re.match(r"^\d+\.\s+", ln):
            n = re.match(r"^(\d+)\.", ln).group(1)
            out.append('<p style="%s">%s、%s</p>' % (S["li"], n, inline(re.sub(r"^\d+\.\s+", "", ln))))
        else:
            out.append('<p style="%s">%s</p>' % (S["p"], inline(ln.strip())))
    return "\n".join(out)


def audit(h):
    """交付前自检:微信会剥掉/改坏的东西,一个都不许出现"""
    bad, low = [], h.lower()
    if "<style" in low:
        bad.append("出现 <style> 标签")
    if re.search(r"\sclass\s*=", low):
        bad.append("出现 class 属性")
    if re.search(r"\sid\s*=", low):
        bad.append("出现 id 属性")
    for t in ("h1", "h2", "h3", "h4", "h5", "h6", "div", "span", "table"):
        if "<" + t in low:
            bad.append("出现 <%s> 标签" % t)
    for prop in ("position:", "transform:", "animation:", "float:"):
        if prop in low:
            bad.append("出现微信不支持的 CSS:%s" % prop)
    for m in re.finditer(r"<p(\s[^>]*)?>", h):
        if "style=" not in (m.group(1) or ""):
            bad.append("有一个 <p> 没有内联样式")
    return bad

实际运行输出(真跑,不是示意):

================================================================
转换结果(前 6 个元素)
================================================================
<p style="font-size:19px;line-height:1.6;font-weight:bold;color:#222222;margin:28px 0 16px 0;text-align:center;">玄关收纳:我家最后只留下三件东西</p>
<p style="font-size:15px;line-height:1.8;letter-spacing:0.5px;color:#3f3f3f;margin:0 0 16px 0;padding:0;">玄关永远堆着鞋和快递盒,出门前翻三分钟找不到钥匙。</p>
<p style="font-size:16px;line-height:1.6;font-weight:bold;color:#222222;margin:24px 0 12px 0;border-left:4px solid #d0a848;padding-left:10px;">一、先做减法,再谈收纳</p>
<p style="font-size:15px;line-height:1.8;letter-spacing:0.5px;color:#3f3f3f;margin:0 0 16px 0;padding:0;"><strong style="font-weight:bold;color:#000000;">第一步不是买收纳盒,是扔东西。</strong> 我把不穿的鞋和攒了没用的袋子清掉,剩下的空间比想象的大。</p>
<p style="font-size:14px;line-height:1.8;color:#888888;margin:0 0 16px 0;border-left:3px solid #dddddd;padding:6px 0 6px 12px;background-color:#f7f7f7;">收纳的第一原则:先减后加,不是先加后减。</p>
<p style="font-size:16px;line-height:1.6;font-weight:bold;color:#222222;margin:24px 0 12px 0;border-left:4px solid #d0a848;padding-left:10px;">二、三件东西够用了</p>
...
总元素数 = 12,总字符数 = 1818

================================================================
交付前审计
================================================================
  [OK] 无 <style> / 无 class / 无 id / 无 h1-h6 / 无 div / 无 table
  [OK] 无 position/transform/animation/float
  [OK] 每个 <p> 都带内联样式

================================================================
反面教材对照(同一个审计器跑一段【错的】HTML)
================================================================
  [X] 出现 <style> 标签
  [X] 出现 class 属性
  [X] 出现 <h2> 标签
  [X] 出现 <div> 标签
  [X] 有一个 <p> 没有内联样式

读结果,四个值得注意的地方:

  1. 转换器输出的每一行都是【一个 <p> 带一长串内联样式】——这就是 3.2 里说的【给公众号读的稿】。它不好看、不好读,但它不会坏;
  2. <strong> 也带了内联样式——因为默认的 <strong> 样式也可能被平台的样式覆盖,写上更稳;
  3. 审计器的三段输出分别对应三种状态:全绿(可以交付)、抓到问题(必须改)、以及反面教材里五条全中——注意那五条恰好就是第二节铁律 1~4 的违反。审计器不是装饰,它把【六条铁律】变成了可执行的检查;
  4. <h2> 也在被禁的名单里——这一点容易被忽略。公众号里用 <p> 模拟标题,不要用真正的标题标签(铁律 4)。

六、四个必须知道的坑

坑一:双标题

现象:发布后标题出现两次(后台标题栏一个、正文开头一个)。

原因:Markdown 稿里的 ## 标题 被一起转进了正文。公众号的标题是独立字段,正文里不该再有它。

解法:3.4.2 的解析器已经把第一个标题行【从正文里拿走了】——这就是那一节为什么要专门删掉标题行。本节 Demo 里保留 ## 行 只是为了演示【它会被转成 <p> 而不是 <h2>】,实际流程里它不会走到这里。

坑二:表格

现象:表格粘过去后所有单元格挤成一坨,或者干脆消失。

原因:微信对 <table> 的支持很不稳定。

解法:表格改成图片(截图或生成图片再上传素材库)。这是唯一可靠的做法,也是很多公众号实际在用的做法。

坑三:正文图片的【延迟爆炸】

现象:在编辑器里预览一切正常,点发布后图片全变裂图。

原因:外链图片在发布时才被替换/校验,微信只认自己图床的地址(mmbiz.qpic.cn 域名)。

解法:正文里每张图都要先上传到微信素材库,把地址换成素材库返回的地址。注意【预览正常】不能作为通过标准——这是本节最阴险的一个坑。

坑四:代码块

现象:技术类文章里的代码粘过去后字体错乱、缩进丢失、甚至被当成普通段落。

原因:微信支持代码块,但语法高亮依赖内联样式,写不好就是一团乱。

解法:代码块的每一行都要单独包一个带内联样式的 <p>(用等宽字体 + 浅底),不要指望 <pre> 自动处理好。如果是长代码,截图反而更省事。

七、⚙️ 2026 现状:为什么【自己写转换器】而不是用现成工具

2026 年做 Markdown 转公众号 HTML 的工具已经很多:

类型 代表 适合谁
在线编辑器 Markdown Nice(mdnice)、各类公众号编辑器 手工写作的人——打开网页、写完复制、粘贴
命令行/脚本工具 各类开源转换器 开发者
本地技能/插件 各类编辑器插件 写在自己的笔记软件里的人

本章为什么还要自己写一个:因为这里的转换是【流水线里的一个环节】,不是【一个人坐在电脑前排版】。

需求 现成工具 自己写
能被 RPA 调用 多数不行(要人手点) 可以
输出能被程序接住 多数只给剪贴板 返回字符串,可直接入库
样式按品牌定制 受主题限制 完全可控
加审计、加字段级校验 没有 可以(本节的审查函数)
出问题时能改 只能等作者更新 自己就能改
📝 一句话概括这条取舍

工具服务【人】,代码服务【流程】。 本章的排版是流程里的一个工序,它需要一个能被程序调用的、输出确定的东西——所以自己写。而如果你只是偶尔手工发一篇文章,用 mdnice 就够了,不必写代码。 选工具还是选代码,看的是【谁来调用它】,不是【哪个更高级】。

30 秒实验

把本节 Demo 里 S["p"] 的那串样式,只改一个数字:把 font-size:15px 改成 font-size:15(去掉单位)。

会发生什么:

结论:审计器能挡住【结构性错误】(少了 style、多了 class),挡不住【语义性错误】(单位写错、颜色写错)。 后者只能靠人眼看一次预览来兜底。

这也是本节和 3.4.2 的一条共同纪律:机器负责【可判定的部分】,人负责【要判断的部分】。

常见误区

  1. 【把 HTML 源码当文本打进编辑器就行】——错。 公众号要的是剪贴板里的富文本,不是 HTML 源码。必须把 HTML 放进剪贴板的【HTML 格式】槽位再粘贴,否则你会看到满屏尖括号。
  2. 【用 <h2> 写小标题最标准】——错。 <h1> 到 <h6> 会被微信转成 <p> 或删掉,样式白写。标题一律用 <p> + 内联样式模拟。
  3. 【编辑器里预览正常就说明没问题】——错。 正文外链图片是在发布那一刻才爆的。预览正常不能作为通过标准——图片必须先上传到素材库。
  4. 【表格直接转过去就行】——错。 微信对 <table> 支持很不稳定。表格改成图片是唯一可靠的做法。
  5. 【有审计器就不用看预览了】——错。 审计器挡【结构性错误】,挡不住【语义性错误】(单位写错、颜色写错、字号不舒服)。机器管可判定的,人管要判断的。
自测三问(点开对答案) 1. **公众号的输出格式为什么只能是【内联样式 HTML】,Markdown 和网页 HTML 都不行?** **Markdown 它完全不认识**(井号和星号会原样显示);**带 `