WCI指数半自动化统计

学校部门要求计算学校公众号的wci指数,而这首先需要获取公众号一段时间内消息的阅读量、点赞数、在看数、以及是否为头条。学校历年都是让学生人力去统计,我试着人力统计了一下,真是又费时又费力,于是尝试整一个爬虫用来获取这几个指标。

参考文章:爬取微信公众号发布的所有文章(包括阅读数,在看数,点赞数)_爬取微信公众号文章-CSDN博客

工具

微信无法像浏览器一样直接通过控制台查看网络通信,因此需要抓包软件,这里使用的是Fiddler,其他抓包软件,类似burpSuit,应该也可以

关键

本次抓包的关键在于:在哪里可以抓到包?

微信内容的展示分两处,如图

左边的是在微信内置浏览器中展示,而右边的是在微信app的框架中展示

其中,左边展示的内容是可以抓到包的,而右边不行(可能也只是我的技术没到)

图1
此最主要的一点就是如何在左边(微信内置浏览器中)查看公众号的消息列表

如何获取在内置浏览器中打开公众号的消息列表

经过我的实验,所有公众号消息列表的url的格式为

1
https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MzkwMTUxMzEwMw==&uin=&key=&devicetype=Windows+11+x64&version=6309062f&lang=zh_CN&a8scene=7&session_us=

而唯一的差异,在于链接中的参数__biz,只要获取每个公众号自身的__biz,然后根据该__biz重铸其链接,而__biz,只需在打开公众号的任意一篇文章,该文章都会在内置浏览器中打开,并且链接中附带其所属公众号的__biz
图2

根据此值重构链接,然后把链接发送给微信好友,就可以在内置浏览器中打开公众号的消息列表了

图3

然后就可以开始研究抓哪些包了

整理思路

通过fiddler抓取进入消息列表的包,并且通过上下滑动内容使得页面加载更多数据,借此推理出前端的逻辑,抓取到的包如下
图4
通过观察每个包中的内容,可以推理出:

“3”:第一次打开时的整个界面,包括网页框架以及公众号的前十条内容

“7”:作用不详,据其url中的action为“urlcheck”,猜测其是检测该url是否正确

“8”,“38”,“44”,“90”:json文件,其中部分包含文章信息,部分不包含

观察包含文章信息的json文件的url,发现样式比较统一,并且可以直接使用浏览器进行访问 @

图5

抓取多个公众号的多条消息,大致分析出url的如下信息

1.使用GET方法,链接为/mp/profile_ext

2.具有值的参数如下

名称 意义 备注
action getmsg 获取消息 不变
__biz 不固定 公众号id 更换公众号时改变
f json 编码格式 不变
offset 10 偏移量 可不变
count 10 接受消息数量 可不变
is_ok 1 是否完成? 不变
uin 不固定 账户凭证之一? 更换账号后可能要改变
key 不固定 密匙 更换公众号、重新登陆后都要重新设定
pass_ticket 不固定 账户凭证之一? 更换公众号、重新登陆后都要重新设定
appmsg_token 不固定 账户凭证之一? 更换公众号、重新登陆后都要重新设定
x5 0 未知 不变

根据以上信息,只需获取一个公众号消息的原始请求,就可以在配置后使用爬虫获取其所有的历史消息

接着分析这个json文件

图6

可以看到,消息相关的信息在该json文件的general_msg_list,且该general_msg_list为一个list,每一个元素对应一条消息,属性如图
图7

比较重要的有

属性名 意义
content_url 消息链接
title 标题
datetime 时间戳

以上是对于消息列表的处理,接下来是对单个消息的处理,

打开任意一篇公众号的文章,抓包,发现对于点赞、观看、在看数来源于/mp/getappmsgext的链接

图8

这个请求使用的是post方法,在url中的有意义参数有

名称 意义 备注
f json 编码格式 不变
uin 不固定 用户标识 同之前
key 不固定 账户凭证之一 同之前
pass_ticket 不固定 账户凭证之一 同之前
wxtoken 777 位置 固定
devicetype Windows%26nbsp%3B11%26nbsp%3Bx64 使用设备 可固定
clientversion 6309062f 微信版本? 可固定
__biz 不固定 公帐号凭证 同之前
appmsg_token 不固定 账户凭证之一 不固定

由于是post,所有除url的参数外,还需要注意param,参考文章爬取微信公众号发布的所有文章(包括阅读数,在看数,点赞数)_爬取微信公众号文章-CSDN博客中提到的请求设置方式,再加上相关的cookie和data

基于以上信息,就可以开始着手写爬虫了

代码编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import requests
import json
import re
from openpyxl import Workbook
from datetime import datetime
import time

#结果保存在文件同目录下的result.xlsx
#------------------------以下为自定义配置部分---------------
# 时间间隔
starttime = "2023-1-1" #统计开始时间,年月日以-分割
endtime = "2024-1-1" #统计结束时间,格式同上,置空时即视为程序执行时间

# 起始的偏移量,如未理解代码逻辑,则置0即可
offset = 0

#url配置,请将公众号抓到的包的链接复制于此处
#(请确保基础路径是https://mp.weixin.qq.com/mp/profile_ext)
# 此处的链接在浏览器直接打开的结果如图5
all_link = "https://mp.weixin.qq.com/mp/profile_ext?action=getmsg&__biz=MzkwMTUxMzEwMw==&f=json&offset=10&count=10&is_ok=1&scene=&uin=MTEyNOTI4N%3D%3D&key=72871c9ef4aa822373d071fa2aac825d375ec78c6c11ba412f14bbcb2533af3c8b7f6156ec356de127f71e88dd0b64c2af109cee96acefd0544e6aeb52bcb3d74df69647c26b0fcdf3091d3816d66a5f70b977da97c9a7aec5052af9a10d664dba18e9cc1a69c8d7cc4a00f26004bcbe7215c90ecf0ced786113b3b320277f1c&pass_ticket=4EIV4uG5foBxl%2Bm02hTwXzNCw2wJk%2BBqp%2Fi%2BAYopOIgLccQ%2FY52S9WZHwjQ9YThxlxLyDgD2Gs5AvWQJtxzb2g%3D%3D&wxtoken=&appmsg_token=1253_ldTkMaQxVEVucYqTFt6Iamcqsjr6pqOkukOsFg~~&x5=0&f=json"

#---------------若未理解程序代码逻辑,以下部分请勿修改-----------------
#通过all_link获取链接
def get_url(link):
pattern = r'(.+?)&offset=(\d+)(.+)$'
match = re.match(pattern,link)
before = ""
after = ""
if match:
before = match.group(1)
before += "&offset="
after = match.group(3)
return before,after

def get_base_info(url):
__biz = ""
uni = ""
key = ""
pass_ticket = ""
appmsg_token = ""
# 定义正则表达式
regex = re.compile(r'__biz=([^&]+).*?uin=([^&]+).*?key=([^&]+).*?pass_ticket=([^&]+).*?appmsg_token=([^&]+)')

# 匹配正则表达式
match = regex.search(url)

# 提取参数值
if match:
__biz = match.group(1)
uni = match.group(2)
key = match.group(3)
pass_ticket = match.group(4)
appmsg_token = match.group(5)
return __biz,uni,key,pass_ticket,appmsg_token

def get_passage_info(url):
mid = ""
idx = ""
sn = ""
# 定义正则表达式
regex = re.compile(r'mid=(\d+).*?idx=(\d+).*?sn=([^&]+)')
# 匹配正则表达式
match = regex.search(url)
# 提取参数值
if match:
mid = match.group(1)
idx = match.group(2)
sn = match.group(3)
return mid,idx,sn

#TODO:测试变量
complete_num=0
def get_result(url,Cookies,params):
#请求头
headers = {
"Cookie":Cookies,
"User-Agent": "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 NetType/WIFI MicroMessenger/7.0.20.1781(0x6700143B) WindowsWechat(0x6309062f) XWEB/8519 Flue"
}
data = {
"is_only_read": "1",
"is_temp_url": "0",
"appmsg_type": "9",
'reward_uin_count': '0'
}
params["mid"],params["idx"],params["sn"]=get_passage_info(url)

# post请求提交后,将返回的respone转为json
url = "http://mp.weixin.qq.com/mp/getappmsgext"
time.sleep(2) #请求频繁频率 2s:无频繁; 1s:34条后显示频繁;1.5: 35条后显示频繁;1.75s:37条后出现问题
obj = requests.post(url, headers=headers, data=data, params=params).json()
# print(obj)
try_time=1 #用来防止死循环
if not("appmsgstat" in obj) :
print("进行"+str(complete_num)+"条后出现问题")
while((not("appmsgstat" in obj))and try_time<10): #有时会请求频繁导致请求失败则需要先暂停一段时间
print("发生错误,响应结果为:"+str(obj))
print("进程将于20秒后尝试重新请求")
time.sleep(20)
print("第"+str(try_time)+"次尝试")
obj = requests.post(url, headers=headers, data=data, params=params).json()
try_time+=1
if try_time==10:
return "这一条请人工检测","这一条请人工检测","这一条请人工检测"
# 获取到阅读数
read_num = obj['appmsgstat']['read_num']
# 获取到点赞数
like_num = obj["appmsgstat"]["old_like_num"]
# 获取到在看数
reading_num = obj["appmsgstat"]["like_num"]
return read_num,like_num,reading_num

if __name__ == '__main__':
#构建url
# url = baseurl + "&__biz=" + __biz + "&f=json&offset=" + offset + "&count=" + count + "&is_ok=1&scene=&uin=" + uni + "&key=" + key + "&pass_ticket" + pass_ticket + "&wxtoken=&appmsg_token=" + appmsg_token + "&x5=0&f=json"
#url1+offset值+url2即为最终使用url
__biz,uni,key,pass_ticket,appmsg_token=get_base_info(all_link)
pre_url,aft_url = get_url(all_link)

Cookies = "pgv_pvid=9348645722; wxuin=1127839285; lang=zh_CN; rewardsn=; wxtokenkey=777; devicetype=Windows11x64; version=6309062f; appmsg_token="+appmsg_token+"; pass_ticket="+ pass_ticket+"; wap_sid2=CLXs5ZkEEooBeV9ISEwtOXlldXJXMG9HMzhLdzQtLWxvQ3llSmJ4aTFpV0doTXl4VUZ6VklTd3RRNzk1aXh6ancySHc3VkRzVldsbWw2OVQzQ21NaDVzMW5oaEZnRFJNT2luSk10dGFqVU1ITVJQZFN2RTJhczZDVUttYmsxUTZWQTRFUXpndkhUMmdzTVNBQUF+MILUvK0GOA1AAQ=="
params = {
"__biz": __biz,
"mid": "",
"sn": "",
"idx": "",
"key": key,
"pass_ticket": pass_ticket,
"appmsg_token": appmsg_token,
"uin": uni,
"wxtoken": "777"
}

#构建时间戳
start_time_stamp = int(datetime.strptime(starttime,"%Y-%m-%d").timestamp())
if endtime :
end_time_stamp = int(datetime.strptime(endtime,"%Y-%m-%d").timestamp())
else :
end_time_stamp = int(datetime.now().timestamp())

# 创建Excel文件和工作表
workbook = Workbook()
worksheet = workbook.active

# 添加表头
worksheet.append(["offset","标题", "URL", "时间","是否为头条","阅读量","点赞数","在看数"])

flag = True #是否继续循环
# 发送GET请求
while(flag):
url = pre_url + str(offset) + aft_url
print("正在进行的offset为"+str(offset))
# print(data)
time.sleep(1)
response = requests.get(url)

# 解析JSON响应
data = response.json()
if data.get("general_msg_list", "")=="":
print("请求出现错误,可能是url错误或过期")
print("目前offset为"+str(offset))
flag = False
break

# 提取所需数据
msg_list = json.loads(data.get("general_msg_list", ""))
entries = msg_list.get("list", [])
# 如果遍历完了该公众号所有的内容,那么其entries长度就为0
if len(entries)==0:
flag = False
break

# 遍历每个条目并写入Excel
for entry in entries:
app_msg_ext_info = entry.get("app_msg_ext_info", {})
comm_msg_info = entry.get("comm_msg_info", {})

title = app_msg_ext_info.get("title", "")
content_url = app_msg_ext_info.get("content_url", "")
datetime_str = comm_msg_info.get("datetime", "")

# 将datetime字符串转换为datetime对象
if datetime_str:
datetime_obj = datetime.fromtimestamp(int(datetime_str))
tempstamp =datetime_obj.timestamp()
datetime_str = datetime_obj.strftime("%Y-%m-%d")
if tempstamp>end_time_stamp:
print("文章的时间为:"+ datetime_str +",不在要求时间")
continue
if tempstamp<start_time_stamp:
flag = False
break
if content_url!="": #有时候出现一些为0的项,难以理解
read_num,like_num,reading_num = get_result(content_url,Cookies,params)
#添加当日头条
worksheet.append([offset,title,content_url,datetime_str,"是",read_num,like_num,reading_num])
complete_num+=1
workbook.save("result.xlsx")
print(title)
print("时间: "+datetime_str)

#添加非头条
childs = app_msg_ext_info.get("multi_app_msg_item_list",[])
for child in childs:
title = child.get("title","")
print(title)
content_url = child.get("content_url","")
read_num,like_num,reading_num = get_result(content_url,Cookies,params)
worksheet.append([offset,title,content_url,datetime_str,"否",read_num,like_num,reading_num])
complete_num+=1
workbook.save("result.xlsx")
if offset == 10: #莫名其妙地,10后的下一个是21,而不是20
offset +=11
else:
offset +=10
workbook.save("result.xlsx")

结语

通过这个代码可以获得公众号指定时间内发送的所有消息的相关数据,由于wci指数的计算公式在不断更新换代,在此就没有融入,还请读者根据这些数据自行计算。