ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python实现微信公众号文章批量下载与本地化工具

2026/9/5 22:04:36 拓冰建站 浏览量
Python实现微信公众号文章批量下载与本地化工具 简介这是一套面向开发者与新媒体运营人员的微信公众号文章批量采集与归档工具源码解决官方接口受限下对历史文章、阅读量、评论等数据的合规化存档需求。资源包共109个文件含34个TypeScript核心逻辑文件、24个Vue前端组件、29张PNG图标与界面素材以及配置类JSON、样式CSS、构建脚本JS等完整覆盖前后端与部署能力压缩后仅12.02MB轻量易部署。已有717人学习下载适用于内容合规审计、自媒体知识库建设、竞品分析及私有化数据中台搭建等场景。源码支持HTML格式100%还原原文排版含内嵌图片与样式提供Docker一键部署方案、订阅式自动抓取机制及开放API接口同时集成合集下载、视频/图文消息解析、评论与转发量导出等功能结构清晰、模块解耦便于二次开发与定制化扩展。1. 项目概述与核心价值最近在整理一些行业资料时发现很多有价值的深度分析都沉淀在各大微信公众号里。想系统性地保存下来离线阅读或者做个本地知识库手动一篇篇复制粘贴效率太低而且格式容易乱。市面上的一些在线工具要么收费要么限制多多用起来总是不顺手。于是我花时间研究并实现了一个基于Python的微信公众号文章批量下载工具并决定把完整的源码和实现思路分享出来。这个工具的核心目标很明确给你一个公众号的名称或ID它能自动抓取该公众号的历史文章列表并将文章正文、图片、视频等资源完整地下载到本地生成结构清晰的HTML或Markdown文件方便你永久保存和查阅。这个工具特别适合内容创作者、市场分析师、学术研究者以及任何需要系统性归档网络信息的个人。比如你可以用它来追踪竞品公众号的更新动态批量下载某个垂直领域的教程合集建立个人知识库或者单纯就是收藏自己喜欢的系列文章。整个过程完全自动化解放双手。接下来我会详细拆解这个工具的设计思路、关键技术点、具体的实现步骤以及我在开发过程中踩过的那些坑和总结的实用技巧。你会发现自己动手实现一个这样的工具并没有想象中那么复杂。2. 工具整体设计与核心思路拆解2.1 为什么选择自己开发而非使用现成工具在决定动手之前我调研过不少方案。有浏览器插件有在线的“文章转PDF”网站也有一些客户端软件。但它们普遍存在几个痛点一是往往有数量限制下载几十篇后就需要付费二是对公众号文章的样式支持不完整图片可能丢失排版会错乱三是无法实现真正的“历史文章”批量抓取很多工具只能处理单篇文章链接。最重要的是数据安全性和可控性。使用第三方在线服务你的阅读列表和下载内容可能会经过别人的服务器存在隐私泄露风险。自己写的工具所有数据处理都在本地完成源码在手一切透明可控。2.2 核心工作流程与模块划分这个下载工具的工作流程可以抽象为四个核心步骤对应四个功能模块公众号定位与列表抓取输入一个公众号的名称或唯一标识如biz参数工具需要模拟微信的请求获取到该公众号的“历史消息”页面列表。这是最核心也是最具挑战的一步因为微信的反爬机制一直在升级。文章链接提取与去重从抓取到的列表页面HTML中解析出每一篇文章的永久链接通常以https://mp.weixin.qq.com/s/...开头。这里需要处理分页加载微信列表是瀑布流和链接去重避免同一篇文章被多次下载。文章内容解析与清洗访问每一个文章链接抓取完整的HTML页面。然后需要从纷繁复杂的页面代码中精准地提取出文章标题、作者、发布日期、正文内容包含图文排版、以及文章内的图片、视频、音频等资源的原始地址。资源下载与本地化打包将上一步解析出的正文HTML进行美化并将文中引用的所有图片、视频等远程资源下载到本地文件夹中同时修改HTML中的链接使其指向本地文件。最后将处理好的HTML文件连同资源文件夹按照日期和标题有序地保存起来。整个工具的架构是模块化的每个步骤相对独立。这样做的好处是如果未来微信的页面结构发生变化或者你想增强某个功能比如增加导出为PDF只需要修改对应的模块而不会牵一发而动全身。2.3 技术选型Python生态的优势我选择Python作为实现语言主要基于以下几点考虑丰富的网络爬虫库requests用于发送HTTP请求简单高效BeautifulSoup4和lxml用于解析HTML如鱼得水。强大的异步支持对于批量下载图片这种IO密集型任务aiohttp和asyncio可以大幅提升效率实现并发下载。成熟的HTML处理工具html2text可以方便地将HTML转为Markdownreadability之类的库可以帮助提取文章主体内容。跨平台与易部署Python脚本在Windows、macOS、Linux上都能运行通过pip安装依赖非常方便也易于打包成可执行文件分享给不懂技术的朋友。3. 关键技术细节与实操要点3.1 如何获取公众号文章列表—— 破解“历史消息”接口这是整个项目的第一个技术难关。你不能直接通过一个公开的API拿到列表。经过抓包分析我发现微信公众平台的文章列表是通过一个特殊的接口动态加载的关键点在于以下几个参数__biz: 公众号的唯一身份标识类似于ID。uin/key: 与用户会话相关的加密参数。offset: 控制分页的偏移量。count: 每页返回的文章数量通常固定为10。begin/end: 时间戳用于按时间范围筛选。实际操作中最可靠的方法是模拟微信客户端或网页端的请求。你需要先通过搜索或已知文章找到一个目标公众号的任意一篇文章。从这篇文章页面的URL或源代码中可以提取出关键的__biz值。然后构造一个携带正确Cookie和Header的请求去访问一个固定格式的列表URL。Cookie尤为重要它代表了你的登录状态通常可以通过登录微信PC版或网页版后从开发者工具的Network面板中复制出来。重要提示这里涉及模拟请求和抓取公开数据务必遵守目标网站的robots.txt协议并将抓取频率控制在合理的、不对对方服务器造成压力的范围内例如在请求间添加随机延时。本工具仅用于个人学习与研究目的的数据收集。3.2 精准提取文章正文与资源拿到文章链接后下一步是获取纯净的正文。微信公众号文章的页面包含了大量无关元素顶部关注引导、底部点赞评论、侧边栏广告、相关推荐等等。我们的目标是只保留文章本身。这里有两种主流思路CSS选择器精准定位分析文章页面的HTML结构找到包裹正文的那个div标签它的id或class通常包含js_content,rich_media_content等关键词。用BeautifulSoup定位到这个节点提取其内部HTML就能得到相对干净的正文。这种方法速度快但依赖微信前端的DOM结构一旦微信改版选择器可能失效。使用内容提取库比如readability-lxml或goose3。这些库的算法会分析页面的标签密度、文本长度等因素智能地判断哪一部分是核心文章内容。这种方法通用性更强不依赖特定网站结构但可能无法100%完美还原微信复杂的排版如代码块、特殊格式等。我采用的是混合策略优先尝试用预设的CSS选择器去定位如果定位失败或内容过短则回退到使用readability库进行提取。实测下来这种方法在绝大多数情况下都能取得很好的效果。对于图片、视频等资源它们在HTML中通常以img>requests2.25.1 beautifulsoup44.9.3 lxml4.6.3 aiohttp3.8.1 readability-lxml0.8.1 html2text2020.1.16 tqdm4.62.3 # 用于显示进度条在终端中进入项目目录运行以下命令一键安装pip install -r requirements.txt4.2 核心代码模块实现我们将代码分成几个Python文件便于管理。1. 配置文件 (config.py)这里存放一些可配置的常量比如请求头、超时时间、保存路径等。import os # 请求头模拟浏览器访问 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } # 网络请求超时时间秒 TIMEOUT 10 # 文章保存的根目录 SAVE_ROOT ./wechat_articles # 请求间隔延时避免过快请求导致IP被限制秒 REQUEST_DELAY 22. 列表抓取模块 (crawler.py)这个模块负责获取公众号的文章列表。我们实现一个WeChatListCrawler类。import requests import time import json from bs4 import BeautifulSoup from urllib.parse import urljoin, urlparse import re from config import HEADERS, TIMEOUT, REQUEST_DELAY class WeChatListCrawler: def __init__(self, cookieNone): self.session requests.Session() if cookie: self.session.headers.update({Cookie: cookie}) self.session.headers.update(HEADERS) def get_article_links_from_page(self, list_url): 从一个列表页中解析出文章链接 links [] try: resp self.session.get(list_url, timeoutTIMEOUT) resp.raise_for_status() soup BeautifulSoup(resp.text, lxml) # 微信公众号文章链接的常见模式 for link in soup.find_all(a, hrefre.compile(r^https?://mp\.weixin\.qq\.com/s)): href link.get(href) if href and chksm not in href: # 过滤掉一些带校验参数的临时链接 links.append(href) time.sleep(REQUEST_DELAY) # 礼貌性延时 except Exception as e: print(f抓取列表页失败 {list_url}: {e}) return list(set(links)) # 去重 # 注意这里简化了连续分页抓取的逻辑。实际中你需要分析微信的Ajax接口或模拟滚动加载。 # 一个常见方法是不断改变URL中的offset参数来获取更多历史消息。由于微信历史列表的完整抓取涉及更复杂的接口分析和模拟上述代码仅提供了基础框架。完整实现需要分析微信的Ajax请求构造包含__biz,uin,key,offset等参数的POST请求并解析返回的JSON数据。这部分代码因微信更新频繁而需要动态调整是项目的核心难点之一。3. 文章解析与下载模块 (article.py)这个模块负责处理单篇文章。import os import aiohttp import asyncio from bs4 import BeautifulSoup from readability import Document import html2text from config import HEADERS, SAVE_ROOT import re class ArticleFetcher: def __init__(self, sessionNone): self.session session or aiohttp.ClientSession(headersHEADERS) async def fetch_article(self, url): 获取并解析单篇文章 try: async with self.session.get(url) as resp: html await resp.text() except Exception as e: print(f下载文章失败 {url}: {e}) return None # 方法1: 尝试用CSS选择器定位微信正文 soup BeautifulSoup(html, lxml) content_div soup.find(div, idjs_content) or soup.find(div, class_re.compile(rich_media_content)) if content_div and len(content_div.get_text(stripTrue)) 200: # 找到了正文区域 title soup.find(meta, propertyog:title) title title[content] if title else 未知标题 # 提取纯净的正文HTML content_html str(content_div) else: # 方法2: 使用readability库智能提取 doc Document(html) content_html doc.summary() title doc.title() # 提取文章发布日期 publish_time None publish_meta soup.find(meta, propertyarticle:published_time) or soup.find(span, class_re.compile(publish_time)) if publish_meta: publish_time publish_meta.get(content) or publish_meta.get_text() return { url: url, title: title, publish_time: publish_time, raw_html: html, content_html: content_html } async def download_resource(self, url, save_path): 异步下载单个资源图片/视频到本地 try: async with self.session.get(url) as resp: if resp.status 200: content await resp.read() os.makedirs(os.path.dirname(save_path), exist_okTrue) with open(save_path, wb) as f: f.write(content) return True except Exception as e: print(f下载资源失败 {url}: {e}) return False async def localize_article(self, article_info, save_dir): 将文章资源本地化并保存为HTML if not article_info: return False soup BeautifulSoup(article_info[content_html], lxml) resource_dir os.path.join(save_dir, resources) os.makedirs(resource_dir, exist_okTrue) # 处理图片 img_tasks [] for i, img in enumerate(soup.find_all(img)): src img.get(data-src) or img.get(src) # 优先使用data-src if src and src.startswith(http): local_filename fimage_{i:03d}.jpg local_path os.path.join(resource_dir, local_filename) img_tasks.append(self.download_resource(src, local_path)) # 替换src属性为本地路径 img[src] f./resources/{local_filename} if data-src in img.attrs: del img.attrs[data-src] # 并发下载所有图片 if img_tasks: results await asyncio.gather(*img_tasks) print(f图片下载完成: {sum(results)} 成功, {len(results)-sum(results)} 失败) # 保存最终的HTML文件 final_html f !DOCTYPE html html head meta charsetutf-8 title{article_info[title]}/title style body {{ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica Neue, Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; line-height: 1.6; color: #333; }} img {{ max-width: 100%; height: auto; display: block; margin: 1em auto; }} /style /head body h1{article_info[title]}/h1 psmall发布时间: {article_info[publish_time] or 未知} | 原文链接: a href{article_info[url]}{article_info[url]}/a/small/p hr {str(soup)} /body /html safe_title re.sub(r[\\/*?:|], _, article_info[title])[:50] # 清理非法文件名字符 filename f{article_info[publish_time][:10] if article_info[publish_time] else nodate}_{safe_title}.html filepath os.path.join(save_dir, filename) with open(filepath, w, encodingutf-8) as f: f.write(final_html) print(f文章已保存: {filepath}) return True4. 主控与调度模块 (main.py)这是程序的入口负责协调各个模块的工作。import asyncio import os from crawler import WeChatListCrawler from article import ArticleFetcher from config import SAVE_ROOT async def main(): # 1. 初始化抓取器需要填入有效的Cookie cookie 你的微信Cookie # 请从浏览器开发者工具中复制 if not cookie or cookie 你的微信Cookie: print(请先在config.py或此处配置有效的微信Cookie。) return list_crawler WeChatListCrawler(cookiecookie) # 2. 指定要抓取的公众号列表页URL示例需要替换 # 如何获取这个列表URL通常可以从公众号资料页或历史消息页的地址栏复制。 # 例如https://mp.weixin.qq.com/mp/profile_ext?actionhome__bizMzA5NDk...#wechat_redirect list_urls [ https://mp.weixin.qq.com/mp/profile_ext?actionhome__bizYOUR_BIZ_HEREscene124#wechat_redirect, ] all_article_urls [] for url in list_urls: print(f正在抓取列表: {url}) links list_crawler.get_article_links_from_page(url) all_article_urls.extend(links) print(f找到 {len(links)} 篇文章链接。) # 简单去重 all_article_urls list(set(all_article_urls)) print(f去重后总计 {len(all_article_urls)} 篇待下载文章。) if not all_article_urls: print(未找到任何文章链接请检查列表URL或Cookie。) return # 3. 创建文章保存目录 import datetime today_str datetime.datetime.now().strftime(%Y%m%d_%H%M%S) save_dir os.path.join(SAVE_ROOT, fcollection_{today_str}) os.makedirs(save_dir, exist_okTrue) # 4. 异步抓取并保存所有文章 fetcher ArticleFetcher() tasks [] for i, article_url in enumerate(all_article_urls[:10]): # 这里限制为前10篇作为演示 print(f({i1}/{len(all_article_urls[:10])}) 处理: {article_url}) task asyncio.create_task(fetcher.fetch_and_save_article(article_url, save_dir)) tasks.append(task) # 控制并发度避免过快 if len(tasks) 3: # 同时处理3篇文章 await asyncio.gather(*tasks) tasks [] # 处理剩余任务 if tasks: await asyncio.gather(*tasks) await fetcher.session.close() print(f\n所有文章处理完成已保存至目录: {save_dir}) # 为ArticleFetcher添加一个便捷方法 async def fetch_and_save_article(self, url, save_dir): article_info await self.fetch_article(url) if article_info: await self.localize_article(article_info, save_dir) ArticleFetcher.fetch_and_save_article fetch_and_save_article if __name__ __main__: asyncio.run(main())4.3 如何获取关键的Cookie和列表URL这是工具能运行起来的前提。这里提供一个通用的手动获取方法获取Cookie在电脑上打开浏览器推荐Chrome或Edge。访问https://mp.weixin.qq.com并登录你的微信需要能登录公众平台普通用户扫码登录即可。按F12打开开发者工具切换到Network(网络) 面板。刷新页面在网络请求列表中点击任意一个请求如profile_ext。在右侧Headers(标头) 选项卡中找到Request Headers部分复制Cookie字段后面那一长串值。获取公众号列表页URL含__biz在微信PC客户端或网页版搜索找到目标公众号。点击进入公众号主页查看“历史消息”。此时浏览器地址栏的URL通常就包含了__biz参数。将这个完整的URL复制下来填入代码中的list_urls列表。安全警告Cookie是个人敏感信息相当于你的登录凭证。切勿将包含真实Cookie的代码上传到GitHub等公开平台。建议将Cookie存储在环境变量或本地配置文件中并通过.gitignore忽略该配置文件。5. 常见问题、排查技巧与优化建议在实际使用和开发过程中你肯定会遇到各种问题。下面是我总结的一些常见坑点和解决方案。5.1 请求被拒绝或返回空数据现象代码运行后抓取到的文章列表为空或者请求返回403/404错误。排查Cookie失效微信Cookie的有效期有限可能已过期。重新登录并获取新的Cookie。请求头不完整模拟的User-Agent不够像真实浏览器。可以尝试从浏览器开发者工具中直接复制一个完整的请求头信息更新到config.py的HEADERS中。频率过高短时间内发送过多请求触发反爬。务必在请求间添加随机延时例如time.sleep(random.uniform(1, 3))。列表URL或参数错误公众号的__biz可能不正确或者列表接口已更新。需要重新抓包分析最新的请求格式。5.2 文章内容提取不完整或格式错乱现象保存的HTML文件里缺少正文或者排版混乱图片显示不正常。排查与解决CSS选择器失效微信前端改版了。打开一篇公众号文章用开发者工具检查正文区域最新的HTML结构和CSS类名更新article.py中的content_div查找逻辑。图片未成功本地化检查图片链接是否成功替换。可能是图片的style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />