
简介本资源是一套面向Python初学者与新媒体运营人员的微信公众号内容批量采集实战工具集解决公众号文章、图片、音频及HTML转PDF等多场景自动化下载与导出需求。压缩包共37个文件含10个核心Python脚本如wechatpho.py系列、linktest.py、Audiodownload等、12张示例图片、6个链接/文本配置文件link.txt等、2个可执行工具wkhtmltopdf.exe及MP.zip、以及PDF、HTML、XZ等辅助文件整体80.23MB结构按功能模块清晰划分便于快速定位与二次开发。目前已有3205人学习下载涵盖文章图文提取、音频单独抓取、多链接批量处理及免安装PDF生成等完整流程。读者可直接运行对应脚本完成任务获得开箱即用的采集能力并通过多版本脚本如wechartpho2.py、wechartpho3OK.py理解不同实现思路与异常处理逻辑具备良好的实践参考价值。1. 项目背景与核心价值作为一个经常需要收集技术资料、整理行业内容的人我手里攒了不少微信公众号文章链接。每次想把这些内容保存下来要么复制粘贴到笔记软件里要么截图存档图片多了还要一张张手动保存音频更是麻烦。时间一长这种重复劳动真的让人崩溃。后来我干脆写了一套Python脚本把公众号文章的正文、图片、音频一次性拉取到本地按文章归档。今天就把这套流程完整拆开分享给大家。先给还不清楚这个需求的朋友说下背景微信公众号文章虽然支持分享和收藏但官方并没有提供“批量导出”功能。你要么一篇文章一篇文章在手机上查看要么借助各种第三方工具的免费额度要么就只能用什么笔记软件慢慢复制。对于经常要做内容备份、离线阅读、素材整理、语料采集的人来说这个痛点非常明显。而Python在网页请求、内容解析、文件保存这条链路上有天然优势几百行代码就能自己动手解决。这篇文章适合谁来参考如果你想自己写一套微信公众号文章下载工具想搞明白页面结构分析、图片懒加载处理、音频提取、并发下载这些知识点或者只是想把某几个公众号的历史文章备份到本地慢慢看都可以按这套思路来做。2. 整体方案设计与技术选型2.1 为什么用Python做这件事选Python不是因为它“热门”而是因为这个场景下它确实顺手。微信公众号文章本质上是网页下载文章最核心的几步就是拿到文章链接、请求页面内容、解析HTML提取正文和资源地址、下载资源到本地。这几个环节Python都有非常成熟的库代码量能压得很低。另一个重要原因是异常处理方便。批量下载意味着会遇到各种不确定性某个链接失效了某张图片被防盗链拦截了某个音频文件因为网络原因下载到一半断了。Python的异常捕获和重试机制写起来很自然配合装饰器或者循环就能处理好。2.2 三种获取文章链接的途径这里需要先说明一个核心前提要下载文章首先得拿到文章的URL链接。公众号文章链接的形式一般是https://mp.weixin.qq.com/s/xxx后面的xxx是文章的编号。获取方式大致有三种。第一种是手动从手机或电脑复制。在微信里打开文章点击右上角菜单选择复制链接把链接粘贴到一个文本文件里。这种方式适合文章量少、偶发需求的情况。第二种是借助搜狗微信搜索。搜狗是唯一的微信公众号文章第三方搜索入口通过它搜索关键词或公众号名称能抓到一部分文章链接。但这个频控比较严格有时候会弹出验证码不建议大规模使用。第三种方案我感觉是最省力的——通过抓包或者浏览器的开发者工具从微信PC端或网页端访问公众号历史文章列表时把返回的JSON数据捕捉到里面会包含文章的标题和链接。把链接提取出来存好就完成了批量任务的输入准备。注意无论用哪种方式请确保你下载的是自己有权限访问的内容比如你自己关注或收藏的文章且仅用于个人学习、备份用途不要大规模扩散。2.3 技术栈选型我最终的选型是这样的requests发HTTP请求获取HTML页面内容和下载图片/音频文件。BeautifulSoup4或lxml解析HTML定位文章正文、图片标签、音频标签。concurrent.futures实现多线程并发下载提升批量任务的效率。re匹配JSON数据中的特定字段比如var msg_link ...。json处理从页面或接口返回的结构化数据。pathlib或os处理文件路径按文章标题创建目录。这套组合不依赖复杂的框架也没有Scrapy那样的学习门槛适合绝大多数情况。如果文章数量巨大上千篇可以考虑上Scrapy但普通场景用requests就够了不要为了技术而技术。3. 核心环节拆解页面请求与正文解析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-Language: zh-CN,zh;q0.9, }有些文章页面在首次请求时会做302跳转requests默认会跟随跳转所以allow_redirects用默认值即可。有些链接有防抓取限制响应的不是文章内容而是验证页这时候可以先尝试带上Cookie再次请求或者降低请求频率。从我做过的测试来看只要不是高频请求正常带上浏览器的User-Agent就能稳定拿到文章页。3.2 提取正文内容拿到HTML之后第一步是按照文章ID创建存储目录然后解析正文。公众号文章的正文内容在HTML中是有固定结构的一个很简单的方式是直接查找idjs_content这个节点。这个节点是整个正文内容区的根节点。from bs4 import BeautifulSoup soup BeautifulSoup(html_text, lxml) content_div soup.find(div, idjs_content)找到节点后可以获取它的纯文本内容、内部的图片标签、音频标签等。这里有个小经验有些文章正文是分段的图片较多时会用>import re title soup.find(meta, propertyog:title)[content] author soup.find(meta, propertyog:article:author)[content] nickname soup.find(meta, propertyog:nickname)[content] publish_time re.search(rvar ct (\d), html_text).group(1)ct是Unix时间戳使用datetime转换成正常日期格式就可以。这些元数据建议顺手存成一个JSON文件或Markdown文件方便后续做目录索引。4. 图片下载懒加载与防盗链处理4.1 图片懒加载的识别与处理公众号文章为了加快首屏加载速度正文里的图片默认启用了懒加载机制。在HTML源码里图片标签通常长这样img>images content_div.find_all(img) for img in images: real_url img.get(data-src) or img.get(src) if real_url: # 处理相对路径等情况 if real_url.startswith(//): real_url https: real_url download_image(real_url, save_dir)4.2 防盗链策略公众号图片存在微信的CDN上一般情况下直接下载是没问题的。但如果你在解析后用程序批量下载偶尔会遇到403错误。主要原因就是请求头里缺少Referer。微信的CDN在某些情况下会校验来源页面如果Referer不是微信公众号的域名可能拒绝响应。解决方式就是在下载图片时也带上一个Referer: https://mp.weixin.qq.com/尤其是当图片链接的域名是mmbiz.qpic.cn时这个头基本能解决大部分403问题。def download_image(url, filepath): img_headers { User-Agent: headers[User-Agent], Referer: https://mp.weixin.qq.com/, } resp requests.get(url, headersimg_headers, timeout15) if resp.status_code 200: with open(filepath, wb) as f: f.write(resp.content)4.3 图片格式与扩展名判断图片的>import os from urllib.parse import urlparse, parse_qs def get_ext_from_url(url): query parse_qs(urlparse(url).query) fmt query.get(wx_fmt, [jpg])[0] if fmt jpeg: return .jpg return f.{fmt}如果链接里没有wx_fmt参数可以根据响应头里的Content-Type字段来判断。再用os.path.join把扩展名拼到文件名后面调用pathlib.Path.mkdir确保目录存在。5. 音频下载理解公众号音频的嵌入方式5.1 音频节点的两种常见形式公众号文章里的音频通常有两种嵌入方式。第一种是页面底部直接放一个音频播放器在HTML中对应的是mpvoice标签或audio标签。第二种是文章通过小程序或H5页面引用了音频资源这种相对少见需要单独分析页面里的JS逻辑。这里重点讲最常见的mpvoice标签。mpvoice是微信公众号自定义的音频标签不是标准HTML标签但它有两个关键属性voice_encode_fileid和play_length。其中voice_encode_fileid的值是一个类似MzkzNTI...的字符串这个字符串后面拼接上/0?wx_fmtmp3就能构成音频的实际下载地址。5.2 从mpvoice标签提取音频地址voice_tags soup.find_all(mpvoice) for voice in voice_tags: voice_id voice.get(voice_encode_fileid) if voice_id: audio_url fhttps://res.wx.qq.com/voice/getvoice?voiceid{voice_id} # 或者使用下面的拼接方式 # audio_url fhttps://mmbiz.qpic.cn/mmbiz_audios/{voice_id}/0?wx_fmtmp3这里分享一个我的经验针对微信音频的下载地址我推荐用https://res.wx.qq.com/voice/getvoice?voiceidxxx这种形式它兼容性比较好稳定性高。我自己在项目里测试过这个方式音频文件能直接下载到本地。记得下载时同样带上Referer请求头避免未知来源拦截。5.3 音频文件命名规则音频文件命名建议用“序号-标题.mp3”或“标题-第几段.mp3”这样排序清晰。如果一篇文章中有多个音频按顺序编号是必要的否则文件名会冲突。audio_index 1 for voice in voice_tags: voice_id voice.get(voice_encode_fileid) if voice_id: audio_url fhttps://res.wx.qq.com/voice/getvoice?voiceid{voice_id} audio_filename os.path.join(save_dir, faudio_{audio_index}.mp3) download_file(audio_url, audio_filename) audio_index 16. 完整实操从环境搭建到批量下载全流程6.1 环境准备与依赖安装这一步假设你的电脑已经装好了Python 3.8以上版本。如果你还没装去Python官网下载安装包安装时勾选“Add Python to PATH”否则命令行里无法直接使用Python命令。打开命令行工具Windows用PowerShell或CmdmacOS/Linux用Terminal执行下面的命令安装依赖pip install requests beautifulsoup4 lxml这三件套就是我们要用的核心库。BeautifulSoup配合lxml解析速度已经很快了足够日常使用。6.2 准备文章链接文件新建一个articles.txt文件每行放一个文章链接格式如下https://mp.weixin.qq.com/s/abc123 https://mp.weixin.qq.com/s/def456 https://mp.weixin.qq.com/s/ghi789这个文件就是我们批量任务的输入列表。你可以手动维护也可以后续用一个小脚本从浏览器书签或笔记软件里自动导出。6.3 主脚本实现下面是完整的实现代码我已经把注释写得比较详细。你把代码保存为wechat_downloader.py放在和articles.txt同一个目录下就可以直接运行。import json import os import re import requests from bs4 import BeautifulSoup from urllib.parse import urlparse, parse_qs from datetime import datetime from pathlib import Path # 全局请求头 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-Language: zh-CN,zh;q0.9, } def get_page(url): 获取文章页面HTML resp requests.get(url, headersHEADERS, timeout20) resp.raise_for_status() resp.encoding utf-8 return resp.text def parse_article(html_text): 解析文章提取标题、正文、图片链接、音频链接等信息 soup BeautifulSoup(html_text, lxml) # 提取标题 title soup.find(meta, propertyog:title) title title[content] if title else 未命名文章 # 提取作者昵称 nickname soup.find(meta, propertyog:nickname) nickname nickname[content] if nickname else 未知公众号 # 提取发布时间 publish_time None match re.search(rvar ct (\d), html_text) if match: publish_time datetime.fromtimestamp(int(match.group(1))).strftime(%Y-%m-%d %H:%M:%S) # 正文区域 content_div soup.find(div, idjs_content) if not content_div: return None # 提取纯文本 text_content content_div.get_text(separator\n, stripTrue) # 提取图片链接 image_urls [] for img in content_div.find_all(img): url img.get(data-src) or img.get(src) if url: if url.startswith(//): url https: url image_urls.append(url) # 提取音频链接 audio_urls [] for voice in soup.find_all(mpvoice): voice_id voice.get(voice_encode_fileid) if voice_id: audio_urls.append(fhttps://res.wx.qq.com/voice/getvoice?voiceid{voice_id}) # 兼容audio标签 for audio in soup.find_all(audio): src audio.get(src) or audio.get(data-src) if src: if src.startswith(//): src https: src audio_urls.append(src) return { title: title, nickname: nickname, publish_time: publish_time, text_content: text_content, image_urls: image_urls, audio_urls: audio_urls, }6.4 下载图片和音频def sanitize_filename(name): 清理文件名中的非法字符 return re.sub(r[\\/:*?|], _, name) def get_extension(url): 根据URL获取文件扩展名 query parse_qs(urlparse(url).query) fmt query.get(wx_fmt, [])[0] if fmt: return . fmt.lower() path urlparse(url).path ext os.path.splitext(path)[1] if ext: return ext return .jpg def download_file(url, filepath, extra_headersNone): 下载单个文件并保存 req_headers HEADERS.copy() if extra_headers: req_headers.update(extra_headers) resp requests.get(url, headersreq_headers, timeout20) if resp.status_code 200: with open(filepath, wb) as f: f.write(resp.content) return True return False def download_article_assets(article_info, save_root): 下载文章的所有资源 title sanitize_filename(article_info[title]) nickname sanitize_filename(article_info[nickname]) # 按公众号名/文章名建目录 article_dir Path(save_root) / nickname / title article_dir.mkdir(parentsTrue, exist_okTrue) # 保存正文纯文本 with open(article_dir / 正文.txt, w, encodingutf-8) as f: f.write(article_info[text_content]) # 保存元数据 meta { 标题: article_info[title], 公众号: article_info[nickname], 发布时间: article_info[publish_time], } with open(article_dir / 元数据.json, w, encodingutf-8) as f: json.dump(meta, f, ensure_asciiFalse, indent2) # 下载图片 img_dir article_dir / images img_dir.mkdir(exist_okTrue) for idx, img_url in enumerate(article_info[image_urls], 1): ext get_extension(img_url) filename img_dir / fimage_{idx:03d}{ext} ok download_file(img_url, filename) if not ok: print(f图片下载失败: {img_url}) # 下载音频 audio_dir article_dir / audios audio_dir.mkdir(exist_okTrue) for idx, audio_url in enumerate(article_info[audio_urls], 1): filename audio_dir / faudio_{idx:03d}.mp3 ok download_file(audio_url, filename) if not ok: print(f音频下载失败: {audio_url})6.5 并发批量执行from concurrent.futures import ThreadPoolExecutor, as_completed def process_one(url, save_root): 处理单篇文章的完整流程 print(f开始处理: {url}) try: html get_page(url) info parse_article(html) if info is None: print(f文章解析失败: {url}) return download_article_assets(info, save_root) print(f完成: {info[title]}) except Exception as e: print(f处理异常: {url}, 错误信息: {e}) def main(): save_root wechat_downloads with open(articles.txt, r, encodingutf-8) as f: urls [line.strip() for line in f if line.strip()] print(f共发现 {len(urls)} 篇文章开始批量下载...) # 多线程并发5个线程避免请求过快被限制 with ThreadPoolExecutor(max_workers5) as executor: futures {executor.submit(process_one, url, save_root): url for url in urls} for future in as_completed(futures): future.result() # 触发异常便于捕获 print(全部任务执行完毕) if __name__ __main__: main()运行方式很简单在命令行里执行python wechat_downloader.py脚本会自动读取articles.txt里的链接逐一下载并保存到wechat_downloads目录下。6.6 运行效果说明执行脚本后你会在终端看到类似这样的输出开始处理: https://mp.weixin.qq.com/s/abc123 完成: 为什么我建议你用Python做自动化 开始处理: https://mp.weixin.qq.com/s/def456 ...下载完成后目录结构是这样的wechat_downloads/ ├── 某公众号/ │ ├── 为什么我建议你用Python做自动化/ │ │ ├── 正文.txt │ │ ├── 元数据.json │ │ ├── images/ │ │ │ ├── image_001.jpg │ │ │ └── image_002.png │ │ └── audios/ │ │ └── audio_001.mp3 │ └── 另一篇文章标题/ │ └── ... └── 另一个公众号/ └── ...这个结构很干净一篇文章一个目录图片和音频单独归档正文是纯文本方便后续搜索和阅读。7. 常见问题与排查技巧实录7.1 图片全部是占位图现象下载下来的图片都是同一个透明或灰色的图大小才几KB。原因没有取>import time import random def process_one(url, save_root): # ... time.sleep(random.uniform(2, 5))7.4 文件名为空或乱码现象保存时提示文件名无效或者保存下来的文件名为空。原因文章标题里可能包含/、\、:、*、?、、、、|这些在Windows下不允许出现在文件名里的字符。解决使用sanitize_filename函数统一过滤这一步不能省。我在代码里已经实现了你可以直接使用。7.5 请求超时或连接错误现象下载过程中隔三差五报Timeout或ConnectionError。原因访问网络的稳定性问题或者目标服务器响应慢。解决给请求设置超时时间比如15到20秒。另外建议在下载文件时增加一个简单的重试机制。def download_file_with_retry(url, filepath, retries3): for attempt in range(retries): try: ok download_file(url, filepath) if ok: return True except Exception: pass time.sleep(2) return False7.6 图片文件损坏或大小异常现象图片能下载但打开时提示文件已损坏。原因下载过程中网络中断或者服务器返回了错误页面但HTTP状态码仍是200。解决下载完成后检查文件大小如果小于某个阈值比如1KB大概率是下载异常重新下载或记录下来人工检查。import os def check_file_valid(filepath, min_size1024): if os.path.exists(filepath): return os.path.getsize(filepath) min_size return False7.7 正文内容提取不全现象某些文章正文只提取到了一部分比如只提到开头的摘要部分。原因文章可能需要登录或分页加载或者正文区域不是标准的js_content。解决手动查看该文章的HTMl结构确认实际正文所在的容器ID是否被动态注入了。有些文章会使用模板号不同的ID这种情况需要单独适配。我这里提供一个锦囊先看js_content是否存在如果不存在就全局搜索rich_media_content或article-content等常见类名。8. 扩展思路从单篇下载到体系化内容管理基础版工具能用之后很多人会想更进一步。这里我分享几个我实际在用的扩展方向代码结构已经预留好了改起来不费劲。8.1 增加文章历史列表自动获取手动维护articles.txt终究是麻烦事。如果你需要长期跟踪某个公众号的动态可以写一个小脚本定时从历史列表页获取最新文章链接追加到任务列表里。这样做需要处理分页和增量更新核心就是记录已下载过文章的唯一编号每次只处理新出现的链接。8.2 保存为HTML而不是纯文本纯文本适合搜索和提取信息但丢失了排版。如果你想保留原文格式可以在保存正文时直接把content_div对应的HTML代码存下来with open(article_dir / 原文.html, w, encodingutf-8) as f: html_content str(content_div) # 把图片data-src替换为src这样HTML才能正常显示图片 html_content re.sub(rdata-src([^]), rsrc\1, html_content) f.write(html_content)把图片的style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />