简介:面向微信公开数据抓取与分析的Python实战项目,适合想系统学习爬虫与数据分析的入门及进阶开发者,整套代码结构简洁、可运行性强。压缩包仅3个文件,以Python主程序为核心,另附背景图片和中文字体,总大小2.45MB,轻量易用,已有2323人学习下载。项目从Requests构造请求、BeautifulSoup解析HTML等基础技术入手,逐一讲解验证码识别、IP限制、User-Agent伪装等反爬应对策略,并展示通过正则或pandas清洗数据,再存储为CSV/Excel。后续利用matplotlib绘制朋友圈发布频率、公众号阅读量分布等可视化图表,还能借助jieba分词、TF-IDF与情感分析等NLP手段挖掘微信用户的行为偏好,学习链路完整。同时强调必须遵循微信使用协议与相关法规,避免侵犯隐私,让读者在合法合规的前提下,掌握从数据采集、清洗、存储到分析挖掘的端到端实战技能。
1. 微信爬虫,绕不开的一道门槛是合规
如果你真的在微信生态里做过数据采集,大概率和我一样被教育过:抓包拿数据、模拟点击、个人号协议,这些路要么被封,要么被限制,能稳定跑三个月的几乎没有。随着接口策略收紧,正经做法反而清晰了——微信生态内的数据获取只有一条路能长期运作:官方接口。这篇文章就把我拆过的一套微信生态数据采集完整方案讲透,基于企业微信 API,用 Python 实现从拉取、解密、清洗到分析的全流程。适合要做客服质检、客户活跃分析、社群运营明细的开发者,也适合刚接触微信生态数据、想避开封号雷区的初学者。
2. 三条技术路线,我为什么只押官方接口
2.1 抓包、个人号协议、官方接口的真实边界
早些年做微信爬虫,大家默认的路数是抓包分析客户端和服务器之间的请求,或者直接模拟登录个人微信。这两条条路线在技术上并不难,难的是它们都踩在脆弱的平衡上——微信端的加密协议频繁更新,设备风控一旦判定异常,轻则滑块验证,重则限制登录。个人号协议这条路尤其危险,它不仅要逆向协议,还要处理验证码、设备指纹、心跳保活,维护成本极高,而且随着版本升级随时可能失效。
官方接口则完全不同。企业微信提供的会话存档能力,本质上是把数据获取的合法性前置了:你获取的是自家员工与客户的沟通记录,调用的是腾讯开放的能力,不碰任何未授权的数据边界。用官方接口意味着你的核心精力可以放在数据处理上,而不是每天和协议对抗。
2.2 会话存档的权限模型与数据覆盖范围
企业微信的会话存档并非默认开启。管理员需要在企业微信管理后台配置,让指定成员加入「存档成员」范围,同时为应用开启「会话内容存档」的权限,并且由企业管理员生成一对 RSA 公钥私钥,其中公钥配置给企业微信后台,私钥留在本地服务端。
权限开启后的数据覆盖范围包括:企业内部成员之间的单聊、外部联系人的单聊、包含内部成员和外部客户的群聊。消息类型覆盖文本、图片、语音、视频、文件、链接,以及撤回消息事件。整体覆盖了绝大多数需要做数据分析的场景——客服会话质检、销售话术分析、客户生命周期追踪都在这个范围内。
2.3 加密与私钥:聊天记录不是明文给你的
值得一提的是,所有聊天记录在接口层都是密文。企业微信的加密方案分两层:先由后台生成一个随机对称密钥,用对称密钥加密消息内容;再用企业配置的 RSA 公钥加密这个对称密钥。拉取数据时,本地服务端需要先用自己的 RSA 私钥解密出对称密钥,再解密消息内容。
我在第一次对接的时候忽略了一个关键点:RSA 解密后得到的是对称密钥的明文,但这个明文还带着一个 4 字节的随机长度头,必须跳过头部的 4 字节再读取剩余部分,才能真正拿到用于 AES 解密的密钥。这个细节如果漏了,解密结果永远是乱码。
3. 搭建采集层:从 0 到 1 拉取会话数据
3.1 获取 access_token:统一入口与租户隔离
企业微信接口统一使用 access_token 作为调用凭证。我们需要创建自建应用,拿到 corpid 和 corpsecret,通过 gettoken 接口换区 token。token 的有效期是 7200 秒,但实际使用中建议提前 5 分钟刷新,而且要避免每次调用都重新获取——频繁请求 gettoken 接口会对应用限流。
import requests import time class TokenManager: def __init__(self, corpid: str, corpsecret: str): self.corpid = corpid self.corpsecret = corpsecret self.access_token = None self.expires_at = 0 def get_token(self) -> str: # 如果当前 token 还有 5 分钟以上有效期,直接复用 if self.access_token and time.time() < self.expires_at - 300: return self.access_token url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken" params = { "corpid": self.corpid, "corpsecret": self.corpsecret, } resp = requests.get(url, params=params, timeout=5) data = resp.json() if data.get("errcode") != 0: raise RuntimeError(f"gettoken failed: {data}") self.access_token = data["access_token"] self.expires_at = time.time() + data["expires_in"] return self.access_token这段代码背后的逻辑不复杂,核心是expires_at - 300这个缓冲期。token 的刷新机制是幂等的,也就是说重复刷新不会导致旧 token 立即失效,但为了保险,请求层最好加一个重试机制。如果发现返回errcode 40014或42001,在重试前需要强制清除本地缓存 token。
3.2 增量拉取:用 seq 游标兜住数据窗口
会话存档接口的数据拉取不是按时间戳分页的,而是通过seq游标从最新的一条增量记录往前拉。每次调用传入limit(最大 1000 条)和seq,接口返回这批消息的密文,以及下一次请求要用的seq。
这里最容易犯的错误是用max_id或时间戳去代替 seq,结果就是重复拉取或漏消息。seq 是单调递增的,只有它才能保证消息不重不漏,所以务必备份在本地存储。
def fetch_room_messages(token: str, seq: int, limit: int = 1000) -> dict: url = "https://qyapi.weixin.qq.com/cgi-bin/msgaudit/get_room_msg" params = {"access_token": token} body = { "seq": seq, "limit": limit } resp = requests.post(url, params=params, json=body, timeout=10) data = resp.json() if data.get("errcode") != 0: # 常见的错误是 600020:seq 非法或超过范围 raise RuntimeError(f"fetch room msg failed: {data}") return datalimit官方上限是 1000,实测建议设置为 500,原因是单次拉取的量越大,解析和解密耗时越长,容易触发网关超时。正确姿势是维护持久化的 seq 值(比如存 Redis 或 SQLite),每次拉取完成后原子更新 seq。
3.3 解密与结构化:把密文变成干净的 DataFrame
密文解密的过程可以拆成三步:用 RSA 私钥解密出对称密钥 → 用对称密钥 AES-CBC 解密消息体 → 解析 JSON 得到消息内容。完整代码如下,可以直接抄:
import json import base64 from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes def decrypt_message(rsa_private_key: bytes, encrypt_key_b64: str, encrypt_msg_b64: str) -> dict: from cryptography.hazmat.primitives.serialization import load_pem_private_key # 1. RSA 解密会话密钥 enc_key = base64.b64decode(encrypt_key_b64) private_key = load_pem_private_key(rsa_private_key, password=None) aes_key = private_key.decrypt( enc_key, padding.PKCS1v15() ) # 真正用于 AES 的密钥必须跳过前 4 字节的随机长度头 real_key = aes_key[4:] iv = real_key[:16] key = real_key[16:32] # 2. AES-CBC 解密消息体 cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) decryptor = cipher.decryptor() msg_bytes = decryptor.update(base64.b64decode(encrypt_msg_b64)) + decryptor.finalize() # 3. PKCS7 去填充(企业微信用的是标准 PKCS7) pad_len = msg_bytes[-1] msg_bytes = msg_bytes[:-pad_len] return json.loads(msg_bytes.decode("utf-8"))注意aes_key[4:]这一行,我在第一次实现时跳过了这个头部,折腾了整整一个下午,解密结果全是乱码。企业微信在 RSA 加密时,会在最前面塞入一个随机长度,这个坑最容易踩,却没有被写进多数示例代码里。另外,AES-CBC 的 IV 是取解密后密钥的前 16 字节,密钥本身是后 16 字节,这一点也和常规的对称解密不同——严格来说,企业微信的 AES 加密是自己定义密钥生成规则,不能直接用通用的解密函数。
4. 数据清洗与存储:让原始数据变成可用资产
4.1 去重与乱序处理是第一步
拉取到的消息在时间上不一定有序。由于会话存档是分布式架构,同一条消息可能在 seq 不同的批次里重复出现,也可能先返回后发生、后返回先发生的消息。在这个阶段,数据清洗的核心是去重和排序。
import pandas as pd def clean_messages(raw_messages: list) -> pd.DataFrame: df = pd.DataFrame(raw_messages) # 消息唯一标识:msgid + 发送人 + 发送时间 df["dedup_key"] = df["msgid"].astype(str) + "_" + df["from"].astype(str) df = df.drop_duplicates(subset="dedup_key", keep="first") # msgtime 是时间戳,统一转成 datetime df["msgtime"] = pd.to_datetime(df["msgtime"], unit="s") df = df.sort_values("msgtime").reset_index(drop=True) return dfdedup_key的选择很关键。企业微信的消息唯一标识是msgid,但同一个 msgid 可能在群聊转发场景下对应不同的from用户,所以需要拼接from字段来保证唯一性。排序用msgtime而非seq,因为seq是拉取顺序,不代表聊天顺序。
4.2 会话维度聚合:把消息分组为对话
分析聊天记录时,多数场景不关心单条消息,而关心对话层面的特征:客户等待时长、会话时长、客服响应速度。这需要按 roomid 和参与人组合做聚合。
def aggregate_sessions(df: pd.DataFrame) -> pd.DataFrame: df = df.sort_values(["roomid", "msgtime"]) # 群聊和单聊区分处理 group_cols = ["roomid"] # 如果只有两个人参与,可以赋一个会话维度;多人群聊则需要按 msgtype 聚合 agg = df.groupby(group_cols).agg( first_msg_time=("msgtime", "min"), last_msg_time=("msgtime", "max"), msg_count=("msgid", "count"), direction_ratio=("from", lambda x: 0) # 实际开发用自定义函数 ).reset_index() agg["session_duration_sec"] = (agg["last_msg_time"] - agg["first_msg_time"]).dt.total_seconds() return agg注意direction_ratio这个字段,实际中需要根据内部成员名单逐条判断消息的发言方向,不能直接用列名简单聚合。基础思路是先维护一个内部成员 ID 集合,然后判断每行消息的from是否在集合内,从而统计客户发言占比。这个指标在客服质检中是核心指标,能直接反应客服是否过度冗长。
4.3 存储选型:优先 SQLite,不用急着上重型数据库
如果消息量在每日几万条以内,SQLite 是最务实的方案。它天然支持事务,适合增量写入,也能直接跑 SQL 做分析,还不需要额外的服务端。但如果消息量到了每日百万条级别,建议把原始消息和聚合结果分离存储:原始消息扔进 ClickHouse 或 MySQL,聚合结果放 Redis 供查询。
import sqlite3 def init_db(db_path: str) -> None: conn = sqlite3.connect(db_path) conn.execute(""" CREATE TABLE IF NOT EXISTS raw_messages ( msgid TEXT PRIMARY KEY, roomid TEXT, from_id TEXT, msgtime TIMESTAMP, msgtype TEXT, content TEXT, seq INTEGER ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS seq_store ( key TEXT PRIMARY KEY, value INTEGER ) """) conn.commit() conn.close()seq_store这个表很多人会忽略。如果你不持久化 seq,重启服务后会从 0 开始拉,接口会直接返回错误,而且会丢失中间的消息。我一般会在每次拉取完成后立刻UPDATE seq_store SET value = ? WHERE key = 'last_seq',再在同一事务里写入消息数据,保证重启后的连续性。
5. 常见问题排查:五个让我夜里爬起来看日志的坑
5.1 access_token 并发刷新导致 401 报错
现象:多个任务同时启动时,部分请求返回errcode 40014或者42001,且刷新 token 后仍然报错。
原因:token 在并发场景下被多个线程同时刷新,旧 token 被新 token 顶掉,而部分请求仍持有旧 token 在途。
解决:加一个全局锁,只允许同一个进程内单个线程去刷新。进程内带上一个 5 分钟的过期缓冲,跨进程场景则要用 Redis 分布式锁。我在项目里就是简单粗暴地在TokenManager上加threading.Lock()包住整个刷新逻辑,问题就地解决。
5.2 接口返回 600020,seq 超过接口可拉范围
现象:拉取会话日志时,某个 seq 对应的批次报错说超过最大范围,后续消息全部拉不到。
原因:企业微信的会话存档只保留最近 3 天的数据,如果 seq 太旧说明数据已经过窗,无法再补拉。
解决:把数据拉取做成常驻的周期任务,每 5 分钟拉一次。如果发现进程长时间宕机,恢复后不要从宕机时间往前找,而是拉取最近一小时的数据做重放,否则等窗口过了数据就直接丢失了。吃过这个亏之后,我把历史备份这个环节做成了强制流程:消息落地先备份原始密文,再走清洗。
5.3 解密失败,返回openssl相关错误
现象:RSA 私钥加载失败,日志信息显示Invalid PEM或者长度不足。
原因:部分同学把private_key.pem文件直接复制到服务端,但换行符或文件编码不一致,PEM 格式校验不过。
解决:PEM 文件按utf-8读取,并去掉首尾的额外空白。在本地强推统一用-----BEGIN PRIVATE KEY-----格式,而不是老的PKCS#1格式,加载顺序先load_pem_private_key,异常时再尝试 PKCS8 转换。
5.4 图片消息下载报 720001 错误
现象:调用媒体文件下载接口,返回720001指令参数错误。
原因:图片、语音这类媒体文件,在消息体中带的是一个sdkfileid,不是直接的文件 URL。需要先调用媒体下载接口拿到解密后的文件内容,而这个接口要求传入msgid以及sdkfileid,两个字段缺一不可。我一开始只传了sdkfileid,白写了一次接口文档。
解决:把msgid字段纳入媒体文件拉取函数的必传参数,并且在拿到媒体内容后,仍然需要用前面解密出来的real_key做一次 AES 解密,这一步很多现有文章都没提,是媒体文件拉取真正的关键路径。
5.5 群聊消息归属错误:内部成员识别失败
现象:统计某个群的客户发言占比时,发现数据明显异常,内部员工被算成客户。
原因:群聊里的消息from字段是 userid,而客户的标识是 external_userid。在清洗时,必须用成员名单区分这两类 ID 的命名空间,否则简单用列表判断会全盘跑偏。
解决:在聚合阶段引入成员表,先从企业微信通讯录接口拉取当前所有成员的 userid 列表,落库后作为白名单。外部联系人检测时,用external_userid的 ID 前缀进入特定逻辑分支。这套判断逻辑要提前设计好,不能在数据清洗阶段再打补丁。
6. 把聊天记录变成时间序列:三个值得先做的分析
6.1 基于小时窗口的咨询峰值分布
拿清洗后的消息表,按date + hour做聚合,能直接看到一天内的咨询高峰。这个分析在做客服排班时很有说服力,因为数据其实是准确的。代码实现上就是用 pandas 做groupby("msgtime.dt.hour"),接下来把每个小时的客户发言数和客服首次响应时间差拉在一张表里,直接对比。
6.2 用响应时长标注服务的「慢节点」
响应时长的计算不能只看相邻两条消息的时间差。要标注当前会话中第一条客户消息到第一条客服回复之间的间隔,这个指标才叫「首次响应时长」。把聚合表和原始消息做 join,过滤掉客户多条连续发言的场景,得到的结果才有业务解释力。我一般会把大于 5 分钟的会话抽出来,看是集中在哪个时间段,这才是改进客服流程的直接依据。
6.3 把分析脚本固化为一套可重复运行的工程
爬虫和分析分开跑很不健康。我现在的习惯是每次拉取完数据,自动触发一个make_report脚本,产出三张表:小时活跃表、会话质量表、消息类型分布表。这样每天晚上 12 点跑到第二天早上,数据自动汇总。从那以后我每次接触新的微信生态数据项目,都会强制走一遍「官方接口拉取 → 密文落地 → 清洗去重 → 聚合分析」的完整闭环,不再去碰抓包那套短命方案。希望这套经验能让你在合规的前提下少走弯路,把精力留在真正有价值的数据分析上。
本文还有配套的精品资源,点击获取