抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

前面一篇博客讲了requests、Xpath和selenium的用法,最后用selenium模拟浏览器对搜狗微信文章做了自动化爬取。从搜狗微信网页爬取的公众号文章其实是不全的,不能保证公众号的所有文章都被搜狗收录,且selenium爬取速度相对较慢(但是对动态页面爬取很有用),因此可以选择另一种方式——直接从微信公众号后台进行爬取。

这两天改了下代码,就讲一讲从微信公众号后台爬文章的思路。

1. 准备工作

首先是申请微信公众号,自从2018年微信公众号加强用户管理以后,一个身份证只可以注册一个订阅号了,除非你有营业执照,以公司为主体注册名额还能加两个。比较建议多弄几个微信公众号,只要绑定自己是运营者就行,可以让朋友帮忙注册一下,从微信公众号后台直接爬是有可能被ban接口的,被反爬机制检测到第一次ban一小时,第二次可能ban一天,看情况而定。

我这里是准备了三个微信公众号,保证爬取过程不中断~

首先进入微信公众号后台,点击图文消息,在跳转的编辑页面上方点击超链接

在这个页面按F12进入开发者工具,链接内容选择其他公众号,输入你想要爬的公众号名字,点击右边放大镜搜索后对返回数据抓包。

这里第一个返回的数据包是显示公众号搜索内容的,一个重要的参数fakeid就是公众号名字的内部编号。然后返回前面的标头,获取cookie,这是我们登录微信公众号的凭证,后面爬取网页必须带上cookie内容。

点击我们要找的公众号(你名字输对的话肯定是第一个),又返回一个数据包,在负载里我们可以看到begin和count两个重要的参数。在试验过后可以发现,begin表示从哪一页开始,count表示一页显示多少天的推送,这里count值在我多次试验之后,发现最大值为5,传入超过5的数都会变成默认值5也就是说不能通过一页获取所有文章的url)!

而在响应体中,我们可以看到所有返回文章的title、link、update_time、digest等重要的信息都在app_msg_list中,上面的app_msg_count值我测试后发现是记录一共发布文章天数的。

在这个公众号例子中,digest本来应该是摘要的,但在这里只是一段甚至半段内容,无法提取有用的信息,所以我直接忽略了这部分数据;而且一般公众号会把subtitle分离出来,这个公众号没有,因此需要写一段代码分离标题中的分类标题,以标题中的竖线来分割“副标题(分类)|标题”。

接下来可以随便点一个link,F12看看文章的html结构,记录文章内容的xpath地址(不记得xpath地址怎么找的话,看前一篇xpath用法)。这里文章的图片我就没有收集了,我只收集了文字部分内容。

每个公众号排版不一样,根据内容再写一个正则匹配一下不需要的内容,也就是对内容“去噪”。不详细讲,因公众号而异,我这里要去除的噪声是公众号底部的进群邀请内容。

2. 代码部分

截至目前为止(2022年12月28日),代码运行正常:

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
import random
import time
import requests, re
from requests.packages import urllib3
from lxml import etree
import xlwt
from pandas import DataFrame

key_word = "植物生物技术Pbj"
xpath_string = '//*[@id="js_content"]//text()' # 文章内容的xpath路径
last_date = 2018 # 想要获得哪一年之后的文章
# 创建工作表格,存储爬取的临时数据
book = xlwt.Workbook(encoding='utf-8',style_compression=0)
sheet = book.add_sheet(key_word,cell_overwrite_ok=True)
col = ('title', 'author', 'content','category',"link","date")
for i in range(0,6):
sheet.write(0,i,col[i]) # 第一行写入属性名称,write对应参数:行、列、值

urllib3.disable_warnings() # 忽略警告
# 最终爬取数据存放列表
title_list = []
link_list = []
cat_list = []
date_list = []
content_list = []
author_list = []

s = requests.Session() # 维持会话

# 对微信公众号查找的headers
headers = {
'User-Agent': "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0",
"Host": "mp.weixin.qq.com",
'Referer': 'https://mp.weixin.qq.com/'
}
cookie_str = ""
cookies = {}

# 加载cookies,将字符串格式的cookies转化为字典形式
def load_cookies():
global cookie_str, cookies
for item in cookie_str.split(';'):
sep_index = item.find('=')
cookies[item[:sep_index]] = item[sep_index + 1:]

# 去噪函数,只适合该公众号
def quzao(content):
if type(content) == str:
i = re.sub('植物生物技术Pbj交流群', '', str(content))
i = re.sub('为了能更有效地帮助广大的科研工作者获取相关信息.*', '', str(i))
return i
else:
return ' '

# 爬虫主函数
def spider():
# 加载cookies
load_cookies()
# 访问官网主页
url = 'https://mp.weixin.qq.com'
res = s.get(url = url, headers = headers, cookies = cookies, verify = False)
if res.status_code == 200:
# 由于加载了cookies,相当于已经登陆了,系统作了重定义,response的url中含有我们需要的token
print(res.url)
# 获得token
token = re.findall(r'.*?token=(\d+)', res.url)
if token:
token = token[0]
else: # 没有token的话,说明cookies过时了,没有登陆成功,退出程序
print('登陆失败')
return
print('token', token)
# 检索公众号
url = 'https://mp.weixin.qq.com/cgi-bin/searchbiz'
data = {
"action": "search_biz",
"begin": "0",
"count": "5",
"query": key_word,
"token": token,
"lang": "zh_CN",
"f": "json",
"ajax": "1"
}
# 继续使用会话发起请求
res = s.get(url = url, params = data, cookies = cookies, headers = headers, verify = False)
if res.status_code == 200:
# 搜索结果的第一个提取它的fakeid
fakeid = res.json()['list'][0]['fakeid']
print('微信公众号fakeid', fakeid)
page_size = 5 # 默认是5天文章1页,这个参数似乎最大值只有5
page_count = 278 # 公众号文章总页数(自己手动调整,爬取到第几页)
cur_page = 1 # 爬取页数(从第几页开始爬取)
l = 1 # excel计数用
while cur_page <= page_count:
url = 'https://mp.weixin.qq.com/cgi-bin/appmsg'
data = {
"action": "list_ex",
"begin": str(page_size * (cur_page - 1)),
"count": str(page_size),
"fakeid": fakeid,
"type": "9",
"query": "",
"token": token,
"lang": "zh_CN",
"f": "json",
"ajax": "1"
}
time.sleep(random.randint(1, 5))
#继续会话发起请求
res = s.get(url = url, params = data, cookies = cookies, headers = headers, verify =False)
if res.status_code == 200:
print('开始爬取页数:', cur_page)
# 文章列表位于app_msg_list字段中
app_msg_list = res.json()['app_msg_list']
for item in app_msg_list:
# 通过更新时间戳获得文章的发布日期
item['post_date'] = time.strftime("%Y-%m-%d", time.localtime(int(item['update_time'])))
if int(item['post_date'].split('-')[0])<last_date:
continue
# 以下标题分离只适合该公众号
if item['title'].find("|") != -1: # 有竖线分离副标题
title = item['title'].split("|")[1].strip()
cat = item['title'].split("|")[0].strip()
elif item['title'].find("|") != -1:
title = item['title'].split("|")[1].strip() # 分离中文竖线
cat = item['title'].split("|")[0].strip()
elif item['title'].find("│") != -1:
title = item['title'].split("│")[1].strip() # 分离另一种很神奇的竖线
cat = item['title'].split("│")[0].strip()
else:
title = (item['title'])
cat = 'N/A'
title_list.append(title)
date_list.append(item['post_date'])
link_list.append(item['link'])
author_list.append(key_word)
cat_list.append(cat)
response = requests.get(url = item['link'], headers = headers)
print("正在解析网页" + str(item['link']) + '......')
time.sleep(random.randint(1, 5)) # 爬一个,休息1-5秒
tree_content = etree.HTML(response.text) # 获取爬到的动态页面源码
try: # 解析xpath,去噪
content = tree_content.xpath(xpath_string)
content = re.sub(r'\s+', '', ''.join(content)) # 获取到的文章内容(去空格)
content = quzao(content)
except:
content = '' # 没有内容的可能是内容违规已撤销
content_list.append(content)
print('解析文章"'+title+'"成功!')
try:
# 以下逐行写入,备份数据用,防止反爬造成数据丢失
sheet.write(l , 0, title)
sheet.write(l , 1, key_word)
sheet.write(l , 2, content)
sheet.write(l , 3, cat)
sheet.write(l , 4, item['link'])
sheet.write(l , 5, item['post_date'])
savepath = './微信公众号_' + key_word + '_.xls'
l += 1
if l % 20 == 0: # 每20行保存一次(适当调大一点,以免保存失败)
book.save(savepath)
print("数据备份成功!已保存" + str(l) + "条!")
except:
continue
# 当前页面数+1
cur_page += 1
print('over!开始保存')
# 中途没有反爬的话,一次写入所有爬取数据
data = {'title': title_list, 'author': author_list, 'content': content_list,'category': cat_list,"link":link_list,"date":date_list}
df = DataFrame(data)
df.to_excel('./微信公众号_' + key_word + '_.xlsx')
print('保存成功!')
spider()

代码只有cookie需要登录微信公众号后台手动获取,复制粘贴进去;page_count由刚才查文章的界面往下拉,找到一共有多少页,其他参数都不用修改。

为了防止半路被反爬,引入了xlwt库,作用是创建工作表,逐行写入保存爬到的临时数据,不然有可能爬到一半被检测到,最后所有数据都不会保存(别问我为什么知道)!最后一步是写入所有数据,名称和临时数据不一样,也是多一步保险措施。爬取过程显示的数据如下:

如果中途被反爬机制检测到,换一个公众号cookie,然后从中断的cur_page处继续,excel另存。

通过以上代码,实现对公众号“植物生物技术Pbj”8447篇推送(从创建的第一天2019年3月1日至2022年12月18日)爬取:

上面的代码是根据“植物生物技术Pbj”这个公众号排版所特制的,一定要注意根据具体公众号决定制作怎么样的去噪函数,总页数其实也可以根据app_msg_count/page_size值写一个函数自动计算出来,但是如果中途被反爬还是要手动改page值,这里就不多此一举了。还有,如果爬取的每页推送天数(也就是第二个data字典中的count值)可以突破5的话,就可以再写一个循环尽量一次拿到多的文章url,这样检测的机率就会大大降低(我每次被检测都是抓取app_msg_list的时候,而不是抓取文章内容的时候)。

经过半天的测试,有一点以下的经验之谈

  • 微信公众号反爬机制可能是检测你翻页的次数,一天翻页的次数在100-200次间比较保险,也就是一次爬500天-1000天内的推送数据
  • 每次抓取数据sleep1-5秒比较靠谱,1-3秒也容易在爬100条左右的时候被抓……
  • 有一个思路是通过保存所有文章url,再进行每个文章内容抓取,但是获取文章列表的data字典中count值最大只能是5,导致我们需要频繁翻页,这个地方如何突破是一个问题。
  • sheet.write方法有的时候会失效,在某一次打开excel之后可能没来得及写入数据就被保存,导致后续无法继续保存临时数据。解决方法之一是保存间隔大一点,这里我设置了每写入20行保存一次。
  • 做好爬取数据的双保险!我这里做了临时数据保存,不要抱侥幸心,不然爬半天数据容易全部木大!

欢迎小伙伴们留言评论~