微信后台很多消息未回复:看到时已经回复不了。有问题可以添加我的微信:菜单 ->联系我
由于最近需要公众号的历史文章信息,所以就尝试爬了一下,虽然目前可以爬到数据,但是还不能够大量的自动化爬取。原因是参数key值具有时效性(具体时间没有验证20分钟的样子),目前也不知道是如何生成的。
文章历史列表爬取
首先先到的是搜狗微信,但是搜狗微信只能看到前十篇文章并且查不到阅读量和在看的数量,尝试爬取手机包,发现没有抓取到信息,后来才知道原因:
1、安卓系统7.0以下,微信信任系统的证书。
2、安卓系统7.0以上,微信7.0一下版本,微信信任系统提供的证书。
3、安卓系统7.0以上,微信7.0以上版本,微信只信任自己的证书。
也尝试过使用appium自动化爬取,个人觉得有点麻烦。所以就尝试抓取PC端的请求。
进入正题,这次抓包使用的是Fiddler。下载链接:https://www.telerik.com/fiddler
Fiddler如何抓包这里不再一一阐述,首先第一次安装Fiddler是需要安装证书才可以抓取HTTPS请求的,
如何安装?
打开Fiddler,从菜单栏找到Tools -> Options -> 点击HTTPS -> 点击Actions 会安装证书 配置成如下:
这里以我自己的公众号为例:在PC端登陆微信,打开Fiddler,按F12是开启/停止抓包,进入公众号历史文章页面,看到Fiddler出现了很多请求,如下图:
由于查看历史记录是跳转到一个新的页面,可以从Body返回较多的看起,同时通过Content-Type也可以知道返回的是css或者html或者js,可以先从html看,于是乎就会找到如上图红色框中的链接,点击他,可以从右边看到返回结果和参数:
从右边的Headers中可以看到请求的链接,方式,参数等,如果想要更清晰的查看参数可以点击WebForms查看,也就是上图展示的结果。这里来描述一下其中重要的参数:
__biz:微信公众号的唯一标识(同一公众号不变)
uin:用户唯一标识(同一个微信用户不变)
key:微信内部算法,具有时效性,目前不知道是如何算出来的。
pass_ticket:是有一个阅读的权限加密,是变化的(在我实际的爬取中发现是不需要的,可以忽略不计)
走到这一步其实已经可以写代码爬取第一页的文章了,但是返回的是html页面,解析页面明显是比较麻烦的。
可以尝试往下滑动,加载下一页数据,看看返回的是json还是html,如果是json就好办,如果还是html,那就只好一点点的解析了。继续往下走会发现:
这个请求就是返回的文章列表,并且是json数据,这就很方便我们去解析了,从参数中发现有一个参数为offset为10,很明显这个参数就是分页的偏移量,这个请求为10加载的是第二页的历史记录,果断修改成0,再发送请求,得到的就是第一页的数据,那么就不需要再去解析html页面了,再次分析参数,发现看着看多参数,有很多一部分是没有用的,最终需要的参数有:
action:getmsg(固定值,应该表示获取更多信息吧)
__biz,uin,key这三个值在上面已经描述了,在这里也是必须的参数
f:json(定值,表示返回json数据吧)
offset:分页偏移量
想要获取公众号的历史列表,这6个参数是必须的,其他的参数可以不用带上。再来分析请求头中的hearders如图:
参数很多,我也不知道那些该带,那些不需要带,最后发现只需要携带UA就可以了,其他都可以不要。最终写出脚本来尝试获取一下:
import requests
url = "链接:http://链接:mp.weixin链接:.qq.com/mp/profile_ext"
headers= {
'User-Agent':'Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Mobile/14A403 MicroMessenger/6.5.18 NetType/WIFI Language/zh_CN'
}
param = {
'action': 'getmsg',
'__biz': 'MzU0NDg3NDg0Ng==',
'f': 'json',
'offset': 0,
'uin': 'MTY5OTE4Mzc5Nw==',
'key': '0295ce962daa06881b1fbddd606f47252d0273a7280069e55e1daa347620284614629cd08ef0413941d46dc737cf866bc3ed3012ec202ffa9379c2538035a662e9ffa3f84852a0299a6590811b17de96'
}
index_josn = requests.get(url, params=param, headers=headers)
print(index_josn.json())
print(index_josn.json().get('general_msg_list'))
获取json对象中的general_msg_list,得到的结果:
获取文章详情
上面已经拿到了链接,请求解析html页面就可以了。这里不再阐述(在全部代码中可以查看)。
获取阅读量和再看量
抓包方式等上面已经说了,在这里就不再废话了
点进文章,滑动到最下方(在快到达底部的时候才会去请求阅读量和再看量),很容易就会捕捉到的请求:
获取阅读量和在看量:
/mp/getappmsgext?f=json&mock=&uin=...(太长了)
获取评论:
/mp.weixin.qq.com/mp/appmsg_comment...
这里我只获取了阅读量和在看量(评论没有去获取但是都是一样的)查看需要的参数:
分析这个请求的参数(这个请求参数真的太多了,心中mmp)发现:
url需要参数:在url中只需要携带uin(用户id)和key值
hearders需要参数:至需要UA
body需要参数:
__biz:公众号唯一标识
appmsg_type:9 (目前来看都是9,必须携带)
mid和sn必须携带,更具这两个参数来判断是那篇文章。
inx:文章的排序,必须携带,对应错获取不到。
is_only_read:1(目前来看都是1,必须携带)
获取阅读量和再看量的代码为:
import requests
# 查询评论接口 重要参数:uin :微信用户唯一ID key:具有失效性的key
url = '链接:https://链接:mp.weixin链接:.qq.com/mp/getappmsgext?uin={你的uin}&key={你的key}
hearder = {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/16D57 MicroMessenger/7.0.3(0x17000321) NetType/WIFI Language/zh_CN',
}
# body参数重要参数:__biz: 微信公众号唯一ID appmsg_type:定值, 必须有
# mid 和 sn 变化值 从上一个页面可以获取 inx 定值 is_only_read 定值
data = {
'__biz': 'MzIwMjM5ODY4Mw==', # 公众号唯一ID 必须
'appmsg_type': '9', # 和 在看 有关 必须
'mid': '2247500578', # 必须 # 不同文章 不同
'sn': 'bcfbfe204ac8d6fb561c6a8e330f4c55', # 必须 和文章有关
'idx': '1', # 必须
'is_only_read': 1, # 必须 和阅读,在看有关
}
index = requests.post(url, headers=hearder, data=data)
print('结果')
print(index.json())
print('在看')
print(index.json().get('appmsgstat').get('like_num'))
print('浏览')
print(index.json().get('appmsgstat').get('read_num'))
最终整理脚本如下
import requests
import json
from urllib import parse
import re
from lxml import etree
import html
import time
headers = {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Mobile/14A403 MicroMessenger/6.5.18 NetType/WIFI Language/zh_CN'
}
articles_url = "链接http:链接//mp.weixin.qq.com链接/mp/profile_ext"
yuedu_url = '链接https:链接//mp.weixin.qq.com/mp链接/getappmsgext'
y_param = {}
param = {
'action': 'getmsg',
'__biz': 'MzIyNTY4MDcxNA==',
'f': 'json',
'offset': 0,
'uin': 'MTY5OTE4Mzc5Nw==',
'key': 'c072b2c2faef4d94fcb6bd27030bdbbb60fc420b14aad30b763f17d4b0e872c5b68bd45fd7392cb9c554e236d16b84310e7ff377e5b3dbdc5732cd8346ea721a3d1c6ef7dc2f2ac0106ac04a6b540948'
}
data = {
'is_only_read': '1',
'appmsg_type': '9'
}
is_bottom = False
def get_articles_list():
'''
获取文章列表
:return: 返回文章列表 list
'''
articles_json = requests.get(articles_url, params=param, headers=headers).json()
if 'base_resp' in articles_json.keys():
print('key值可能失效')
return None
return articles_json
def analysis_articles_list():
'''
解析文章列表参数
获取除 文章,点赞,在看的所有信息
:return: 一个字典
'''
# 获取 10 篇
articles_json = get_articles_list()
articles_info = {}
# 不为空 获取当前文章数 等于0表示没有了
if articles_json and articles_json.get('msg_count') > 0:
# 获取文章列表
articles_lsit = json.loads(articles_json.get('general_msg_list'))
if articles_lsit.get('list'):
for articles in articles_lsit.get('list'):
articles_info['datetime'] = articles.get('comm_msg_info').get('datetime')
if articles.get('app_msg_ext_info'):
articles_info = dict(articles_info, **articles.get('app_msg_ext_info'))
articles_info['is_Headlines'] = 1
yield articles_info
if articles_info.get('is_multi'):
for item in articles_info.get('multi_app_msg_item_list'):
articles_info = dict(articles_info, **item)
articles_info['is_Headlines'] = 0
yield articles_info
else:
global is_bottom
is_bottom = True
def get_articles_digset(articles_info):
time.sleep(5)
content_url = articles_info.get('content_url').replace('amp;', '')
cansu = parse.parse_qs(parse.urlparse(content_url).query)
html_text = requests.get(content_url, headers=headers).text
html_text = etree.HTML(html_text)
html_text = html_text.xpath('//div[@id="js_content"]')[0]
html_text = etree.tostring(html_text).decode('utf-8')
dr = re.compile(r'<[^>]+>', re.S)
wenzhang_text = dr.sub('', str(html_text))
articles_info['text'] = html.unescape(wenzhang_text).strip()
y_param['uin'] = param['uin']
y_param['key'] = param['key']
data['__biz'] = param['__biz']
data['mid'] = cansu['mid'][0]
data['sn'] = cansu['sn'][0]
data['idx'] = cansu['idx'][0]
y_json = requests.post(yuedu_url, headers=headers, params=y_param, data=data).json()
try:
articles_info['read_num'] = y_json.get('appmsgstat').get('read_num', '0')
articles_info['like_num'] = y_json.get('appmsgstat').get('like_num', '0')
except Exception as e:
articles_info['read_num'] = 0
articles_info['like_num'] = 0
print(e)
return articles_info
def insert_data(all_data):
print(all_data)
def get_dime(timestamp):
# 利用localtime()函数将时间戳转化成时间数组
localtime = time.localtime(timestamp)
dt = time.strftime('%Y-%m-%d %H:%M:%S', localtime)
return dt
def main():
# 主入口
for offset in range(1, 1000):
# 分页获取文章列表
if not is_bottom:
print('正在爬取第%d页' % offset)
if offset % 2 == 0:
time.sleep(5)
param['offset'] = (offset-1) * 10
for articles in analysis_articles_list():
articles_info = get_articles_digset(articles)
insert_data(articles_info)
else:
break
if __name__ == "__main__":
main()
还存在的问题
参数uin:用户的唯一id,是不用改变的,问题不大
参数__biz:可以通过搜狗微信获取(通过搜狗微信搜索公众号可以在页面找到__biz)
参数key:问题很大,暂时没办法获取到
但是单独爬取一个公众号(文章不是特别多的时候)时间是够的。我在爬取的途中遇见了443的问题,可能是爬取太快,不知道加上代理ip有没有用(还没有尝试)
既然key要手动修改上去,我就索性没有去搜狗获取__biz。(有兴趣的可以去尝试一下)
key过期怎么办?
用Fiddler从新抓包获取新的key值,替换上去就可以了。
上面的源码复制下来需要把uin,__biz,key值换成自己的,url中由于微信限制,我添加了链接两个字,去掉就好了。