Python批量下载微信公众号文章图片音频,自动化采集全攻略 简介这是一套面向Python初学者与新媒体运营人员的微信公众号内容批量采集实战工具集解决公众号图文、音频及PDF导出等高频需求场景。资源包含10个核心Python脚本如wechatpho.py系列、Audiodownload、Worddownload等支持文章图片批量下载、多链接文本导出、指定音频抓取并集成免安装版wkhtmltopdf.exe实现HTML一键转PDF辅以12张示例图片、6个链接/输出文本模板及工具压缩包便于快速部署调试。压缩包共37个文件总计80.23MB涵盖py、jpg、txt、exe、zip等多种类型目录结构按功能模块划分如Phodownload、Audiodownload、MP等逻辑清晰、开箱即用。目前已有3205人学习下载提供完整可运行代码、典型输入样例link.txt及多版本适配脚本wechatpho2.py至wechatpho4.py显著降低爬取门槛并规避常见解析异常。微信公众号文章、图片、音频批量下载我用Python把这件麻烦事变成了自动化流水线做内容采集和素材整理的朋友应该都有过这种经历看到一篇公众号文章写得特别好里面的配图想存下来做参考音频想单独提取出来反复听但打开浏览器F12一个个翻资源、右键另存遇到图片防盗链还给你显示个裂图简直折磨人。这个需求我前前后后折腾了三个版本从最开始只会用requests硬怼到后来把文章HTML结构摸得门儿清——其实公众号文章的正文、图片、音频全部都是标准的HTML标签和JSON数据只要定位正确就能稳定提取。这篇文章我把自己踩过的坑和最终稳定的方案完整写出来适合已经会一点Python基础、想直接落地做成工具的人也适合纯小白照着抄作业。另外你在搜索结果里看到的各种“批量下载工具3.2 by长风998”原理也基本都是这篇文章要讲的东西学会自己写就不用依赖第三方小工具数据安全心里也有底。1. 先搞清楚公众号文章的资源藏在哪1.1 公众号文章页面到底返回了什么微信公众号的文章页面其实就是一个普通的网页服务端返回的是标准HTML。当你用浏览器打开一篇文章时地址栏的URL一般长这样https://mp.weixin.qq.com/s/xxxxxx这里面包含一个唯一的__biz参数和mid、idx等一堆参数这些参数标识了具体的公众号和文章编号。用Python请求这个URL之后返回的HTML里藏着三块核心数据var msg_title 文章标题这是标题var msg_desc 摘要这是摘要div idjs_content classrich_media_content这里面包裹了正文HTML图片和音频都在里面。正文区域有>pip install requests beautifulsoup4 lxml如果你只是临时跑一下不装lxml用Python自带的html.parser也能解析只是速度慢一些。文章一多就有明显差距建议直接上lxml。2. 整体设计思路下载器拆成三个模块写这个工具之前我先把需求拆成了三个层次避免一上来就写成一坨浆糊单篇文章解析器输入文章URL输出标题、正文文本、图片URL列表、音频URL列表资源下载器输入URL列表逐一下载支持重试、超时、防盗链处理批量调度器输入多篇文章URL循环调用解析器和下载器自动建文件夹归类。这个分层逻辑到今天看依然是合理的。解析器只负责“找出资源”下载器只负责“把资源保存到本地”调度器只负责“安排任务”。三者互不干扰后面想加一个“只下载文章存成PDF”的功能只需改解析器那一层其他模块完全不用动。3. 核心代码实现从HTML里精准提取图片和音频3.1 请求头伪装这一步决定成败微信公众号对非浏览器请求有一定校验直接裸用requests请求大概率拿到的是异常页面或者验证码。我的处理方式是构造一套完整的浏览器请求头其中User-Agent和Referer尤其重要。import requests HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://mp.weixin.qq.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } def fetch_html(url): resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding utf-8 return resp.texttimeout10不是随手写的我实测过公众号服务器偶尔响应超过5秒设置10秒能在“等待太久”和“误判超时”之间取一个平衡。3.2 解析标题和正文区域用BeautifulSoup解析正文区域是最稳的方式from bs4 import BeautifulSoup soup BeautifulSoup(html, lxml) title soup.find(h1, class_rich_media_title).get_text().strip() js_content soup.find(div, idjs_content)有些文章的h1类名不是rich_media_title比如活动页、视频页结构不一样这在后面问题排查部分展开讲。先用id定位js_content是最通用的方式id在单篇页面是唯一的。3.3 图片提取一个正则就够但要注意三点提取图片URL的方式很简单遍历js_content里所有img标签取>import re img_urls [] for img in js_content.find_all(img): src img.get(data-src) or img.get(src) if src and mmbiz.qpic.cn in src: img_urls.append(src)有个细节必须注意部分图片的>mpvoice classjs_editor_audio audio_iframe voice_encode_fileidMzA4MD... .../mpvoice音频的真实地址其实是拼接出来的voice_encode_fileid的值就是sound_url的加密参数。公众号音频文件存放在res.wx.qq.com或mmbiz.qpic.cn域名下直接请求voice_encode_fileid是拿不到原始数据的。它需要进一步通过一个URL模板拼接https://res.wx.qq.com/voice/getvoice?mediaidVOICE_ENCODE_FILEID把VOICE_ENCODE_FILEID替换成voice_encode_fileid属性的值就能直接下载。注意这个模板我在多个不同公众号上实测都可用属于公众号音频的标准分发接口。提取方式voip_tags js_content.find_all(mpvoice) audio_urls [] for tag in voip_tags: mediaid tag.get(voice_encode_fileid) if mediaid: audio_urls.append(fhttps://res.wx.qq.com/voice/getvoice?mediaid{mediaid})这里有一个容易踩的坑有些转载文章正文里的mpvoice标签是嵌套在section里的属性名拼写完全一样但值可能带有URL编码比如%3D代替。稳妥做法是对voice_encode_fileid做一次urllib.parse.unquote解码避免拼接后的URL错误。3.5 资源下载带回退策略的通用函数写一个通用的下载函数支持自定义Referer防图片防盗链重试机制网络抖动自动重试文件大小限制跳过损坏的零字节文件import os from urllib.parse import urlparse def download_file(url, save_path, refererhttps://mp.weixin.qq.com/, retries3): headers HEADERS.copy() headers[Referer] referer for attempt in range(1, retries 1): try: resp requests.get(url, headersheaders, timeout15, streamTrue) resp.raise_for_status() with open(save_path, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) if os.path.getsize(save_path) 0: os.remove(save_path) raise ValueError(downloaded file is empty) return True except Exception as e: print(f[第{attempt}次] 下载失败: {url} 原因: {e}) return False关于chunk_size我试过4096、8192、16384三个值8192在速度和内存占用之间最均衡。公众号图片通常几百KB音频几MB8192字节的块大小既不会太慢内存占用也可以忽略。4. 批量下载多篇文章、多资源的统筹调度4.1 输入方式设计批量下载首先要解决“文章URL从哪来”的问题。实用场景无非两种手动复制一批文章链接放到urls.txt文件里每行一个想抓某个公众号最近的文章列表通过抓包方式拿列表数据。第二种方式细节较多我放到第五部分单独讲。先看第一种的调度器def main(): with open(urls.txt, r, encodingutf-8) as f: urls [line.strip() for line in f if line.strip()] for idx, url in enumerate(urls, 1): print(f正在处理第 {idx}/{len(urls)} 篇: {url}) html fetch_html(url) title, img_urls, audio_urls parse_article(html) # 以文章标题建目录非法字符替换 safe_title re.sub(r[\\/:*?|], _, title) os.makedirs(safe_title, exist_okTrue) for i, img_url in enumerate(img_urls, 1): ext img_url.split(wx_fmt)[-1].split()[0] save_path os.path.join(safe_title, f图片_{i:03d}.{ext}) download_file(img_url, save_path) for j, audio_url in enumerate(audio_urls, 1): save_path os.path.join(safe_title, f音频_{j:03d}.mp3) download_file(audio_url, save_path)文件夹命名为文章标题图片图片_001.jpg这样音频音频_001.mp3这样。排序编号是我的个人习惯方便后面按顺序查看也方便和其它素材合并。4.2 增量下载与断点续传如果你要下载的文章非常多比如一次几百篇网络中途断了会导致一部分资源没下完。我加了增量判断如果文件已存在且大小大于0就跳过。这个逻辑在上面的download_file函数里加一个前置判断即可if os.path.exists(save_path) and os.path.getsize(save_path) 0: print(f文件已存在跳过: {save_path}) return True这是我做了三个版本之后才加的。前两个版本跑一半网络中断重跑一遍从头下载浪费了大把时间。现在的方案是先把所有图片URL、音频URL、保存路径全部算出来遇到已存在的文件直接跳过只下载缺失文件。这样即使中断重跑一次脚本已完成的资源不会重复下载。4.3 限速与礼貌抓取批量下载时我会在每篇文章之间加一个短暂的延时import time time.sleep(random.uniform(1, 3))不要小看这个随机延时。如果脚本以零间隔持续请求公众号的风控很容易识别出非人类行为轻则返回异常验证页重则IP被临时限制。这个延时不是官方要求但却是长期稳定跑批量的关键实测从“每篇延时3秒”降到“每篇延时1秒”对下载总量影响不大但稳定性提升明显。5. 公众号列表抓取用抓包方式获取历史文章URL5.1 为什么需要抓包手动复制文章链接适合零散下载但如果要批量下载整个公众号的历史文章手动复制不现实。公众号后台并没有公开的“文章列表导出”功能。这时候就需要借助抓包工具从电脑版微信或公众号后台页面里把列表接口的数据抓出来。5.2 抓包流程这里说的抓包指的是抓PC微信客户端和公众号服务器之间的HTTP请求用到的工具是Fiddler或Charles。我以Fiddler为例Windows环境下载安装Fiddler打开“HTTPS解密”开关并信任证书电脑版微信登录打开任意一个公众号的历史文章页在Fiddler的请求列表里过滤域名mp.weixin.qq.com往下滚动历史文章列表观察新出现的/cgi-bin/appmsg开头的请求点击该请求右侧面板能看到JSON响应数据里面就是文章列表。响应体的JSON结构大概长这样{ app_msg_cnt: 200, app_msg_list: [ { aid: xx, title: 文章标题, link: https://mp.weixin.qq.com/s?__biz...mid...idx...sn..., update_time: 1700000000 } ] }app_msg_list数组里每一项都包含文章标题和链接直接把link字段提取出来就是你下载脚本的输入。5.3 把抓包结果转成URL列表Fiddler可以把响应数据复制成文本写一个小脚本解析出全部linkimport json with open(appmsg_response.json, r, encodingutf-8) as f: data json.load(f) urls [item[link] for item in data.get(app_msg_list, [])] with open(urls.txt, w, encodingutf-8) as f: f.write(\n.join(urls))另外请求/cgi-bin/appmsg的响应头里有个Cookie字段这个Cookie是微信客户端会话凭证。后续请求历史文章列表时必须在请求头带上这个Cookie否则接口返回401。配合Python的requests库把Fiddler里看到的请求头完整复制过来就能批量翻页抓完整列表。需要说明的是抓包操作针对的是PC微信客户端和公众号服务器之间的正常数据交互你只能抓到自己有权限访问的公众号内容。这类技术仅用于个人学习、备份和素材管理请勿用于侵权用途。6. 反爬、防盗链、格式错乱四个高频问题一次说清6.1 图片下载后打不开或全是HTML内容现象图片能下载但文件后缀是.jpg打开却是一堆乱码或网页源码。原因下载到的是公众号的“拦截页”或者“错误提示页”而不是真实图片数据。常见的触发条件请求头里没带Referer或者带了错误的RefererURL缺失wx_fmt等参数导致服务器返回了错误响应。排查方法用代码打印出响应头的Content-Type。如果是text/html说明拿到的不是图片正确情况下图片的Content-Type是image/jpeg、image/png、image/webp等。解决在download_file函数里务必带上Referer: https://mp.weixin.qq.com/。这个防盗链机制是公众号图片服务器最常见的拦截手段。6.2 音频链接拼接后403公众号音频的真实CDN域名有多个常见的是res.wx.qq.com但部分老文章的音频存在mmbiz.qpic.cn下直接用getvoice模板可能403。我测试了大量样本后确认最稳定的方式是用HTML源码里的voice_encode_fileid拼getvoice接口如果失败再尝试从HTML里直接找cdn_url字段它有时会以JS变量的形式出现在页面代码中match re.search(rvar cdn_url_1_0 ([^]);, html) if match: audio_url match.group(1).replace(\\, )这个方法不保证每篇都有但老文章的比例不低。如果两种方式都拿不到那就是音频源本身已失效只能放弃。6.3 动态加载的图片抓不到少数公众号文章里的图片不是直接写在HTML里的而是通过JS动态插入到页面中。直接请求文章URL拿到的HTML里js_content区域可能没有图片img标签只有一串被转义的JSON或JS变量。这种情况只靠静态解析不够。我采用的方案是用Python的requests请求一个“模拟浏览器”的接口这个接口就是公众号文章回复页的/s页面本身在返回的HTML源码里搜索关键字window.__DATA__或window.appData这个全局变量里存了完整的渲染数据包括图片URL。这算是一个隐藏字段match re.search(rwindow\.__DATA__ (.*?);/script, html, re.S)但说实话这类文章比例很低正常创作者发布的文章大多是静态HTML。如果你抓取失败优先检查文章是否被长文折叠img被裁剪成模糊占位或是否为付费文章这两种情况需要额外的处理逻辑。6.4 公众号封禁和请求频率控制批量下载时如果请求速度过快可能会触发公众号平台风控机制IP或Cookie被临时限制访问。我不建议把并发数调得过高控制在“单线程 每篇文章间隔2~3秒”是最稳妥的。你也别想着多线程疯狂并发公众号的接口没有为爬虫优化一旦被限制可能连正常的文章浏览都会失败。实测下来限制并发不是牺牲效率反而避免了反复切换IP的麻烦。7. 实战复盘一次完整的批量下载过程为了帮你理清全流程我模拟一次真实操作场景。假设我需要下载某公众号近期十篇文章的图片和音频素材用于个人剪辑项目。第一步打开电脑版微信逐个打开文章在浏览器里复制文章链接或者用第五部分的抓包法批量获取。第二步把十篇文章链接写进urls.txt每行一个。第三步运行脚本。python wechat_downloader.py脚本运行输出正在处理第 1/10 篇: https://mp.weixin.qq.com/s/xxxx 已保存文章: 如何高效学习Python - 下载图片: 图片_001.jpg - 下载图片: 图片_002.jpg - 下载音频: 音频_001.mp3 ...第四步全部跑完后检查每个文件夹里的文件数量是否和解析出的URL数量一致。这一步很重要。我的脚本会在最后打印统计信息print(f图片: 成功 {img_success}/{len(img_urls)}音频: 成功 {audio_success}/{len(audio_urls)})如果成功率低于90%优先检查网络其次是请求头。根据我的经验稳定网络下单篇文章的图片下载成功率可以到99%以上音频到95%以上。8. 后续扩展这套代码还能干这些事当你把核心流程跑通之后追加新的能力其实不难保存正文为Markdown在解析器里提取js_content的纯文本或HTML用html2text库转成Markdown就可以生成一份适合进笔记软件的文章归档自动生成Excel清单把每篇文章的标题、URL、图片数、音频数写入Excel方便管理大批量素材定时增量抓取结合抓包获取的最新文章链接放在服务器上定时运行实现公众号内容增量备份对接视频下载公众号文章里的视频一般用iframe嵌入腾讯视频提取iframe的src后可以用you-get或ffmpeg直接拉流保存。但记住一个原则任何扩展都尽量在“解析层”做不要在“下载层”做。只要数据提取干净了后面想生成什么格式都是水到渠成。9. 结尾说点实在的我在实际使用中发现这个下载脚本最常被忽略的不是代码本身而是文件夹管理和文件名规范化。刚开始我懒得处理文件名非法字符结果Windows下好几个文件夹直接创建失败才回头加了re.sub(r[\\/:*?|], _, title)这一行。后来遇到图片多到几百张的文章又体会到编号补零的好处——图片_001和图片_100排序正常但没人想看到图片_1排在图片_10后面。这些都是跑过真实数据才会有的体会。最后再分享一个小技巧跑完批量下载后我会顺便生成一个_index.txt清单文件记录每张图片或音频对应的原文链接和原始URL。别觉得多余等一个月后想追溯某张图的出处这个清单能替你省下大量翻找时间。工具从来不是为了“跑完就完事”而是要把素材管理也纳入流程才算是真正解决了问题。本文还有配套的精品资源点击获取