| name | wechat-exporter |
| description | 完整爬取/导出微信公众号文章的工作流助手。当用户提到"下载微信公众号文章"、"爬取微信公众号文章"、"批量导出微信公众号"、"备份微信公众号内容"、"抓取微信公众号文章"、"保存微信公众号文章"时立即使用本 skill。无论用户说的是"爬"、"抓"、"导出"、"备份"还是"下载",只要涉及微信公众号文章批量获取,都应触发本 skill。 |
微信公众号文章导出工作流
两种场景,选对路径:
| 场景 | 方案 |
|---|
用户给了具体文章 URL(mp.weixin.qq.com/s/...) | → 路径 A:curl + Python 直接抓取 |
| 批量导出整个公众号 / 多篇文章 | → 路径 B:wechat-article-exporter 在线工具 |
路径 A:单篇文章 URL → 直接下载转换
环境说明(已验证有效)
- 抓取:用
curl(系统自带),加浏览器 User-Agent,能正常拿到微信文章 HTML
- 解析:用
python(注意:Windows 上必须用 python,不能用 python3,后者是 Microsoft Store 占位符)
- 文件路径:Windows 下 Python 无法访问
/tmp/,中间文件和输出文件都用实际 Windows 路径(如 E:/test_file/)
- 终端编码:Windows 终端是 GBK,print 中文会乱码——直接写文件,用
sys.stdout.buffer.write(...encode('utf-8')) 输出状态
不可用的方法
WebFetch 工具:mp.weixin.qq.com 被网络策略拦截,无法使用
var msg_title 正则:当前微信 HTML 里已无此变量,提取不到标题
完整脚本(可直接复用)
第一步:下载 HTML
curl -sL --max-time 20 \
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" \
-H "Accept-Language: en-US,zh;q=0.9" \
"https://mp.weixin.qq.com/s/ARTICLE_ID" > "OUTPUT_DIR/wechat_article.html"
第二步:解析并转 Markdown
import re, sys
with open('OUTPUT_DIR/wechat_article.html', encoding='utf-8', errors='replace') as f:
html = f.read()
title = 'wechat-article'
for pat in [r'property="og:title"\s+content="(.*?)"', r'content="(.*?)"\s+property="og:title"']:
m = re.search(pat, html)
if m:
title = m.group(1).strip()
break
start = html.find('id="js_content"')
end = -1
for marker in ['id="js_content_blocked"', '<div class="rich_media_tool"', 'class="rich_media_extra_info"']:
end = html.find(marker, start)
if end != -1:
break
content_html = html[start:end] if end != -1 else html[start:start+200000]
def html_to_md(s):
s = re.sub(r'<!--.*?-->', '', s, flags=re.S)
s = re.sub(r'<style[^>]*>.*?</style>', '', s, flags=re.S)
s = re.sub(r'<script[^>]*>.*?</script>', '', s, flags=re.S)
for n in range(6, 0, -1):
s = re.sub(, m, n=n: + *n + + re.sub(, , m.group()).strip() + , s, flags=re.S)
s = re.sub(, m: + re.sub(, , m.group()) + , s, flags=re.S)
s = re.sub(, m: + re.sub(, , m.group()) + , s, flags=re.S)
s = re.sub(, m: + re.sub(, , m.group()) + , s, flags=re.S)
s = re.sub(, m: + re.sub(, , m.group()).strip() + , s, flags=re.S)
s = re.sub(, m: + re.sub(, , m.group()) + , s, flags=re.S)
s = re.sub(, m: + re.sub(, , m.group()) + + m.group() + , s, flags=re.S)
():
mm = re.search(, m.group(), flags=re.S)
+ mm.group().strip() + mm
s = re.sub(, img_to_md, s, flags=re.S)
s = re.sub(, m: + re.sub(, , m.group()).strip(), s, flags=re.S)
s = re.sub(, , s)
s = re.sub(, , s)
s = re.sub(, , s)
s = re.sub(, m: + re.sub(, , m.group()).strip() + , s, flags=re.S)
s = re.sub(, , s)
s = re.sub(, , s)
s = re.sub(, m: + re.sub(, , m.group()).strip().replace(, ) + , s, flags=re.S)
s = re.sub(, , s)
s = s.replace(, ).replace(, ).replace(, ).replace(, ).replace(, ).replace(, )
s = s.replace(, )
s = re.sub(, , s)
s.strip()
md = html_to_md(content_html)
md = re.sub(, , md)
md = re.sub(, , md)
md = re.sub(, , md)
():
t = line.strip()
t = re.sub(, , t)
t = re.sub(, , t)
(
re.(, t, flags=re.I)
re.(, t, flags=re.I)
re.(, t, flags=re.I)
)
edge_lines = md.splitlines()
edge_lines ( edge_lines[].strip() is_edge_meta_line(edge_lines[])):
edge_lines.pop()
edge_lines ( edge_lines[-].strip() is_edge_meta_line(edge_lines[-])):
edge_lines.pop()
md = .join(edge_lines)
():
t = re.sub(, , line)
re.sub(, , t.strip())
():
t = line.strip()
(t re.(, t) re.search(, t))
():
p = plain_line(line)
(
re.(, p)
re.(, p)
re.search(, p)
)
():
t = line.strip()
p = plain_line(t)
(
re.(, t)
re.search(, p)
re.search(, p)
re.search(, p)
re.search(, p)
re.search(, p)
re.search(, p)
( p)
( p)
( p)
)
lines = md.splitlines()
footer_start = -
i, line (lines):
is_footer_promo_line(line):
footer_start = i
j = i -
blanks =
j >= :
prev = lines[j]
prev.strip():
blanks +=
blanks <= :
j -=
is_decorative_line(prev) is_short_footer_lead(prev):
footer_start = j
blanks =
j -=
footer_start != -:
lines = lines[:footer_start]
drop_line_pats = [
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
]
lines = [
line line lines
is_decorative_line(line) (re.(p, plain_line(line)) p drop_line_pats)
]
lines ( lines[-].strip() is_decorative_line(lines[-]) re.(, lines[-].strip())):
lines.pop()
lines re.(, lines[-].strip()):
lines.pop()
lines ( lines[-].strip() is_decorative_line(lines[-]) re.(, lines[-].strip())):
lines.pop()
md = .join(lines)
md = re.sub(, , md).strip()
output =
(, , encoding=) f:
f.write(output)
sys.stdout.buffer.write(.encode())
关键注意事项:
- 将
OUTPUT_DIR 替换为实际 Windows 路径(如 E:/test_file)
- 将
ARTICLE_ID 替换为实际文章 ID
- 微信图片 URL 有时效性(数天),需要长期保存时单独下载图片
- 如果需要本地化图片,只从清理后的 Markdown 提取图片 URL;不要从原始 DOM 图片列表直接下载,避免保存已剔除的文末关注二维码、互动引导图等尾部推广内容
- 输出 Markdown 默认只保留标题和正文,不主动添加来源、原文链接、作者、发布时间等元信息;这些字段在不同文章中写法不固定,若出现在正文头尾,用通用边缘元信息规则清理
路径 B:批量导出整个公众号
使用开源工具 wechat-article-exporter(GitHub)。
使用在线版(零部署,推荐)
- 打开 https://down.mptext.top
- 用微信公众平台账号(非个人微信号)扫码登录
- 搜索目标公众号,加载全部文章列表
- 按需筛选(标题/作者/时间/原创/合集),或全选
- 选择导出格式后批量下载
| 格式 | 适用场景 |
|---|
| HTML | 100% 还原排版样式,带图片,首选 |
| Markdown | Obsidian、Notion 等笔记软件 |
| DOCX | Word 编辑 |
| Excel | 元数据统计(标题/日期/阅读量) |
| TXT / JSON | 纯文本 / 结构化数据 |
Docker 私有部署
services:
wechat-article-exporter:
image: ghcr.io/wechat-article/wechat-article-exporter:latest
ports:
- "3000:3000"
volumes:
- ./data:/app/.data
restart: always
docker compose up -d
获取评论和阅读量(进阶)
需抓包获取 Credentials(__biz / key / uin / pass_ticket):
- 安装 mitmproxy,配置手机代理
- 手机微信打开目标公众号任意文章
- 从
mp.weixin.qq.com 请求中提取上述四个参数
- 在工具"设置 → Credentials"中填入
详见:https://docs.mptext.top/advanced/wxdown-service.html
常见问题
| 问题 | 原因 | 解决方法 |
|---|
| 扫码登录失败 | 用的是个人微信号 | 必须用公众平台账号(mp.weixin.qq.com)扫码 |
| 搜索不到公众号 | 公众号关闭了被搜索 | 无法通过此工具获取 |
| 加载文章很慢 | 文章多 + 接口频率限制 | 正常现象,工具自动续传 |
| HTML 图片不显示 | 微信图片链接过期 | 尽快下载,或在线打开 HTML |
| session expired | 登录会话超时 | 重新扫码 |