之前有个朋友找我说想写个Python爬虫把公司公众号每篇文章的阅读量、点赞量自动抓下来做运营分析。一打开后台一个半月、几十篇文章要一篇篇手动记数据想想就头疼。这活儿用Python来做其实很简单——文章链接是固定的页面数据是公开的只需要一个脚本循环跑一遍就行。这篇文章我会彻底讲清楚微信公众号文章阅读数/点赞数抓取的原理和完整实现从最基础的页面分析讲到最终能直接跑的Playwright代码保证你照着做5分钟就能抓下第一篇文章的数据。适合有一定Python基础、想入门爬虫的读者也适合拿来直接改造成自己的小工具。1. 先拆解微信文章的加载逻辑数据藏在哪1.1 直接GET HTML拿到的只有骨架如果你兴冲冲地写一个最基础的爬虫import requests url https://mp.weixin.qq.com/s/xxxxxxxx resp requests.get(url) resp.encoding utf-8 html resp.text print(html)然后在返回的HTML源码里搜索“阅读数”或“readNum”你会发现两种情况要么什么都搜不到要么能搜到一个idreadNum的标签但里面的内容是空的。这不是你代码写错了而是微信压根就没把阅读数渲染在原始的HTML文件里——页面加载之后浏览器会执行一堆JavaScript这些JS再去调用后端接口把阅读数、点赞数填进页面。这是现代网页非常常见的“前后端分离”模式。微信的公众号文章页虽然看起来像个轻量静态页面但内部依然走了“先加载壳、再填充数据”的流程。所以想用requests.get一把梭直接拿到带数据的HTML是不现实的。1.2 找到真正的数据来源接下来要搞清楚的问题就是那个把阅读数填进页面的JS到底调的哪个接口方法很简单用Chrome打开一篇公众号文章按F12打开开发者工具切到Network面板然后刷新页面。你会发现浏览器发了很多请求其中有一个请求的名字特别显眼可能是getappmsgext或者类似带appmsg字样的接口。点开它看Response{ appmsgstat: { like_num: 32, old_like_num: 100, read_num: 12034 } }阅读数和点赞数就在这个接口的返回体里。这个接口的完整URL长这样参数已打码https://mp.weixin.qq.com/mp/getappmsgext? __bizMzA3ND... mid2651523456 idx1 sn8c1234... appmsg_tokenxxx keyxxx fjson ...到这里你已经知道了数据在哪下一步的问题就变成了能不能用requests直接模拟发这个请求1.3 为什么不能直接调这个接口理论上可以但实际操作里有两个门槛。第一个门槛是appmsg_token。这个token是微信在加载文章页时在页面HTML里写入的变量。你可以在一开始用requests请求文章URL然后在返回的HTML源码里搜appmsg_token一般能找到一段类似var appmsg_token xxxxx的代码。但这玩意儿有时效性刷新得太频繁或隔的时间太长可能就失效了。第二个门槛是key参数。这是一个签名值由appmsg_token、__biz、mid、idx、sn用一套算法计算出来的。具体的算法是微信前端JS里的一个压缩混淆过的函数想逆向出来得花时间分析混淆代码。所以如果你只是想快速拿数据为了一个签名去硬刚混淆JS性价比太低了。我当时果断放弃了纯requests方案转而用“有头浏览器”来解决问题——只要浏览器能正常渲染页面我就能从页面上直接读到阅读数和点赞数完全不需要关心接口和签名算法。2. 技术选型不逆向签名改用浏览器渲染2.1 三条技术路线的对比在动手写代码之前我先把可选方案摆出来对比了一下方案实现难度速度稳定性适用场景requests 手动构造接口签名高极快中token易过期大规模长期抓取有逆向能力的人Selenium中较慢中启动慢浏览器控制不稳单机小批量兼容老项目Playwright低中高快速验证、中小批量、维护成本低纯requests方案对新手不友好光是还原签名算法这一关就能卡住一半人。Selenium是老牌工具但它的find_element_by_*老接口在最新版已经被移除而且在没有显式等待的情况下经常要靠sleep硬扛写出来的代码既丑又容易挂。对比下来Playwright 是最平衡的选择。2.2 为什么我最终选了Playwright第一新项目API设计好。Playwright的page.locator、page.wait_for_selector设计更现代化等元素不用手写轮询用起来比Selenium顺手太多。第二自动等待做得好。默认有actionability检查元素可见、可操作才继续执行省掉大量time.sleep(1)这种“玄学等待”。实际体验下来同样的任务Playwright的代码量大约只有Selenium的六成。第三内置多浏览器支持。Chromium、Firefox、WebKit都能跑无论用户环境是Windows、macOS还是Linux兼容性都更好。当然Playwright也不是没有缺点——它比纯requests方案慢不少。每打开一篇文章算上网络请求和JS渲染至少要等2到3秒。但如果只是统计几十上百篇文章这点时间完全能接受。5分钟抓20篇绰绰有余。2.3 环境准备两条命令搞定先安装Python包pip install playwright playwright install chromium注意这是两条命令。很多人只跑了第一条装完库之后忘了下载浏览器内核然后运行时报Executable doesnt exist。这里的playwright install chromium就是去下载Chromium内核到本地默认会放在用户目录下的缓存文件夹里。安装完成后验证一下版本python -c import playwright; print(playwright.__version__)能输出版本号就说明环境OK了。3. 完整代码抓到单篇文章的阅读数和点赞数3.1 核心代码不到30行直接上代码这是完整可跑的版本from playwright.sync_api import sync_playwright def fetch_wechat_article(article_url): with sync_playwright() as p: browser p.chromium.launch( headlessTrue, args[--disable-blink-featuresAutomationControlled] ) context browser.new_context( user_agentMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, viewport{width: 1440, height: 900}, localezh-CN ) page context.new_page() page.goto(article_url, timeout60000, wait_untilcommitted) try: page.wait_for_selector(#readNum, timeout15000) page.wait_for_selector(#likeNum, timeout5000) read_text page.inner_text(#readNum).strip() like_text page.inner_text(#likeNum).strip() except Exception as exc: print(f[抓取失败] {article_url}: {exc}) read_text like_text None browser.close() return read_text, like_text if __name__ __main__: url https://mp.weixin.qq.com/s/xxxxxxxx read_num, like_num fetch_wechat_article(url) print(阅读数:, read_num) print(点赞数:, like_num)这段代码的逻辑非常简单启动Chromium浏览器打开文章页面等待阅读数和点赞数的DOM元素出现然后用inner_text()把里面的文字取出来。3.2 关键点讲解这几个参数是跑通的核心下面几个点决定了脚本能不能稳定运行。为什么用wait_untilcommittedpage.goto里有个wait_until参数常见的有load、domcontentloaded、networkidle、committed。默认值是load也就是等页面所有资源包括图片、脚本都加载完才返回。但微信公众号文章页面有时候会有一些慢速统计脚本load可能会等很久。所以我用committed意思是“只要请求已经发出、浏览器已经提交导航”就算完成之后再用wait_for_selector去等阅读数元素这个组合更稳。为什么用wait_for_selector而不是sleep大多数页面渲染阅读数是一个异步过程快则几百毫秒慢则几秒。写死sleep(3)的话网络差的时候不够用网络好的时候又白白浪费时间。wait_for_selector会轮询DOM元素一出现就立刻继续配合15秒超时既稳又快。#readNum和#likeNum是固定的吗从我测试过的文章来看这两个ID目前是稳定的分别是阅读数和点赞数容器。但微信偶尔也会微调前端结构如果你发现代码跑通了但拿到的结果是None可以用开发者工具再确认一下页面上阅读数那段HTML的ID是不是变了。3.3 数据清洗把“1.2万”转换成12000细心的话你会发现阅读数超过一定量级后页面上显示的是类似“1.2万阅读”这样的文案而不是12034。这是微信的格式化效果。我们在爬数据时通常希望得到纯数字方便后续统计。写个简单的转换函数def parse_count(text): if not text: return 0 text text.strip().replace(阅读, ).replace(赞, ) if 万 in text: return int(float(text.replace(万, )) * 10000) return int(text)这里有坑如果文本是1.2万直接int()会报错得先转成float再乘10000。另外如果文本里带了“阅读”两个字要先去掉。这个函数比较简单你在实际项目里可以根据页面文案调整比如遇到“10万”这种爆款文案就要额外处理加号。到这里单篇文章的数据抓取已经跑通整个过程不超过5分钟。但实际工作中需求通常不仅仅是抓一篇而是抓几十篇上百篇那就涉及到批量操作和存储了。4. 批量抓取几十篇文章一次搞定4.1 先准备好文章链接列表批量抓取的第一步是拿到所有文章URL。有两种情况。第一种你是文章的作者或管理员可以直接在公众号后台“图文分析”里导出文章链接列表。第二种你想抓一个公众号的所有历史文章URL。这个稍复杂需要用“历史消息”页去翻我会在第六节展开讲。这里我们先假设你已经有一个urls.txt每一行放一篇文章链接https://mp.weixin.qq.com/s/A https://mp.weixin.qq.com/s/B https://mp.weixin.qq.com/s/C4.2 批量循环抓取带随机间隔的完整代码批量代码和单篇非常类似只是外面加了个循环每次抓完后随机休息一下避免请求太密集被平台限制访问import time import random from playwright.sync_api import sync_playwright urls [line.strip() for line in open(urls.txt, r, encodingutf-8) if line.strip()] with sync_playwright() as p: browser p.chromium.launch(headlessTrue, args[--disable-blink-featuresAutomationControlled]) context browser.new_context( user_agentMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, viewport{width: 1440, height: 900}, localezh-CN ) page context.new_page() results [] for idx, url in enumerate(urls, 1): print(f正在处理第 {idx}/{len(urls)} 篇: {url}) try: page.goto(url, timeout60000, wait_untilcommitted) page.wait_for_selector(#readNum, timeout15000) read_text page.inner_text(#readNum).strip() try: like_text page.inner_text(#likeNum).strip() except Exception: like_text 0 read_num parse_count(read_text) like_num parse_count(like_text) results.append({url: url, read_num: read_num, like_num: like_num}) except Exception as exc: print(f [失败] {exc}) results.append({url: url, read_num: 0, like_num: 0}) # 随机休息1-2秒降低被限制的风险 time.sleep(random.uniform(1, 2)) browser.close() print(全部抓取完成共, len(results), 条)这段代码有几个细节值得注意。第一个with sync_playwright() as p这段在整个循环里只启动了一次浏览器而不是每篇文章启动一次。如果你把launch放进循环里那每抓一篇都要启动浏览器、再关闭慢不说还容易被系统判定为异常行为。第二个点赞数的选择器有时候可能匹配不到少数文章点赞功能被关闭所以这里用了try/except抓不到就默认0不让整个流程中断。第三个time.sleep(random.uniform(1, 2))是刻意加的。对于微信公众号这类平台来说限制访问频率是基本操作1到2秒的随机间隔是对双方都友好的节奏。如果你抓的是自己的文章、只抓几十篇这个间隔可以稍微收窄如果抓的是别人公众号的长列表建议间隔加到2到4秒。4.3 结果导出CSVExcel不乱码的小细节数据抓下来总得存起来。最简单的方式是写入CSV用Python标准库就能搞定import csv with open(wechat_article_stats.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[url, read_num, like_num]) writer.writeheader() writer.writerows(results)注意这里编码用了utf-8-sig而不是utf-8。原因是CSV文件在Excel里打开时UTF-8无BOM格式会乱码加个BOM就不会了。这是很多人容易忽略的细节。如果后续要做数据分析可以再把CSV读进pandas生成折线图、柱状图观察阅读数的变化趋势。5. 踩坑记录这些坑我花了三个晚上才填平5.1 headless模式被识别返回验证页我第一次跑脚本的时候用的就是默认的headless模式结果页面加载出来不是文章而是微信的“环境异常”验证页。这是因为headless Chromium的navigator.webdriver属性默认是true很容易被反爬JS检测到。解决办法是args[--disable-blink-featuresAutomationControlled]这个参数可以降低被检测的概率。如果还是不行可以把headlessFalse开一个有界面的浏览器窗口手动操作一次确认能正常访问后再考虑headless优化。实际项目中如果是要在服务器上跑headless几乎是必须的所以这个参数一定要加上。5.2 等待时间不够抓到空值用sleep(2)固定等待的做法非常不可靠。我第一次跑批量脚本的时候前几篇都好好的到后面几篇突然抓回None排查了半天发现是某篇公众号文章里嵌了一个比较大的视频组件网络差点2秒根本不够渲染。改成wait_for_selector(#readNum, timeout15000)之后问题彻底消失。经验总结不要用固定sleep去赌网络一定要用显式等待。绑定了“某个元素出现”这个条件脚本才真正知道自己应该等到什么时候。5.3 频繁切换文章被限制访问早期测试时我连开10篇文章每篇间隔只有几百毫秒结果到第10篇时页面被重定向到了mp.weixin.qq.com/mp/verifypage提示需要“验证”才能继续访问。这就是短时间内请求太密集。解决办法有两个一是把每篇文章之间的间隔拉长比如time.sleep(random.uniform(2, 4))二是加一个User-Agent池用fake_useragent库随机切换UA。不过实测下来间隔策略比UA池更管用。还有一点同一个浏览器实例里连续打开多篇文章比每篇新开一个浏览器实例要安全得多这也是我把browser.close()放在循环外面的原因。5.4 有些文章没有点赞数显示不是所有文章都有点赞数。比如有些企业号发的投票类图文或者被投诉后部分功能被限制的文章页面上可能只有阅读数没有点赞区域。如果代码里不处理这种情况整个脚本会因为page.inner_text(#likeNum)报超时错误而中断。所以在批量脚本里我把点赞数的获取单独包了一层try/except这样单篇文章有问题不会影响整个批次。这也算是一个通用经验批量爬虫里的单条异常绝不应该终止整个任务。5.5 登录态与账号安全公众号文章页面在未登录微信账号的情况下也可以访问阅读数和点赞数也能正常显示。但如果你需要抓取“阅读原文点击量”“分享数”这类更细粒度的数据就必须以运营者身份登录公众号后台了。这里我不展开讲后台爬虫的细节只提醒一点涉及账号密码或cookie的操作务必注意信息安全不要把登录凭证硬编码在脚本里更不要提交到公开仓库。一旦泄露轻则被封号重则影响整个公众号的数据安全。6. 扩展思路从历史文章到数据看板6.1 自动获取公众号全部历史文章链接批量抓取的前提是有一批URL那这些URL怎么来如果目标是某个固定公众号可以通过微信内的“历史消息”入口找到该公众号的历史文章列表页然后用Playwright模拟上拉加载不断采集页面里的文章链接。大体思路在微信客户端内打开目标公众号主页点击右上角“...”进入“历史消息”。在电脑浏览器里打开历史消息页滚动页面每滚动一次列表会加载更多文章。用Playwright的mouse.wheel模拟滚动每滚一次休息1秒然后用选择器采集当前页面所有a[href*mp.weixin.qq.com/s]的链接。去重后保存到urls.txt。示例片段page.goto(https://mp.weixin.qq.com/mp/profile_ext?actionhome__biz..., timeout60000) for _ in range(30): page.mouse.wheel(0, 1000) page.wait_for_timeout(1000) links page.eval_on_selector_all( a[href*mp.weixin.qq.com/s], els els.map(e e.href) )这套逻辑跑通之后就实现了“输入一个公众号主页链接输出这个号所有历史文章阅读数/点赞数”的全流程自动化。注意历史消息页通常要求登录态桌面浏览器里可能无法直接访问必要的时候要先在浏览器里扫码登录微信网页版。6.2 定时监控数据增长阅读数和点赞数是随时间增长的。如果你想做“数据增长曲线”可以在服务器上部署一个定时任务crontab或APScheduler每天固定时间跑一次抓取脚本把结果追加到数据库里。积累一段时间后就能得到每篇文章的阅读增长趋势对判断“什么样的选题传播力更强”非常有价值。简单一点的实现就是用Linux的crontab每天中午12点跑一次Python脚本0 12 * * * cd /path/to/project python batch_fetch.py logs.txt 21在多跑几天后你就可以对比同一篇文章在不同时间点的阅读数增量识别出哪些文章还在长尾增长哪些已经停滞。6.3 与数据分析看板联动抓下来的数据最终要能可视化。我个人的做法是脚本运行完数据写入MySQL或PostgreSQL然后用Metabase或Superset配置一个简单的看板按日期、按文章维度展示阅读量、点赞量。整个过程不涉及任何复杂的BI工具但对运营团队的判断足够实用了。如果你暂时不想上数据库也可以直接用pandas在脚本里做聚合分析快速算一下平均阅读数、最高阅读数、阅读数中位数这些指标。对于日常运营周报来说这些数据已经很有说服力。最后再分享一个我自己的体会爬虫这个东西能做但要有度。公众号的文章数据是公开的你抓自己账号、或者抓别人账号的阅读数做分析只要频率克制、不恶意爆破、不商业滥用问题不大。但要是用爬虫搞营销号矩阵、批量刷量那就越线了。这个脚本的价值在于帮你省掉重复性的手动工作而不是帮你发明一套“黑科技”。希望大家拿到代码后先跑通再根据自己的业务场景改造做出真正有用的工具。
阅读完成 · 觉得有帮助?