Wechat To Pdf
概述
抓取微信公众号文章的正文图片(正文多为长图/书页扫描图),按序合成为 PDF。只转图片,不提取文字——图文混排文章的纯文字段落不会进 PDF;适用于图片型内容(如书本扫描页、图文长图)。核心难点是微信反爬:普通浏览器 UA 会被拦截返回 17KB 验证页,必须使用微信内置浏览器 UA 才能拿到完整正文。
触发说明:本技能只处理微信公众号文章,触发基于能力意图("公众号图片内容 → PDF")。范围词(如"第一章")只是批量模式的一个过滤参数,不作为技能识别依据。
两种使用模式
模式一:微信单篇
用户给一个文章链接(https://mp.weixin.qq.com/s/xxxx 或带 __biz/mid/idx/sn 参数的完整链接):
python scripts/wechat_to_pdf.py --url "文章链接" --out "输出目录" [--name "自定义文件名"]
文件命名规则(单篇):识别这篇文章的书信息来命名,书的信息一般见于公众号标题(程序自动读取 og:title,通常即书名)。优先级——① --name 自定义名;② 文章标题(自动清洗掉"(章节目录索引)"等说明性后缀);③ 链接标识兜底(如短链 AbCdEf123),保证不会生成无意义文件名。
模式二:微信目录页批量(核心场景)
用户给一个目录索引页链接(一篇文章里列了所有章节的超链接),并指定范围:
python scripts/wechat_to_pdf.py --index "目录页链接" --scope "第一章" --out "输出目录"
- 自动解析目录页全部正文链接,按 (mid, idx, sn) 去重
--scope传章节关键词(如"第一章"、"单元一"),只处理匹配条目;不传则处理全部- 文件夹与文件都按目录各层级命名:从目录页的单元/章标题自动推导两级文件夹,文件用目录页里该条目的标题(节名/章名)命名,如
输出目录/第一单元 单元名/第一章 章名/第一节 节名.pdf;整体未识别出目录层级时所有 PDF 平铺在输出根目录,个别条目缺单元/章信息时归入「其他」文件夹 - 文件命名规则(批量):文件名 = 目录条目标题(目录页里该链接的锚文本)→ 文章自身标题(书的信息)→
mid_idx兜底,保证与目录层级一一对应 - 完成后在输出目录生成
_report.txt核对报告(成功/失败明细,失败按原因分类) - 其他参数:
--max N限流调试、--retry N失败自动重试轮数(默认 1 轮)、--skip-existing跳过已有文件
自检机制(内置)
- 抓取诊断修复:抓取失败时按策略池自动换组合重试(桌面/苹果/安卓微信 UA、完整请求头、文章自身作为来源页共 5 套),并诊断失败原因(反爬拦截 / 链接失效 / 网络错误 / 内容异常),报告里写明原因与已尝试策略。
- PDF 完整性校验:每篇合成后立即用 pypdf 校验——文件可打开、页数 > 0;校验失败视为该篇失败并记录原因。
- 失败自动重试:批量模式跑完后,失败项自动进入下一轮重试(默认 1 轮,
--retry 0关闭,--retry 2加一轮)。 - 核对报告:
_report.txt含总体统计、失败原因分类、逐篇明细,方便快速定位。 - 断点续传:
--skip-existing跳过已生成 PDF,中断后重跑不会重复下载。
关键实现要点(勿改动,实测踩坑)
- UA 必须用 MicroMessenger:脚本内
UA常量已内置,普通浏览器 UA 会命中反爬验证页(verify.js),表现为len(r.text) < 20000且无js_content。 - 图片在
data-src:正文图片是懒加载,真实地址在<img data-src="...">,不是src。且只取js_content容器内的,自动排除头部/尾部(头像、封面、推荐、广告位)。图片域名判断必须用mmbiz.qpic.cn全域名,不能只匹配子串mmbiz——否则会混入res.wx.qq.com/mmbizappmsg/...js等 JS 文件引用导致下载失败。 - 新版 JS 渲染排版兜底:部分公众号正文由 JS 动态渲染(HTML 里无静态
js_content容器、图片藏在脚本数据里)。容器内提取为空时,回退从cdn_url: 'https://mmbiz.qpic.cn/...'数据字段提取正文图片,并排除封面(og:image/twitter:image)与头像,避免把非正文图合成进 PDF。 - 广告/组件过滤:正文内如混入广告位、小程序卡片、音视频组件(特征:class/data-type 含
mp-common、js_sg_bar、qqmusic、mp-miniprogram、js_ad),先整块切除再提取图片。 - 链接反转义:目录页链接含
&,需html.unescape;图片 URL 可能含#锚点需截断。 - webp 兼容:图片可能是 webp 格式,
img2pdf不支持,合成前用 Pillow 转 png。 - 请求限速:图片下载
sleep 0.4、文章之间sleep 1.5,避免频控封 IP。 - 同名去重:目录页同一链接常对应多个锚文本(如"章末提示 / 第一节 XX"共用一个链接),按 (mid, idx, sn) 去重,一篇只处理一次。
- 目录页标题解析:章节标题是
<p><strong><span>…</span></strong></p>结构(非 h1-h4),且章名可能被拆成多个 span,必须匹配整个<strong>块拼接文本。扫描范围限定在js_content容器内,单遍跟踪<a>状态跳过链接内文本(避免页面脚本里的<a字符串干扰)。坐标须加正文偏移,与链接节点保持同一基准,否则排序错乱。 - 通用层级识别(不依赖特定公众号排版):三策略递进——① 强前缀:
第X单元/篇/部分/一、→一级,第X章/专题/(X)→二级,第X节→三级;② 字号聚类兜底:仅当页面无任何强前缀层级词时启用,按字号最大=一级、次大=二级(18px/16px 风格),避免把大字号正文误判为标题;③ 都识别不出则平铺存放。文件夹名直接用上下文标题列表(一级在前、二级在后),不做特定内容匹配,因此"第一部分/模块一/专题"等风格同样支持。
环境依赖
- Python 3 + requests + Pillow + img2pdf(自检用 pypdf,已随 venv 安装)
- 运行环境:
C:/Users/jengson/.workbuddy/binaries/python/envs/default/Scripts/python.exe(已装齐依赖) - 若在其他环境运行:
pip install requests pillow img2pdf pypdf
环境适配说明
- 若运行环境的回收站不可用、或
os.remove删临时文件被安全策略拦截,脚本内置_force_delete()会用系统底层 API 兜底删除,图片文件都能删掉。 - 极端情况下可能残留一个空的
_tmp_imgs目录(仅限删除被拦截的环境,正常环境不会有),不影响 PDF 产物,手动删掉即可。
常见问题
- 抓取失败:脚本会自动诊断原因并显示,常见原因包括——「反爬拦截」(自动换 5 套微信 UA 策略重试)、「链接失效」(文章被删除或链接过期,需人工确认)、「网络错误」(网络波动,稍后重跑)、「内容异常」(页面返回异常)。批量模式下失败项会在报告中按原因分类。
- 某篇无图片:可能是纯文字推文(本技能只转图片,不提取文字),如实告知用户。
- 图文混排文章:只转图片,文字段落不会进 PDF。若需要图文都转,属另一套 HTML 渲染方案,超出本技能范围。
- 无关小图自动过滤:脚本默认过滤掉明显不是正文的图片——开头/末尾的装饰条、小图标、公众号关注二维码图等。规则是:①单图短边过小(宽 < 280 或高 < 220)、像素过少(< 9 万)、比例极端(宽高比 > 5 或 < 0.2)直接丢弃;②末尾 1 张如果像素明显小于正文主流书页(< 1/3 且差值 > 10 万),也丢弃。下载时会在终端打印"已过滤无关小图 N 张"和原因,正常书页内容不会被误杀。
- 输出文件名:单篇按书的信息(文章标题清洗后)命名,批量按目录条目标题命名;脚本已自动清洗
\/:*?"<>|非法字符,空标题会自动用链接标识/mid_idx兜底,不会出现无意义文件名。
资源
scripts/wechat_to_pdf.py
完整抓取-下载-合成主脚本,含两种模式、自检与重试机制。
Scan to join WeChat group