这几天师父有个小项目,挺有意思,如何使用python爬微信公众号中的新闻信息。大体流程如下。图1:流程
其实我们看到,这里并没有想象中的“智能”——依然需要手动刷公众号文章,然后才能够收集到信息。(误:更新的第9部分是更加智能的操作,减少手刷)需要用到的工具:Python,Fiddler(附上下载地址)https://www.telerik.com/fiddlerwww.telerik.com具体操作如下
1. 电脑下载fiddler图2:下载fiddler
2. 安装之后,点开第一眼看到的是这样图3:fiddler第一次点开之后
这里附上fiddler的介绍。https://www.cnblogs.com/zhaoyanjun/p/7068905.htmlwww.cnblogs.com
3. 设置由于我们只抓手机浏览信息,所以这里选择 from remote clients only(弹出来的提示都选Yes)。图4:设置Tools-Options-HTTPS
然后设置Actions:点击Actions,选择Trust root certificate以及export root certificate to desktop(弹出来的提示都选Yes)。图5:设置Actions由于是手机连接代理,所以勾选allow remote computers to connect ,记住8888这个数字(不同的电脑可能不同,弹出来的提示都选Yes)。
图6:设置Tools-Options-Connections关闭Fiddler
4. 手机设置(我使用红米手机,其他手机大致一样)找到IP地址,电脑cmd中输入ipconfig,找到自己的ip地址,例如我的IP地址是192.168.124.14
手机连接和电脑一样的WiFi;
设置手机WiFi(手机“设置”中),在手机中修改“代理”的“主机名”(电脑中的ip地址192.168.124.14)以及“端口”(来自图6的fiddler listens on post的数字8888)。图7:设置手机WiFi代理(proxy)在手机浏览器中输入网址:192.168.124.14:8888(主机名+Port),前往下载FiddlerRoot Certificate。
图8:手机进入网址192.168.124.14:8888
图9:点击FiddlerRoot Certificate
图10:下载之后安装它,随意命名,我命名为“Fiddler2”
5. 重启电脑的Fiddler,手机点开公众号文章,电脑Fiddler收集信息图11中的序号5~20是我在手机点开微信公众号的文章之后、电脑Fiddler记录的信息。其中HTTPS的信息最为关键。如果没有这类信息的记录,建议返回上面看看,看看是哪一步出错了。图11:记录分析某一条记录观察图12 的序号16,左边查看“json”+“HTTPS”+“mp.weixin.qq.com”的提示信息,右边查看“inspectors”+“headers”中的“miscellaneous”中的“refrerer”中的网址。这个网址就是微信公众号的最终链接,也是我们要收集的链接。
图12:具体分析可以复制这条链接,在网页中打开,的确是刚刚在手机打开的公众号的文章。
图13:复制Fiddler记录的链接,在浏览器中点开过滤注意观察到,微信公众号所有的信息都是以“mp.weixin.qq.com”开头,因此这里通过Filter将其单独过滤出来。
图14:过滤重启Fiddler,在手机中不断点开公众号的文章,fiddler记录如下。
图15:过滤之后的信息
可以看到,序号存在着跳跃,因为过滤起到了作用。
6. 将所有信息导出全选之后,右击,选择“Save”-“selected sessions”-“as text”图16:导出手机浏览记录该txt中记录了所有的浏览网址信息,建议另存为utf-8格式,便于Python读取。
图17:txt信息
7. Python抽取公众号信息抽取txt中的链接
import numpy as np
data = []
with open(r'...\1_Full.txt', 'r', encoding='utf-8') as fp:
for line in fp:
if 'Referer: https://mp.weixin.qq.com/' in line: //将含有重要信息的链接保留到data中
data.append(line[9:])
// 去重
data = np.unique(data)网页结构抓取,待完善
8. 通过电脑微信客户端抓取公众号的信息
在一遍一遍刷手机之后,本人务必厌烦。。如果能够通过鼠标点击电脑为内心客户端,然后通过fiddler收集信息,那么就不用刷手机了。。注意,在调整fiddler的时候,anaconda的jupyter关闭(可以使用spyder),否则fiddler会出问题。
操作差不多。
首先,将fiddler-Tools-Options-HTTPS,将Decrypt HTTPS traffic修改为“from all processes”.图18:电脑收集微信公众号的操作
然后,同样在自己的浏览器中,输入IP地址+8888,下载证书。图19:下载FiddlerRoot证书
下载之后进行安装。图20:安装证书
其他设置filter和上面手机设置一样,都是把关于wp.weixin的内容筛选出来。
然后,刷电脑端微信公众号,那么filter就能够记录下所有的公众号文章。注意,一旦打开fiddler,那么电脑无法访问其他网页,因为百度等防爬机制很严格,会检测到fiddler已经启动。
9. 更加自动和智能的操作
无论是刷手机收集信息,还是通过电脑端刷公众号,依然是需要人点击信息,不够智能。这里在参考了新的案例之后,能够进行颠覆性的改进
首先,本文前面的模块依然需要了解。当已经能够在电脑端刷微信公众号的文章、同时fiddler能够收集https的信息,那么继续往下。以“首都之窗”微信公众号为例。
(1)电脑微信端的操作
打开fiddler。
点击设置-通用设置-使用系统默认浏览器打开网页。图21:电脑微信端设置
然后,随意点击“首都之窗”的任意一篇文章,会在浏览器中弹出来。放在那里,不用理会。顺便把fiddler中记录的这个文章信息删了。留着fiddler空白,记录第25图的重点内容!
这一步的目的是为了能够顺利在浏览器中打开公众号的历史消息并且刷新。图22:先点一篇文章
图23:该文章在浏览器弹出来
图24:完整操作
接着,进入“首都之窗”公众号,点击查看历史消息。图25:查看历史消息
同样,“历史消息”在浏览器(绝不能在微信客户端下拉、因为fiddler收不到信息)中弹出来,然后往下开始刷几下,需要看到有新的内容弹出来,同时看到fiddler正在记录更新的信息。fiddler更新的消息就是最重要的内容。图26:在浏览器中下拉几次“历史消息”
(2)fiddler信息分析
刚刚通过在浏览器下拉公众号历史消息,fiddler采集到了更新的信息。我们开始分析。图27:分析因为下拉历史消息而收集到的某一条记录
选择第8条记录(该记录来自浏览器中下拉历史记录而收集到的消息),重点部分已经在headers中圈出来了。
(3)链接分析(看不下去的话,直接看代码怎么拼出链接)
分析这个链接。可以看到,它是由几个部分组成。
①http://mp.weixin.qq.com/mp/profile_ext?②action=getmsg ③&__biz=MzA5NDY5MzUzMQ== ④&f=json⑤&offset=20 ⑥&count=10 ⑦&is_ok=1 ⑧&scene=124 ⑨&uin=777 ⑩&key=777 ⑪&pass_ticket= ⑫&wxtoken= ⑬&appmsg_token=1052_D6g2L7mM%252BaKLoVQK33V8q4D4wk3doi7QeR3Zog~~⑭&x5=0&f=json HTTP/1.1
那么我们需要关注的信息是:
③__biz:公众号的id(公众号的biz唯一),⑤offset:翻页标志,⑬appmsg_token:某个有时效性的token(隔一段时间会变化)
我们再看下面几个链接
GET /mp/profile_ext?action=getmsg&__biz=MzA5NDY5MzUzMQ==&f=json&offset=40&count=10&is_ok=1&scene=124&uin=777&key=777&pass_ticket=&wxtoken=&appmsg_token=1052_D6g2L7mM%252BaKLoVQK33V8q4D4wk3doi7QeR3Zog~~&x5=0&f=json HTTP/1.1
GET /mp/profile_ext?action=getmsg&__biz=MzA5NDY5MzUzMQ==&f=json&offset=60&count=10&is_ok=1&scene=124&uin=777&key=777&pass_ticket=&wxtoken=&appmsg_token=1052_D6g2L7mM%252BaKLoVQK33V8q4D4wk3doi7QeR3Zog~~&x5=0&f=json HTTP/1.1
biz和appmsg_token一致,offset改变,即为新的一页。因此,第一步,我们已经找到了翻页的规律。链接中只有这三个在变化,其他没有变动。因此,链接在python中能够写成:
api = 'https://mp.weixin.qq.com/mp/profile_ext?action=getmsg&__biz={0}&f=json&offset={1}&count=10&is_ok=1&scene=124&uin=777&key=777&pass_ticket=&wxton=&appmsg_token={2}&x5=0&f=json HTTP/1.1'.format(
__biz, offset, appmsg_token)
(4)cookie和headers
cookie保存的是微信登录的信息,在爬虫的时候需要填进去。我们只要关注wsp_sid2的cookies信息。
cookies同样来自图27。找到wap_sid2=CK6vyK4CElxLdmda............
headers同样来自图27。找到 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"
(5)爬取
好的,以上找到了很多信息。初步的python如下:
import requests
import json
# 链接拼接三个信息
__biz = "MzA5NDY5MzUzMQ=="
appmsg_token = "1052_D6g2L7mM%252BaKLoVQK33V8q4D4wk3doi7QeR3Zog~~"
offset = 20
# cookies和headers
cookies = "wap_sid2=CK6vyK4CElxLdmda......."
headers = {'Cookie':cookies,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"
}
# api拼出来
api = 'https://mp.weixin.qq.com/mp/profile_ext?action=getmsg&__biz={0}&f=json&offset={1}&count=10&is_ok=1&scene=124&uin=777&key=777&pass_ticket=&wxton=&appmsg_token={2}&x5=0&f=json HTTP/1.1'.format(
__biz, offset, appmsg_token)
# 抓取并且json化
resp = requests.get(api, headers = headers, verify=False).json()
为什么需要把resp进行json化?我们可以尝试着打开9(3)一开始的链接的网页,图28:拼出来的api打开的网页长这样
很明显这是个json信息。因此需要json化。复制该网页的全部文本,放在http://json.cn网页中,可以看到完整的json结构。这就是resp的网页结果。图29:把图28的文本信息复制放在json.cn中的结果
那么resp在python中被json化之后的结果如下图30:resp在python中的结构
(6)网页解析
接下来,关注该resp的结构以及一层一层分析。图30中resp中errmsg=ok和ret=0,均表示网页可以正常打开(如果报错的话,ret=-3)。next_offset是下一次翻页的标志,需要保存起来。
next_offset = resp.get('next_offset')
general_msg_list = resp.get('general_msg_list')
# 将general_msg_list转为json格式
msg_list = json.loads(general_msg_list)['list']
general_msg_list 是 重要的内容。点击general_msg_list,这依然是一个json结构。图31:general_msg_list依然是json结构。
复制里面的文本,放到http://json.cn中看看是什么。图32:general_msg_list放到json.cn中的结果
因此被json化之后的msg_list,在python中长这样图33:把general_msg_list进行json化在python中的样子
可以看到,msg_list中含有10个记录。我们抽出一个记录,进行具体分析。在分析之前,我们要明确一个东西。msg_list中包含了10个记录,不是指10篇文章,而是10次推送。某一次公众号推送消息,可能同时发布好几条文章,也有可能是一篇文章。因此,要明白,单个msg记录,是指一个推送(and可能一次性发布了好几篇文章)。图34:某一次推送,一起发布了3篇文章
msg = msg_list[0]图35:某一个具体的msg
该msg里面包含了“app_msg_ext_info”和“comm_msg_info”两个内容。在http://json.cn中,这两个内容分别长这样子。图36:某一个msg具体的两部分---app_msg_ext_info和comm_msg_info
那么comm_msg_info包含了该推送的基本信息:推送ID,时间等。
app_msg_ext_info是什么?且听我慢慢分析。首先,title,digest一直到is_multi,都是该次推送的打头文章(就是图34中带图片的那个文章的信息)。例如title标题/digest关键词/content_url链接/source_url原链接等。
is_multi是判断该次推送是不是有读篇文章;=1表示yes,=0表示no。那么这里等于1,说明该次推送还有其他文章,存在于multi_app_msg_item_list中。
把multi_app_msg_item_list取出来。
multi_app_msg_item_list = app_msg_ext_info.get('multi_app_msg_item_list')图37:该推送剩下的两篇文章藏在multi_app_msg_item_list中
到目前为止,我们已经分析完了整体的流程。
总结图38:总结如何走出第一步
图39:具体分析结构
(7)具体代码如下
import requests
import json
from datetime import datetime
import pandas as pd
import time
class WxMps:
def __init__(self, biz, appmsg_token, cookies, offset, city):
self.biz = biz
self.msg_token = appmsg_token
self.offset = offset
self.headers = {'Cookie':cookies, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
}
self.city = city
def parse1(self, resp):
# 控制下一个抓取的offset
offset = resp.get('next_offset')
# 将包含主要内容的list转为json格式
general_msg_list = resp.get('general_msg_list')
# 一个msg_list中含有10个msg
msg_list = json.loads(general_msg_list)['list']
df1 = pd.DataFrame(columns = ['msg_id', 'post_time', 'msg_type', 'title', 'cover', 'author', 'digest', 'source_url', 'content_url'])
# 循环message列表
for msg in msg_list:
# msg是该推送的信息,包含了comm_msg_info以及app_msg_ext_info两个信息,注意某一个推送中可能含有多个文章。
comm_msg_info = msg.get('comm_msg_info')
app_msg_ext_info = msg.get('app_msg_ext_info')
# 该推送的id
msg_id = comm_msg_info.get('id')
# 该推送的发布时间,例如1579965567需要转化为datetime,datetime.fromtimestamp(1579965567)
post_time = datetime.fromtimestamp(comm_msg_info['datetime'])
# 该推送的类型
msg_type = comm_msg_info.get('type')
if app_msg_ext_info:
# 推送的第一篇文章
title, cover, author, digest, source_url, content_url = self.parse2(app_msg_ext_info)
df2 = self.df_process(msg_id, post_time, msg_type, title, cover, author, digest, source_url, content_url)
df1 = pd.concat([df1, df2])
# 判断是不是多篇文章
is_multi = app_msg_ext_info.get("is_multi")
# 如果是1,继续爬取;如果是0,单条推送=只有一篇文章
if is_multi:
multi_app_msg_item_list = app_msg_ext_info.get('multi_app_msg_item_list')
for information in multi_app_msg_item_list:
(title, cover, author, digest, source_url, content_url) = self.parse2(information)
df2 = self.df_process(msg_id, post_time, msg_type, title, cover, author, digest, source_url, content_url)
df1 = pd.concat([df1, df2])
return df1, offset
def start(self):
offset = self.offset
df1 = pd.DataFrame(columns = ['msg_id', 'post_time', 'msg_type', 'title', 'cover', 'author', 'digest', 'source_url', 'content_url'])
while offset <= 30: # 自行修改
print(offset)
api = 'https://mp.weixin.qq.com/mp/profile_ext?action=getmsg&__biz={0}&f=json&offset={1}&count=10&is_ok=1&scene=124&uin=777&key=777&pass_ticket=&wxton=&appmsg_token={2}&x5=0&f=json HTTP/1.1'.format(
self.biz, offset, self.msg_token)
resp = requests.get(api, headers = self.headers, verify=False).json()
time.sleep(10)
# ret 和 status 判断能不能继续抓取
ret, status = resp.get('ret'), resp.get('errmsg')
if ret == 0 or status == 'ok':
print("Crawling : ok")
print("\n")
print("\n")
df2, offset = self.parse1(resp)
df1 = pd.concat([df1, df2])
df1['city'] = self.city
else:
print("Before break, offset with problem is%d"%offset)
break
return df1
def parse2(self, info):
title = info.get('title')
cover = info.get('cover')
author = info.get('author')
digest = info.get('digest')