☰
公众号文章数据采集:阅读、点赞、转发、在看、评论、收藏批量抓取实战
2026/10/17 18:43:29 网站建设 项目流程

简介:这份资源面向需要采集微信公众号文章互动数据的开发者与运营分析人员,解决阅读数、点赞数、转发数、在看数、评论数、收藏数等指标难以批量获取的问题,适合具备一定编程基础、希望快速验证接口调用与数据抓取思路的技术人员参考。压缩包共6个文件,约330KB,包含可执行程序、配置文件、动态链接库及JSON解析组件等,其中exe与config用于直接运行和参数调整,dll与xml支撑数据序列化与依赖调用,txt则提供试用说明,整体结构紧凑、开箱即用。目前已有226人学习下载,说明该方案在同类需求中具备一定参考价值。读者可借此了解公众号文章各项互动数据的获取流程、请求参数组织方式与返回结果解析方法,并在此基础上扩展为批量采集或定时监控脚本,为内容运营分析、竞品数据对比等场景提供可复用的技术思路。

1. 公众号文章数据采集:阅读、点赞、转发、在看、评论、收藏到底能不能拿

做公众号内容复盘时,最头疼的不是写不出东西,而是写完不知道效果到底怎么样。后台自带的统计只给你看个大概,想横向对比几篇稿子的收藏数、转发数、在看数,或者把评论拉出来做词频分析,就得靠手工一条条抄。我见过有运营同学拿手机截图再手动录入 Excel,一篇稿子折腾十几分钟,十篇下来一下午没了。这个资源包解决的就是这件事:把公众号文章的阅读数、点赞数、转发数、在看数、评论数、收藏数这几项指标,用脚本批量抓下来,落成结构化数据。它适合做内容运营、数据分析、竞品监测的从业者,也适合想拿真实数据练爬虫的新手。但先说清楚边界:这不是一个“输入链接就出结果”的傻瓜工具,它依赖你对公众号文章页面的结构有基本认知,也需要处理登录态和频率控制,下面一步步拆。

2. 数据从哪来:公众号文章页面的几个数据入口

2.1 页面里能直接读到的字段

打开一篇公众号文章,右键查看网页源代码,你会看到几个关键位置。阅读数和点赞数在页面加载时由前端脚本异步填充,早期版本直接写在 HTML 里,现在多数情况需要等 JS 执行完才能拿到。在看数和转发数不在正文页,而在文章底部的互动区域,部分账号会显示“xx人都在看”。评论数在正文末尾的留言区入口,收藏数则更隐蔽,通常只在后台或者特定接口返回。

我一般会先确认目标文章是哪种形态:普通图文、视频号关联文章、还是被转载的。形态不同,能拿到的字段完整度差别很大。普通图文最全,转载文章往往缺收藏数和转发数。

2.2 接口与页面结构的对应关系

公众号文章的数据并不是全部走同一个接口。阅读、点赞、在看通常由mp.weixin.qq.com/mp/getappmsgext这类接口返回,评论走appmsg_comment接口,收藏数在公开页面基本拿不到,需要登录态下的特定请求。这意味着你不能指望一个请求搞定所有字段,得分开处理。

常见做法是先用文章 URL 拿到__biz、mid、idx、sn这几个参数,它们是后续请求的钥匙。缺一个,接口就返回错误码。下面这段代码演示怎么从文章链接里提取这些参数:

import re from urllib.parse import urlparse, parse_qs def parse_article_url(url): """ 从公众号文章链接中提取 __biz, mid, idx, sn 支持两种常见链接格式: 1. https://mp.weixin.qq.com/s?__biz=xxx&mid=xxx&idx=1&sn=xxx 2. https://mp.weixin.qq.com/s/xxxxx (短链,需要先重定向) """ parsed = urlparse(url) qs = parse_qs(parsed.query) # 标准格式直接取 biz = qs.get('__biz', [None])[0] mid = qs.get('mid', [None])[0] idx = qs.get('idx', [None])[0] sn = qs.get('sn', [None])[0] if not all([biz, mid, idx, sn]): raise ValueError("链接缺少必要参数,可能是短链,需要先请求重定向拿到完整 URL") return { '__biz': biz, 'mid': mid, 'idx': idx, 'sn': sn } # 使用示例 url = "https://mp.weixin.qq.com/s?__biz=MzA5xxx&mid=2650000001&idx=1&sn=abc123def" params = parse_article_url(url) print(params)

逻辑说明:urlparse拆出查询串,parse_qs把每个参数转成列表,取第一个值。参数说明:__biz是公众号唯一标识,mid是文章消息 ID,idx是文章在推送中的位置,sn是签名。这四个参数缺一不可,短链必须先发一次请求拿到 302 跳转后的完整地址。

提示:短链重定向要用allow_redirects=False手动拿 Location 头,不要直接跟随,否则容易丢参数。

3. 动手抓取:请求构造、登录态与字段解析

3.1 构造带 Cookie 的请求

公众号接口对未登录请求限制很严,阅读数可能返回 0 或者直接拒绝。你需要从浏览器里复制一份有效的 Cookie,重点是wap_sid2和wxuin这两个字段。我一般会在无痕窗口登录一次,把 Cookie 导出成 JSON,脚本里读取。

import requests import json import time def load_cookies(cookie_file): """从 JSON 文件加载 Cookie,格式为 [{"name":"xx","value":"yy"}, ...]""" with open(cookie_file, 'r', encoding='utf-8') as f: cookie_list = json.load(f) return {c['name']: c['value'] for c in cookie_list} def fetch_article_stats(params, cookies): """ 请求 getappmsgext 接口获取阅读、点赞、在看等数据 """ api = "https://mp.weixin.qq.com/mp/getappmsgext" headers = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) " "AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", "Referer": "https://mp.weixin.qq.com/", "Accept": "application/json, text/javascript, */*; q=0.01" } payload = { "__biz": params['__biz'], "mid": params['mid'], "idx": params['idx'], "sn": params['sn'], "is_only_read": "1", "is_temp_url": "0", "appmsg_type": "9" } resp = requests.post(api, params=payload, headers=headers, cookies=cookies, timeout=10) if resp.status_code != 200: raise RuntimeError(f"请求失败,状态码 {resp.status_code}") data = resp.json() # 关键字段在 appmsgstat 里 stat = data.get('appmsgstat', {}) return { 'read_num': stat.get('read_num', 0), 'like_num': stat.get('like_num', 0), 'old_like_num': stat.get('old_like_num', 0), # 在看数 'forward_num': stat.get('forward_num', 0), # 转发数,部分账号有 'comment_num': data.get('comment_count', 0) # 评论数,有时在顶层 } # 使用 cookies = load_cookies('cookies.json') params = parse_article_url(url) stats = fetch_article_stats(params, cookies) print(stats) time.sleep(2) # 频率控制,别贪快

逻辑说明:getappmsgext是 POST 请求,参数放在 query string 里,Cookie 放在请求头。返回的 JSON 里appmsgstat包含阅读、点赞、在看,评论数有时在顶层comment_count。参数说明:is_only_read=1表示只读模式,appmsg_type=9是图文类型。time.sleep(2)是必须的,连续请求容易触发风控。

3.2 评论和收藏的单独处理

评论数虽然能在上面接口拿到,但评论内容要单独请求appmsg_comment接口,分页拉取。收藏数在公开接口里基本拿不到,我试过几个版本,只有登录态下且文章被收藏过才可能返回,多数情况返回 0 或者字段缺失。如果你确实需要收藏数,常见做法是引导用户截图后台,或者用公众号后台的导出功能,脚本层面不要抱太大期望。

def fetch_comments(params, cookies, page=0): """拉取评论,page 从 0 开始""" api = "https://mp.weixin.qq.com/mp/appmsg_comment" payload = { "action": "getcomment", "__biz": params['__biz'], "mid": params['mid'], "idx": params['idx'], "sn": params['sn'], "offset": page * 10, "count": 10, "appmsg_type": "9" } resp = requests.get(api, params=payload, cookies=cookies, timeout=10) data = resp.json() comments = [] for item in data.get('elected_comment', []): comments.append({ 'nickname': item.get('nick_name', ''), 'content': item.get('content', ''), 'like_num': item.get('like_num', 0), 'create_time': item.get('create_time', 0) }) return comments # 翻页拉取 all_comments = [] for p in range(3): # 先拉前 3 页 all_comments.extend(fetch_comments(params, cookies, p)) time.sleep(1.5)

逻辑说明:评论接口用 GET,offset控制偏移,count每页条数。参数说明:elected_comment是精选评论列表,nick_name是昵称,content是评论正文,like_num是评论点赞数。翻页时注意offset是累加的,不是页码。

注意:评论接口对频率更敏感,建议每页间隔 1.5 秒以上,连续快速翻页大概率返回空列表。

4. 避坑与排查:那些让我返工三次的细节

4.1 阅读数返回 0 或字段缺失

现象:请求成功,状态码 200,但read_num是 0,其他字段正常。原因:Cookie 过期或者wap_sid2失效,接口降级返回默认值。解决:重新登录导出 Cookie,检查wap_sid2是否还在有效期内。我一般会在脚本里加一个校验,如果连续三篇阅读数都是 0,就停下来提示更新 Cookie。

4.2 转发数和收藏数拿不到

现象:接口返回的 JSON 里根本没有forward_num和collect_num字段。原因:这两个字段不是所有文章都公开,部分账号类型或者文章设置里关闭了展示。解决:不要强求,把能拿到的字段先落库,缺失的标记为null。如果业务强依赖,只能走后台导出。

4.3 频率控制导致 IP 被限

现象:前几篇正常,后面全部返回ret: -3或者直接 403。原因:请求间隔太短,触发了风控。解决:把time.sleep调到 3 到 5 秒,并且不要用同一个 Cookie 高频请求。我一般会准备两三个账号轮换,但注意别用自动化工具登录,手工登录一次导出 Cookie 就行。

4.4 短链解析丢参数

现象:parse_article_url报错说缺少参数。原因:拿到的是https://mp.weixin.qq.com/s/xxxxx这种短链,查询串里没有__biz。解决:先发一次 HEAD 请求,拿Location头,再从完整 URL 里解析。代码里加一个判断,如果__biz为空就走重定向逻辑。

4.5 评论翻页重复或漏数据

现象:第二页和第一页有重复评论,或者中间少了几条。原因:offset计算方式不对,或者评论在翻页过程中有新数据插入。解决:用create_time去重,翻页时记录上一页最后一条的时间戳,下一页从这个时间往前取。别依赖固定offset。

5. 数据落库与批量调度:把零散字段变成可分析的表

5.1 用 SQLite 存结构化结果

抓下来的数据如果只打印在终端,等于白干。我一般用 SQLite 建两张表:一张存文章指标,一张存评论。字段设计上,阅读、点赞、在看、转发、评论数各占一列,收藏数单独一列允许为空。下面建表语句可以直接抄:

CREATE TABLE IF NOT EXISTS article_stats ( id INTEGER PRIMARY KEY AUTOINCREMENT, biz TEXT NOT NULL, mid TEXT NOT NULL, idx INTEGER NOT NULL, sn TEXT NOT NULL, title TEXT, read_num INTEGER DEFAULT 0, like_num INTEGER DEFAULT 0, watching_num INTEGER DEFAULT 0, forward_num INTEGER DEFAULT 0, comment_num INTEGER DEFAULT 0, collect_num INTEGER, crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(biz, mid, idx) ); CREATE TABLE IF NOT EXISTS article_comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, biz TEXT NOT NULL, mid TEXT NOT NULL, nickname TEXT, content TEXT, like_num INTEGER DEFAULT 0, create_time INTEGER, crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

逻辑说明:UNIQUE(biz, mid, idx)保证同一篇文章不会重复插入,用INSERT OR REPLACE更新。参数说明:collect_num允许 NULL,因为多数情况拿不到。crawl_time记录抓取时间,方便做时间序列对比。

5.2 批量调度与增量更新

单篇抓取没意义,批量才是这个资源包的价值。我一般把文章链接存一个列表,循环调用抓取函数,每篇之间sleep3 秒。增量更新靠UNIQUE约束,重复抓取时用INSERT OR REPLACE覆盖旧值,这样阅读数涨了也能更新。

import sqlite3 def save_stats(conn, params, stats, title=''): sql = """ INSERT OR REPLACE INTO article_stats (biz, mid, idx, sn, title, read_num, like_num, watching_num, forward_num, comment_num, collect_num) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ conn.execute(sql, ( params['__biz'], params['mid'], params['idx'], params['sn'], title, stats.get('read_num', 0), stats.get('like_num', 0), stats.get('old_like_num', 0), stats.get('forward_num', 0), stats.get('comment_num', 0), stats.get('collect_num') )) conn.commit() # 批量循环 conn = sqlite3.connect('gzh_stats.db') for url in article_urls: try: p = parse_article_url(url) s = fetch_article_stats(p, cookies) save_stats(conn, p, s) print(f"OK: {url}") except Exception as e: print(f"FAIL: {url} -> {e}") time.sleep(3) conn.close()

逻辑说明:INSERT OR REPLACE依赖唯一约束,存在则替换。参数说明:conn.commit()每篇提交一次,避免中途崩溃丢数据。time.sleep(3)是保命间隔,别省。

5.3 验证数据完整性的两个技巧

第一,抓完后跑一个SELECT COUNT(*) FROM article_stats WHERE read_num = 0,如果比例超过 20%,大概率是 Cookie 失效或者风控。第二,对比同一篇文章隔天再抓一次,阅读数应该只增不减,如果变小了,说明接口返回了缓存或者异常值。我一般会在脚本最后加一个简单的校验输出,不通过就人工看一眼。

提示:别把 Cookie 硬编码在脚本里,单独放一个文件,加个.gitignore,这是血泪经验。

6. 进阶:把评论数变成词频,用收藏数反推内容价值

数据落库之后,真正的活才开始。我习惯用jieba对评论做分词,统计高频词,看看读者到底在讨论什么。下面这段代码把评论内容读出来,去掉停用词,输出前 20 个高频词:

import sqlite3 import jieba from collections import Counter STOPWORDS = set(['的', '了', '是', '我', '你', '他', '她', '它', '在', '和', '就', '都', '而', '及', '与', '这', '那']) def word_freq(biz, mid): conn = sqlite3.connect('gzh_stats.db') rows = conn.execute( "SELECT content FROM article_comments WHERE biz=? AND mid=?", (biz, mid) ).fetchall() conn.close() words = [] for (content,) in rows: for w in jieba.cut(content): if len(w) > 1 and w not in STOPWORDS: words.append(w) return Counter(words).most_common(20) # 使用 top_words = word_freq('MzA5xxx', '2650000001') for w, c in top_words: print(f"{w}: {c}")

逻辑说明:jieba.cut做中文分词,过滤单字和停用词,Counter统计频次。参数说明:len(w) > 1去掉单字噪声,停用词表可以按业务补充。这个结果可以直接喂给词云工具,或者做人工复盘。

收藏数虽然难拿,但一旦拿到,它的分析价值比阅读数高。阅读数受推送时间影响大,收藏数更反映内容的长尾价值。我一般会把收藏数除以阅读数,得到一个“收藏率”,收藏率高的文章往往适合做系列化或者做成付费内容。这个指标不用多,每周看一次趋势就够。

从那以后我每次跑批量抓取,都强制先跑一遍 Cookie 有效性校验和频率检查,宁可慢一点,也不愿意再看到跑了一半全部返回 0 的场面。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询