系列收官篇。接口打通后,最后一块硬骨头是内容格式——微信对正文图片、封面大小、CSS 写法各有铁规,而且失败方式都是「静默失败」:不报错,就是结果不对。本篇三个限制各给完整实现代码。
一、限制 1:正文图片只认微信图床
现象:草稿推送成功,打开一看正文图全没了。
原因:微信对正文 <img> 的 src 有域名白名单——只接受自家图床地址,外链 URL 直接吞掉,不提示不报错。
解法:推送前把每张外链图下载下来,调 media/uploadimg 转存,用返回的微信域 URL 替换原文:
def upload_body_image(token, img_bytes):
data = compress(img_bytes, max_bytes=1500*1024, max_width=1080)
r = requests.post(f"{BASE}/cgi-bin/media/uploadimg",
params={"access_token": token},
files={"media": ("image.jpg", data, "image/jpeg")},
timeout=120)
return r.json()["url"]
def replace_images(html, token):
for url in re.findall(r'<img[^>]+src="(https?://[^"]+)"', html):
raw = requests.get(url, timeout=60).content
html = html.replace(url, upload_body_image(token, raw))
return html
注意原图别直接传:我的配图一张 4-5MB,166 张就是近 800MB,先压到 1080 宽 / 1.5MB 内再传,整批推送速度快一倍。
二、限制 2:封面素材 64KB 上限
现象:封面上传接口报错。
原因:material/add_material 的 thumb 类型限制 64KB(是 KB 不是 MB),高分辨率配图普遍超标几十倍。
解法:循环压缩——先逐档降 JPEG 画质,降不动就缩尺寸,直到达标:
def compress(img_bytes, max_bytes, max_width=None, quality=88):
im = Image.open(io.BytesIO(img_bytes)).convert("RGB")
if max_width and im.width > max_width:
im = im.resize((max_width, int(im.height * max_width / im.width)),
Image.LANCZOS)
q = quality
while True:
buf = io.BytesIO()
im.save(buf, "JPEG", quality=q)
if buf.tell() <= max_bytes or q <= 30:
return buf.getvalue()
q -= 12
if buf.tell() > max_bytes * 2 and im.width > 600:
im = im.resize((int(im.width*0.75), int(im.height*0.75)),
Image.LANCZOS)
# 封面:compress(raw, max_bytes=60*1024, max_width=900)
# → add_material(type=thumb) → thumb_media_id
实测压到 60KB 内的封面,在订阅号消息列表里清晰度完全够用。
三、限制 3:只认内联样式
现象:Markdown 转好的排版进草稿箱后全丢——标题没层级、表格没边框、引用块没底色。
原因:微信编辑器只认内联样式(标签内 style="..."),外部样式表和 class 全部失效。markdown 库产出的裸 HTML 等于没有样式。
解法:准备一套标签级样式表,转换后逐标签注入:
INLINE_STYLES = {
"h2": "font-size:20px;font-weight:700;margin:30px 0 14px;"
"padding-left:10px;border-left:4px solid #2b6cb0;",
"p": "font-size:16px;line-height:1.9;margin:0 0 16px;color:#3f3f3f;",
"blockquote": "margin:16px 0;padding:12px 16px;background:#f7f7f7;"
"border-left:3px solid #c9c9c9;color:#777;font-size:14px;",
"table": "width:100%;border-collapse:collapse;font-size:14px;margin:16px 0;",
"th": "background:#f2f4f6;border:1px solid #ddd;padding:8px 10px;",
"td": "border:1px solid #ddd;padding:8px 10px;",
"img": "display:block;margin:16px auto;max-width:100%;border-radius:6px;",
# ul/ol/li/pre/code/hr/strong 按需补充
}
def to_wechat_html(body_md):
html = markdown.markdown(body_md, extensions=["tables", "fenced_code"])
for tag, style in INLINE_STYLES.items():
html = re.sub(rf"<{tag}>", f'<{tag} style="{style}">', html)
return f'<section style="font-family:...">{html}</section>'

一次写好后,所有文章排版统一,比手动排的还整齐。这套正则注入对标准 markdown 库输出够用(它默认不产带属性的标签);如果引入其他转换器产生带属性的标签,需要换成 HTML parser 处理。
四、两个顺手记的小坑
标题 64 字符上限:连载文标题带长前缀容易顶到,超长时 draft/add 直接拒收。本地先校验,别等对端报错。
Windows 控制台 GBK:与微信无关但 Windows 必踩——日志里输出 emoji 直接 UnicodeEncodeError,程序崩在「打印成功提示」这一步。脚本开头强制 UTF-8:
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
五、系列总结
三篇回顾:
- (一)选型与架构:官方 API 完胜模拟登录和第三方工具;四接口串起全流程
- (二)接通细节:token 缓存、IP 白名单(动态 IP 应对)、报错对照表、排期策略
- (三)格式限制(本篇):图床转存、封面 64KB、内联样式,全是「静默失败」型坑
最终效果:30 篇 Markdown → 草稿箱 20 分钟,30/30 成功,166 张图自动转存。整条链路沉淀为一个 200 行的脚本,新增文章一条命令进草稿箱。
三条通用经验:
- 微信内容侧三大铁规(自家图床 / 封面 64KB / 内联样式)提前知道,省一半调试时间
- 所有「静默失败」型问题,靠结果校验兜底(推完抓草稿内容核对图片数量),不能只看接口返回码
- 能本地检查的不发到对端,报错提示里带上解法——脚本是自己天天用的,体验细节值得抠
如果这篇对你有用,欢迎关注看「AI 工具人 PM 实战」系列更新;这三个格式限制你踩过几个?评论报个数;觉得有用就收藏备用。