1. 项目缘起:一个被“历史数据”困扰的日常需求
做内容运营或者自媒体分析的朋友,估计都遇到过这个头疼事:想回顾一下自己公众号过去几年的数据表现,看看哪篇文章爆了,哪个话题凉了,用户增长曲线是怎么走的。你兴致勃勃地打开公众号后台,准备大干一场,结果发现后台的数据导出功能,要么限制时间范围,要么导出的字段不全,格式还乱七八糟,想做个跨年度的趋势分析,得手动一页一页地翻,一篇一篇地复制粘贴。这效率,简直能把人逼疯。
更别提如果你想分析竞品或者某个垂类大号的运营策略了。公开渠道能看到的数据非常有限,阅读数、点赞数、在看数,这些最核心的互动指标,除了单篇文章页面上显示的那个数字,几乎没有批量获取的途径。市面上倒是有一些第三方数据平台,但要么收费昂贵,要么数据更新不及时,要么就是担心数据安全和合规问题。于是,自己动手写一个工具,就成了很多技术背景的运营者或数据分析师心里冒出来的念头。
我这个脚本,就是在这种背景下诞生的。它不是什么高深莫测的黑科技,核心目标就一个:自动化、批量化地获取指定公众号的历史文章数据。这里的“历史数据”,我主要聚焦在文章层面,包括但不限于:文章标题、永久链接、发布时间、阅读数、点赞数(好看数)、在看数、文章摘要(如果有)、封面图链接等。把这些数据规整地抓下来,存到本地数据库或者Excel里,后续你想做内容分析、用户画像、传播效果评估,就有了第一手、干净的数据原料。
这个需求听起来简单,但真动手做,你会发现微信公众平台的反爬机制和接口限制就像一道道关卡。它不像爬取一个普通的静态网页,直接requests加BeautifulSoup就能搞定。你需要模拟登录、处理Cookie、理解它前端渲染和数据加载的逻辑,甚至要应对频繁请求带来的封禁风险。接下来,我就把自己趟过的路、踩过的坑,以及最终跑通的方案,拆开揉碎了跟大家分享。这不是一个“万能脚本”,而是一个基于特定技术思路的实战记录,希望能给有同样需求的朋友提供一个可行的参考框架。
2. 核心思路与工具选型:为什么是“Fiddler + 公众号接口”这条路
最开始,我也尝试过几种常见思路,但都遇到了不小的障碍。
第一种是纯前端模拟。用Selenium或者Puppeteer这类浏览器自动化工具,模拟用户操作,打开公众号主页,滚动加载,然后从页面HTML里解析数据。这个方法直观,但问题很大。首先,效率极低,加载和渲染页面需要时间;其次,公众号文章列表页是动态加载的,需要不断滚动触发,稳定性差;最重要的是,阅读数、点赞数这些关键数据,在列表页是不显示的,你必须点进每一篇文章的详情页才能看到,这意味着你要发起海量的页面请求,被封IP的风险极高,几乎不可行。
第二种是寻找公开的RSS源。早些年微信公众号是支持RSS的,但现在官方早已关闭。虽然有一些第三方服务(如WeRSS)能提供部分公众号的RSS,但覆盖不全、数据延迟、且有中断风险,无法作为稳定可靠的数据源。
所以,经过一番摸索和测试,我最终确定的路线是:通过抓包工具(Fiddler/Charles)分析微信公众号网页端或手机端的网络请求,找到其获取文章列表和文章详情的真实后端接口(API),然后使用Python脚本模拟这些请求,直接获取结构化的JSON数据。
这个方案的优势很明显:
- 高效:直接调用数据接口,绕过页面渲染,速度极快。
- 精准:获取的是源头结构化的数据(JSON),字段清晰,无需复杂的HTML解析。
- 稳定:只要微信不彻底改变其接口鉴权方式和参数逻辑,脚本就能持续工作。
工具选型如下:
- 抓包分析工具:Fiddler Everywhere或Charles。我主要用Fiddler,因为它对Windows友好,且能方便地解密HTTPS流量(需要安装证书)。这一步的目的是“侦察”,找到我们需要的API地址、请求头、参数和返回数据结构。
- 开发语言:Python 3.8+。生态丰富,
requests库处理HTTP请求简单强大,json库解析数据,pandas和openpyxl处理数据存储,sqlite3或pymysql操作数据库,一套下来非常顺畅。 - 关键Python库:
requests: 发送HTTP请求的核心。json: 解析接口返回的JSON数据。pandas: 数据清洗、处理和导出为Excel。sqlalchemy/pymysql/sqlite3: 可选,用于将数据持久化到数据库。time/datetime: 处理时间、添加请求间隔,防止被封。logging: 记录脚本运行日志,方便排查问题。
注意:任何爬虫行为都应遵守网站的
robots.txt协议,并尊重数据版权。本脚本及思路仅用于个人学习、分析自有公众号数据,或获取已公开的、无明确禁止抓取声明的数据。严禁用于商业爬取、侵犯他人权益或对目标服务器造成恶意负载。
3. 关键步骤拆解:从抓包到数据落地的完整链路
整个流程可以分解为几个关键阶段,每个阶段都有需要注意的细节。
3.1 第一步:侦察——用Fiddler捕获关键API
这是最核心的一步,决定了脚本的可行性。你需要在自己电脑或手机上配置好Fiddler的代理,并安装好CA证书以解密HTTPS流量。
- 打开目标公众号:在微信PC客户端或浏览器中,打开你想要抓取数据的公众号主页。例如,在浏览器中访问
https://mp.weixin.qq.com/并登录后,进入目标公众号的“图文消息”列表页。 - 开始抓包:在Fiddler中清空当前会话,然后在公众号页面里进行“滚动加载”操作。随着你不断向下滚动,新的文章列表会被加载出来。
- 寻找目标请求:在Fiddler捕获到的一大堆请求中,你需要筛选出那个携带文章列表数据的请求。通常,这类请求的URL会包含明显的关键字,比如
appmsgpublish(已发布文章)、appmsg(文章)、list(列表)等。返回格式是application/json。 - 分析请求详情:找到疑似目标请求后,重点查看:
- Request Headers(请求头):特别是
Cookie、User-Agent、Referer、X-Requested-With等。Cookie是维持登录状态的关键,脚本需要复用这个。 - Query String Parameters(查询参数):URL问号后面的参数。通常会有
action(动作,如list_ex)、begin(开始位置)、count(每次获取数量,通常是5或10)、fakeid(公众号的唯一ID)、token(一个动态令牌)等。fakeid是公众号的身份证,至关重要。 - Response(响应):查看返回的JSON数据结构。里面应该有一个数组,包含了文章的基本信息,如标题(
title)、链接(link)、发布时间(create_time)、封面图(cover)等。但注意,阅读数(read_num)和点赞数(like_num)通常不在这里,需要另一个接口。
- Request Headers(请求头):特别是
通过反复滚动和观察,你就能确定获取列表的API URL和参数规律。例如,可能是一个类似https://mp.weixin.qq.com/cgi-bin/appmsgpublish?action=list_ex&...的地址。
3.2 第二步:解析——获取公众号唯一标识fakeid
fakeid是公众号在微信后台系统里的唯一ID,不是我们常见的微信号或公众号名称。如何获取它?
- 从公众号主页URL获取:在浏览器中打开公众号的图文消息列表页,其URL通常格式为
https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=XXXXXX==&scene=124#wechat_redirect。其中__biz参数后面的XXXXXX==(Base64编码)经过解码后,往往就是fakeid或其相关标识。但这种方式有时不稳定。 - 从抓包请求中提取(推荐):在Fiddler捕获的列表请求参数里,直接找到
fakeid参数的值。这是最准确的方式。把这个值记录下来,它就是脚本中需要使用的目标公众号ID。
3.3 第三步:构建——Python脚本的核心逻辑
有了API地址、参数和fakeid,就可以开始编写脚本了。脚本主要分为几个函数模块:
import requests import json import time import pandas as pd from datetime import datetime import logging # 配置日志 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) class WeChatCrawler: def __init__(self, cookie, token, fakeid): """ 初始化爬虫,设置关键参数 :param cookie: 从浏览器/Fiddler复制的完整Cookie字符串 :param token: 从请求参数中获取的token(注意token有时效性) :param fakeid: 目标公众号的唯一ID """ self.session = requests.Session() self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...', # 使用真实的UA 'Cookie': cookie, 'Referer': 'https://mp.weixin.qq.com/', } self.token = token self.fakeid = fakeid self.base_url = 'https://mp.weixin.qq.com/cgi-bin/appmsgpublish' self.article_list = [] # 存储所有文章基本信息 def get_article_list(self, begin=0, count=10): """ 获取一页文章列表 :param begin: 起始偏移量 :param count: 每页数量(通常最大为10,微信限制) :return: 文章列表数据 和 是否还有下一页 """ params = { 'action': 'list_ex', 'begin': begin, 'count': count, 'fakeid': self.fakeid, 'token': self.token, 'lang': 'zh_CN', 'f': 'json', 'ajax': '1' } try: resp = self.session.get(self.base_url, headers=self.headers, params=params, timeout=30) resp.raise_for_status() # 检查HTTP错误 data = resp.json() if data.get('base_resp', {}).get('ret') == 200001: logger.error("Token可能已过期或Cookie失效,请更新。") return None, False app_msg_list = data.get('app_msg_list', []) has_next = data.get('has_next', 0) == 1 for item in app_msg_list: article_info = { 'aid': item.get('aid'), 'title': item.get('title'), 'link': item.get('link'), 'create_time': datetime.fromtimestamp(item.get('create_time', 0)), 'cover': item.get('cover'), 'digest': item.get('digest', ''), # 注意:列表接口通常没有阅读数/点赞数 } self.article_list.append(article_info) logger.info(f"获取到文章: {article_info['title'][:30]}...") return app_msg_list, has_next except requests.exceptions.RequestException as e: logger.error(f"请求列表失败: {e}") return None, False except json.JSONDecodeError as e: logger.error(f"解析JSON失败: {e}, 响应文本: {resp.text[:200]}") return None, False def get_article_detail(self, article_url): """ 获取单篇文章的详细数据,特别是阅读数、点赞数。 这里需要找到获取详情的接口,可能需要解析文章页面或调用另一个API。 注意:微信对阅读数等敏感数据接口保护严密,直接通过文章链接可能拿不到。 一种常见方法是:文章列表接口返回的 `app_msg_list` 里,每个item可能包含 `appmsgid` 和 `itemidx`。 另一个详情接口可能需要组合这些参数。 由于微信策略经常变动,此部分为难点,下文会详细讨论。 """ # 此处为难点,暂不实现具体代码 pass def crawl_all(self, max_pages=50): """ 爬取所有历史文章列表 :param max_pages: 最大爬取页数,防止无限循环 """ begin = 0 count = 10 current_page = 0 while current_page < max_pages: logger.info(f"正在爬取第 {current_page + 1} 页,起始位置 {begin}") _, has_next = self.get_article_list(begin, count) if not has_next: logger.info("已爬取所有文章列表。") break begin += count current_page += 1 time.sleep(2 + random.random()) # 重要!添加随机延迟,避免请求过快 logger.info(f"列表爬取结束,共获取 {len(self.article_list)} 篇文章。") def save_to_excel(self, filename='wechat_articles.xlsx'): """将文章列表保存到Excel""" if not self.article_list: logger.warning("文章列表为空,无法保存。") return df = pd.DataFrame(self.article_list) df.to_excel(filename, index=False) logger.info(f"数据已保存至 {filename}") # 使用示例 (需要替换为你的真实数据) if __name__ == '__main__': # 这些信息需要从Fiddler捕获的请求中提取 YOUR_COOKIE = '你的Cookie字符串,很长' YOUR_TOKEN = '你的Token' YOUR_FAKEID = '公众号的Fakeid' crawler = WeChatCrawler(cookie=YOUR_COOKIE, token=YOUR_TOKEN, fakeid=YOUR_FAKEID) crawler.crawl_all(max_pages=100) # 尝试爬100页 crawler.save_to_excel()这个框架完成了最基础的文章列表抓取。但最关键、也最棘手的阅读数、点赞数,并不在列表接口中。
3.4 第四步:攻坚——如何获取阅读数与点赞数
这是整个项目最大的挑战。微信将这些数据保护得很好。经过我的测试和研究,目前(请注意时效性)有几种思路,但都不完美:
- 通过文章永久链接页面抓取(不稳定):直接请求文章链接(如
https://mp.weixin.qq.com/s/XXXXXX),从返回的HTML中解析。阅读数和点赞数可能藏在某个script标签的变量里,或者通过后续的AJAX请求加载。这种方法需要解析HTML,且微信可能随时改变前端代码结构,稳定性差。此外,频繁请求文章页面极易触发风控。 - 寻找内部数据接口(较优但复杂):在公众号管理后台(mp.weixin.qq.com)操作时,Fiddler可能会捕获到获取文章统计数据的专用接口。例如,在“图文分析”或“单篇图文”页面,可能会触发请求某个包含
getappmsgext(获取文章扩展信息)字样的API。这个接口可能需要appmsgid(文章ID)和itemidx(多图文中的索引)等参数,并且对Cookie和Token的校验极其严格。这个接口是获取阅读点赞数据最“正统”的途径,但参数构造和鉴权逻辑非常复杂,且属于微信内部接口,使用需格外谨慎。 - 模拟“在看”数据请求(仅供参考):点赞(好看)数有时可以通过模拟点击“在看”的请求来间接获取?这个思路更偏向于逆向工程,难度和风险都极高,不推荐普通用户尝试。
实操建议:对于个人学习,可以尝试方法1,但要做好解析规则经常失效的心理准备。如果只是为了分析自己公众号的数据,公众号后台本身提供了数据导出功能(虽然不好用),或者使用微信官方提供的API(需认证的服务号,且有严格权限和频率限制),这才是最合规的途径。
在我的脚本中,我暂时将get_article_detail函数留空,并添加了日志提示。在实际应用中,如果你通过抓包找到了稳定的详情数据接口,可以在此函数中实现对应的请求和解析逻辑。
4. 核心难点与避坑指南:我踩过的那些“坑”
写这个脚本的过程,就是不断踩坑和填坑的过程。下面这些经验,希望能帮你节省大量时间。
4.1 Cookie与Token的时效性与管理
- 坑1:Cookie过期:从浏览器或Fiddler复制出来的Cookie,是有生命周期的。可能几小时,也可能几天后就失效了。脚本跑着跑着就返回“未登录”错误。
- 应对:脚本里要有重试和异常处理机制。一旦检测到
ret值为200001等登录失效的标识,就记录日志并停止运行,提示用户需要更新Cookie。可以考虑将Cookie持久化到文件,并编写一个简单的“更新Cookie”的辅助流程。
- 应对:脚本里要有重试和异常处理机制。一旦检测到
- 坑2:Token的动态性:
token参数在很多请求中都需要,而且它似乎是动态变化的,和当前会话有关。直接从某一次抓包的请求里复制一个token,可能很快失效。- 应对:观察发现,在同一个登录会话中,
token在一定时间内是有效的。我们的脚本应在一次执行周期内,使用最初获取的那个token。如果脚本需要长期定时运行,可能需要模拟一个“保持会话活跃”的定时任务,或者研究token的生成/刷新机制(这很难)。
- 应对:观察发现,在同一个登录会话中,
4.2 请求频率控制与反爬策略
- 坑3:请求太快被封:如果你不加任何延迟,连续快速请求接口,很快就会被微信服务器限制,返回错误或直接封禁IP一段时间。
- 应对:必须添加延迟!在每次循环请求后,使用
time.sleep()添加一个随机延迟,比如time.sleep(2 + random.random())。这样模拟人类操作,能大大降低被封风险。对于大量历史数据,慢就是快。
- 应对:必须添加延迟!在每次循环请求后,使用
- 坑4:User-Agent被识别:使用默认的
python-requests的UA,很容易被识别为爬虫。- 应对:使用真实的浏览器User-Agent字符串。可以从Fiddler抓到的请求头里复制。
4.3 数据解析与字段缺失
- 坑5:JSON结构变化:微信后台的接口返回格式并非一成不变。今天
app_msg_list在data里,明天可能就换了个名字。- 应对:解析JSON时,多用
.get(‘key’, default)的方式,避免直接[‘key’]导致KeyError崩溃。做好日志记录,把异常的响应内容片段记录下来,方便调整解析逻辑。
- 应对:解析JSON时,多用
- 坑6:关键数据不在列表接口:如前所述,阅读数、点赞数缺失是常态。
- 应对:调整心理预期。本脚本的首要目标是高效获取文章元数据(标题、链接、时间)。对于阅读点赞数据,要么接受缺失,要么投入更多精力去攻破详情接口(并承担更高风险和不稳定性)。
4.4 关于fakeid的获取稳定性
- 坑7:
fakeid获取不到或错误:从公众号主页URL解码__biz参数不一定100%准确,特别是对于某些类型的公众号。- 应对:最可靠的方法永远是从实际成功的数据请求参数中提取。确保你从Fiddler里找到的那个能返回文章列表的请求,其参数里的
fakeid就是你要用的那个。
- 应对:最可靠的方法永远是从实际成功的数据请求参数中提取。确保你从Fiddler里找到的那个能返回文章列表的请求,其参数里的
5. 数据存储、清洗与简单分析示例
抓取到的数据是原始、杂乱的,需要经过处理才能用于分析。
5.1 存储方案选择
- JSON文件:简单,适合数据量小、临时存储。使用
json.dump保存列表。 - CSV/Excel文件:最常用,方便用Excel或Pandas打开查看。使用
pandas.DataFrame.to_csv/to_excel。 - 数据库(SQLite/MySQL):适合数据量大、需要复杂查询或长期积累。可以设计表结构,如
articles(id, title, link, publish_time, read_num, like_num, ...)。
我的脚本示例中用了Excel,因为它对大多数人最友好。在实际项目中,我更喜欢用SQLite,轻量且功能强大。
import sqlite3 def save_to_sqlite(self, db_path='wechat_data.db'): conn = sqlite3.connect(db_path) cursor = conn.cursor() # 创建表(如果不存在) cursor.execute(‘’‘ CREATE TABLE IF NOT EXISTS articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, link TEXT UNIQUE, -- 链接唯一,防止重复插入 publish_time TIMESTAMP, cover_url TEXT, digest TEXT, read_count INTEGER DEFAULT 0, like_count INTEGER DEFAULT 0, crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ‘’‘) # 插入或更新数据 for article in self.article_list: cursor.execute(‘’‘ INSERT OR REPLACE INTO articles (title, link, publish_time, cover_url, digest) VALUES (?, ?, ?, ?, ?) ‘’‘, (article[‘title‘], article[‘link‘], article[‘create_time‘], article[‘cover‘], article[‘digest‘])) conn.commit() conn.close() logger.info(f“数据已保存至SQLite数据库: {db_path}”)5.2 简单数据清洗与分析
有了数据,就可以用Pandas进行一些简单的分析了。
import pandas as pd import matplotlib.pyplot as plt # 读取数据 df = pd.read_excel(‘wechat_articles.xlsx‘) df[‘publish_time‘] = pd.to_datetime(df[‘create_time‘]) # 确保时间是datetime类型 # 1. 发布频率分析:按年-月统计文章数量 df[‘year_month‘] = df[‘publish_time‘].dt.to_period(‘M‘) monthly_count = df.groupby(‘year_month‘).size() print(“月度发文数量:“) print(monthly_count) # 2. 标题长度与阅读数关系(假设有阅读数字段‘read_num‘) if ‘read_num‘ in df.columns: df[‘title_length‘] = df[‘title‘].str.len() plt.scatter(df[‘title_length‘], df[‘read_num‘]) plt.xlabel(‘标题长度‘) plt.ylabel(‘阅读数‘) plt.title(‘标题长度与阅读数关系‘) plt.show() # 3. 找出最受欢迎的文章(按阅读数或点赞数) if ‘read_num‘ in df.columns: top_10_read = df.nlargest(10, ‘read_num‘)[[‘title‘, ‘publish_time‘, ‘read_num‘, ‘like_num‘]] print(“阅读数TOP10文章:“) print(top_10_read)这些分析虽然基础,但能快速给你一个内容表现的整体印象。
6. 脚本的优化与扩展方向
一个能跑起来的脚本只是开始,要让它更健壮、更实用,还有很多可以优化的地方。
- 配置化:将
Cookie、Token、Fakeid、请求头、数据库连接信息等写入一个配置文件(如config.yaml或config.ini),避免硬编码。 - 日志与监控:使用
logging模块记录不同级别的日志(INFO, WARNING, ERROR),并输出到文件,方便后续排查问题。可以记录每次请求的URL、状态码、耗时。 - 断点续爬:如果脚本中途因网络或封禁中断,应该能从断点恢复,而不是重头开始。可以在本地记录一个
offset.json文件,保存当前已爬取的页码或最后一条文章的ID。 - 异常重试与代理:使用
requests的适配器或tenacity库,为请求添加重试机制。如果IP被封,可以考虑集成代理IP池,但这会大大增加复杂度。 - 分布式与调度:如果需要监控成百上千个公众号,可以考虑使用
Scrapy框架,并结合Celery和Redis进行分布式任务调度。但这属于工业级方案了。 - 数据更新策略:对于已抓取过的公众号,后续运行脚本时,应该只抓取新发布的文章。可以通过对比数据库中已存在的最新文章发布时间或唯一ID(如
aid)来实现增量抓取。
最后必须再次强调,技术是把双刃剑。这个脚本分享的是在技术层面解决一个具体问题的思路和方法。在实际使用中,请务必遵守相关法律法规和平台规则,将之用于正当的学习和研究目的,控制请求频率,避免对他人服务器造成不必要的负担。对于核心的阅读点赞数据获取,如果找不到稳定合规的接口,或许接受它的缺失,转而更深入地分析已有的标题、摘要、发布时间等数据,也能获得许多有价值的洞察。毕竟,内容策略的优化,不仅仅只看那几个数字。