微信公眾號是目前最為流行的自媒體之一,上面有大量的內(nèi)容,如何將自己感興趣的公眾號內(nèi)容爬取下來入客,離線瀏覽,或者作進(jìn)一步的分析呢腿椎?
下面我們討論一下微信公眾號文章的爬取桌硫。
環(huán)境搭建
- windows 7 x64
- python3.7 (Anaconda 3)
- vscode編輯器
- Firefox開發(fā)版
爬蟲原理分析
首先網(wǎng)頁登陸微信公眾平臺(https://mp.weixin.qq.com/),登陸成功后啃炸,點(diǎn)擊新建群發(fā)->自建圖文,插入超連接在如下的對話框中铆隘,點(diǎn)擊選擇其他公眾號。
在彈出的編輯超鏈接的對話框中南用,輸入想要爬取的公眾號名字膀钠,回車
下拉列表中第一個就是我們想找的掏湾,點(diǎn)擊它,彈出的這個公眾號的文章列表托修,是按照時(shí)間排序的忘巧。
我們看一下這個過程中前后端交互的HTTP請求和響應(yīng)。
檢索公眾號
請求url: https://mp.weixin.qq.com/cgi-bin/searchbiz
方法: GET
提交的參數(shù)為
{
"action": "search_biz",
"begin": "0",
"count": "5",
"query": "地球知識局",
"token": "138019412",
"lang": "zh_CN",
"f": "json",
"ajax": "1"
}
請求中的字段
action 動作
begin 列表的起始
count 列表的數(shù)目
query 查詢的字符串
f 參數(shù)格式 這里為json
ajax 應(yīng)該代碼ajax請求
lang 語言 這里是中文
token 這應(yīng)該是授權(quán)信息睦刃,下文會深究
得到的響應(yīng)為
{
"base_resp": {
"ret": 0,
"err_msg": "ok"
},
"list": [
{
"fakeid": "MzI1ODUzNjQ1Mw==",
"nickname": "地球知識局",
"alias": "diqiuzhishiju",
"round_head_img": "http://mmbiz.qpic.cn/mmbiz_png/DCftNYRGoKWLHFETxuTzGBguTwAibl0p8BpXmNIkBTmNth2Vd6vEWibtT8mLYWG6e5aiaa97u5LmjhbXn19a8Cr6g/0?wx_fmt=png",
"service_type": 1
},
{
"fakeid": "MzU5MjI3MzIyMg==",
"nickname": "地球知識局庫",
"alias": "",
"round_head_img": "http://mmbiz.qpic.cn/mmbiz_png/b5kRqlMaRNHJnJ1ibFUPOichbvtVGk7CWicj406ZAccBuOpr2JibShHSAvUN7iaSuQj3rN66P8akeKa63rjy11NNkicw/0?wx_fmt=png",
"service_type": 2
},
{},
{},
{}
],
"total": 45
}
響應(yīng)中各字段的含義不難看出
fakeid 為該公眾號的唯一的id砚嘴,為一串bs64編碼
nikename 為公眾號的名稱
alias 為別名
round_head_img 為圓形logo的url
service_type 服務(wù)類型 不太清楚 沒必要深究用不到
獲取公眾號文章列表
請求網(wǎng)址:https://mp.weixin.qq.com/cgi-bin/appmsg
請求方法:GET
提交的參數(shù):
{
"action": "list_ex",
"begin": "0",
"count": "5",
"fakeid": "MzI1ODUzNjQ1Mw==",
"type": "9",
"query": "",
"token": "138019412",
"lang": "zh_CN",
"f": "json",
"ajax": "1"
}
action 行為
begin 列表開始索引
count 列表返回的公眾號的時(shí)間區(qū)間長度,如5表示返回5天的數(shù)據(jù)
fakeid 這個公眾號的ID
type 不知道
query 檢索的關(guān)鍵字涩拙,這里為空
token 用戶的token
lang 語言
f 數(shù)據(jù)格式,這里為json
ajax
響應(yīng)為
{
"app_msg_cnt": 919,
"app_msg_list": [
{
"aid": "2247518136_1",
"appmsgid": 2247518136,
"cover": "https://mmbiz.qlogo.cn/mmbiz_jpg/DCftNYRGoKWG0USHVfs1FG2pGKfz0BMUI3FLibHTrYe1a7WMKzZnazCKDJ9OUfuibGbewFqIiakic8MEqDkNiaXHH7w/0?wx_fmt=jpeg",
"create_time": 1578235906,
"digest": "三不管地帶容易出問題",
"is_pay_subscribe": 0,
"item_show_type": 0,
"itemidx": 1,
"link": "http://mp.weixin.qq.com/s?__biz=MzI1ODUzNjQ1Mw==&mid=2247518136&idx=1&sn=812ec79199ae793f28770287969d0f2b&chksm=ea0462d2dd73ebc40f6ecc4f1f52fb2a3e0c798ca152aa89cc42b8e77ef6e54234695ad43025#rd",
"tagid": [],
"title": "肆虐非洲的“博科圣地”究竟是什么际长?",
"update_time": 1578235905
},
{},
{}
],
"base_resp": {
"err_msg": "ok",
"ret": 0
}
}
響應(yīng)的字段
app_msg_cnt 表示這個公眾號已經(jīng)發(fā)布了919次文章,不代表919篇文章
aid 文章唯一的id兴泥,應(yīng)該是
appmsgid 代表一次群發(fā)工育,如三篇文章是一次性群發(fā)的,其appmsgid相同
cover 文章封面圖片的url
create_time 創(chuàng)建時(shí)間戳
digest 文章的摘要信息
is_pay_subscribe
item_show_type
itemidx 在這次群發(fā)中的序號
link 文章的url
tagid 為一個列表
title 文章的標(biāo)題
update_time 文章更新的時(shí)間戳
這些已經(jīng)包含了一篇文章的元數(shù)據(jù)了搓彻。
token從哪兒來
上面的GET方法提交的參數(shù)有中都有個token字段如绸,這個字段的用途應(yīng)該鑒權(quán)用的,這個值從哪兒來的旭贬?我們在前面的HTTP請求中找怔接,發(fā)現(xiàn)幾乎所有的請求中的都帶有這個token,我猜測這個token是用戶登陸時(shí)從后端返回來的稀轨。
為了印證這個判斷扼脐,重新登陸一次,發(fā)現(xiàn)了有這樣的一個HTTP請求奋刽。
請求網(wǎng)址:https://mp.weixin.qq.com/cgi-bin/bizlogin?action=login
請求方法:POST
表單數(shù)據(jù):
{
"userlang": "zh_CN",
"redirect_url": "",
"token": "",
"lang": "zh_CN",
"f": "json",
"ajax": "1"
}
響應(yīng):
{
"base_resp": {
"err_msg": "ok",
"ret": 0
},
"redirect_url": "/cgi-bin/home?t=home/index&lang=zh_CN&token=1193797244"
}
后端返回了一個重定向的uri瓦侮,其中就包含了token的值。
完成這個請求后佣谐,頁面進(jìn)行了重定向肚吏,并且以后的每次請求都有會有l(wèi)ang=zh_CN&token=xxxx這兩個參數(shù)。
代碼實(shí)現(xiàn)
完成了上面這些分析狭魂,下面我們進(jìn)行代碼實(shí)現(xiàn)须喂。
# -*- coding:utf-8 -*-
# written by wlj @2020-1-6 23:12:47
#功能:爬取一個公眾號的所有歷史文章存入數(shù)據(jù)庫
#用法:python wx_spider.py [公眾號名稱] 如python wx_spider.py 地球知識局
import time
import json
import requests,re,sys
from requests.packages import urllib3
from pymongo import MongoClient
urllib3.disable_warnings()
#全局變量
s = requests.Session()
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/'
}
#cookies 字符串,這是從瀏覽器中拷貝出來的字符串,略過不講
cookie_str = "xxxx"
cookies = {}
#加載cookies趁蕊,將字符串格式的cookies轉(zhuǎn)化為字典形式
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:]
#爬蟲主函數(shù)
def spider():
#本地的mongodb數(shù)據(jù)庫
mongo = MongoClient('127.0.0.1',27017).wx.gzh
#加載cookies
load_cookies()
#訪問官網(wǎng)主頁
url = 'https://mp.weixin.qq.com'
res = s.get(url=url,headers=headers,cookies = cookies,verify=False)
if res.status_code == 200:
#由于加載了cookies坞生,相當(dāng)于已經(jīng)登陸了,系統(tǒng)作了重定義掷伙,response的url中含有我們需要的token
print(res.url)
#獲得token
token = re.findall(r'.*?token=(\d+)',res.url)
if token:
token = token[0]
else:#沒有token的話是己,說明cookies過時(shí)了,沒有登陸成功任柜,退出程序
print('登陸失敗')
return
print('token',token)
#檢索公眾號
url = 'https://mp.weixin.qq.com/cgi-bin/searchbiz'
data = {
"action": "search_biz",
"begin": "0",
"count": "5",
"query": sys.argv[1],
"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:
#搜索結(jié)果的第一個往往是最準(zhǔn)確的
#提取它的fakeid
fakeid = res.json()['list'][0]['fakeid']
print('fakeid',fakeid)
page_size = 5
page_count = 1
cur_page = 1
#分頁請求文章列表
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"
}
res = s.get(url=url,params = data,cookies=cookies,headers=headers,verify=False)
if res.status_code == 200:
print(res.json())
print('cur_page',cur_page)
#文章列表位于app_msg_list字段中
app_msg_list = res.json()['app_msg_list']
for item in app_msg_list:
#通過更新時(shí)間戳獲得文章的發(fā)布日期
item['post_date'] = time.strftime("%Y-%m-%d",time.localtime(int(item['update_time'])))
#插入數(shù)據(jù)庫卒废,如果已經(jīng)存在同aid的話沛厨,更新,不存在摔认,插入
mongo.update_one(
{'aid':item['aid']},
{"$set":item},
upsert=True
)
print(item['post_date'],item['title'])
if cur_page == 1:#若是第1頁逆皮,計(jì)算總的分頁數(shù)
#總的日期數(shù),每page_size天的文章為一頁
app_msg_cnt = res.json()['app_msg_cnt']
print('app_msg_cnt',app_msg_cnt)
#計(jì)算總的分頁數(shù)
if app_msg_cnt % page_size == 0:
page_count = int(app_msg_cnt / page_size)
else:
page_count = int(app_msg_cnt / page_size) + 1
#當(dāng)前頁面數(shù)+1
cur_page += 1
print('完成参袱!')
spider()
結(jié)果
可以看到电谣,所有文章的元數(shù)據(jù)已經(jīng)存入數(shù)據(jù)庫了。
下一節(jié)抹蚀,我們講如何利用文章的url來爬取文章內(nèi)容剿牺,這個比較簡單。
這兒還存在一個問題环壤,騰訊的這個接口有頻率限制晒来,當(dāng)爬取的次數(shù)太多,頻率太快時(shí)郑现,就請求不到數(shù)據(jù)了湃崩,會返回這樣的信息。
{'base_resp': {'err_msg': 'freq control', 'ret': 200013}}
至少間隔一天接箫,這個賬號才能繼續(xù)爬取攒读,不知道如何破解。