问题
最近博客和公众号两边同时更新,内容都是从 Markdown 写的。博客那边 WordPress 对 Markdown 支持还行,但公众号这边就麻烦了——Markdown 的标题、代码块、链接、图片路径都得手动转成微信富文本格式。每次发一篇 1500 字的文章,光排版调整就要 20 分钟。
手动排版出了几次问题:代码块换行丢了、图片链接写死本地路径、<strong> 和 ** 混用。上周连着两天发出去的文章代码块格式都是乱的,读者留言说”代码糊成一团”——丢人啊。
决定写个脚本自动化这个流程。

方案
需求很简单:
- 读 Markdown 文件
- 把
#/##/###转成对应字号的<p>段落(公众号不支持 h1~h6) - 代码块用
<pre>包<code>,保留缩进和换行 - 图片引用替换成已上传的 CDN URL
- 链接
<a>加上target="_blank" - 列表
<ul><li>正常处理 - 输出纯 HTML 片段,可以直接粘贴到公众号编辑器
用了 Python 的 markdown 库做基础解析,然后自定义扩展处理公众号的特殊需求:
import markdown
from markdown.extensions import codehilite, fenced_code
import re
class WechatExtension(markdown.Extension):
def extendMarkdown(self, md):
md.postprocessors.register(
HeaderToPara(md), 'header_to_para', 175)
class HeaderToPara(markdown.postprocessors.Postprocessor):
def run(self, text):
# h1: 18px bold center
text = re.sub(r'<h1>(.*?)</h1>',
r'<p style="font-size:18px;font-weight:bold;
text-align:center;">\1</p>', text)
# h2: 16px bold with color
text = re.sub(r'<h2>(.*?)</h2>',
r'<p style="font-size:16px;font-weight:bold;
color:#2c3e50;">\1</p>', text)
# h3: 15px bold
text = re.sub(r'<h3>(.*?)</h3>',
r'<p style="font-size:15px;font-weight:bold;">
\1</p>', text)
return text
图片替换的逻辑最麻烦。Markdown 里的路径是相对路径,但公众号需要图床 URL。脚本里建了个映射表:
IMG_MAP = {
'./img/cover.png': 'https://cdn.example.com/cover.png',
'./img/diagram.png': 'https://cdn.example.com/diagram.png',
}
class ImageRewriter(markdown.postprocessors.Postprocessor):
def run(self, text):
for local, remote in IMG_MAP.items():
text = text.replace(f'src="{local}"', f'src="{remote}"')
return text
完整脚本大概 120 行,核心就三个处理器:标题转换、图片重写、外链加 target。用法:
python3 md2wechat.py article.md --img-map img_map.json > output.html
输出的 HTML 直接粘到公众号编辑器里,格式完全对。

踩坑
搞这个脚本踩了 3 个坑。
1. markdown 库默认不启用的扩展
想当然以为 markdown.markdown(text) 就能处理代码块,结果 <pre><code> 根本没出来。查了文档才知道 fenced_code 和 codehilite 得手动开:
md = markdown.Markdown(extensions=[
'fenced_code',
'codehilite',
'tables',
WechatExtension()
])
fenced_code 处理围栏代码块,tables 处理表格,缺一个都不行。
2. 正则把代码块里的标签也替换了
标题转换的 re.sub 最初没加边界限制,把代码块里展示的标签也给替换了,调试了半天才从输出的乱码定位到问题。
修复方案:先把代码块区域提取出来,做完替换再塞回去:
CODE_BLOCKS = []
def extract_code(text):
def repl(m):
CODE_BLOCKS.append(m.group(0))
return f'%%CODEBLOCK_{len(CODE_BLOCKS)-1}%%'
return re.sub(r'<pre><code.*?</code></pre>',
repl, text, flags=re.DOTALL)
def restore_code(text):
for i, block in enumerate(CODE_BLOCKS):
text = text.replace(f'%%CODEBLOCK_{i}%%', block)
return text
3. 公众号对空行的处理不一致
编辑器里看是正常的空行,预览时多出一行空白。试出来是 <p><br></p> 渲染的问题。公众号编辑器会把连续空 <p> 标签吃掉但 <br> 留着。补救办法——输出时把空内容的 p 标签统一替换成 <p><br></p>。

总结
120 行代码省了每天 20 分钟的手动排版,一个月下来就是 10 个小时。值。
脚本丢在 GitHub 上了,有同样需求的朋友可以直接拿去改。AI 时代,能自动化的事就别手动做——打字再快也没脚本快。
写了 13 天自动化发布脚本的每日文章,今天这篇是关于这个流程里某个环节的优化。下一个想把封面图生成也接进去——markdown 写完,封面图自动出、排版自动转,一条命令搞定。