基於PC端的爬取公眾號歷史文章
- 2019 年 10 月 6 日
- 筆記
微信後台很多消息未回復:看到時已經回復不了。有問題可以添加我的微信:菜單 ->聯繫我
由於最近需要公眾號的歷史文章資訊,所以就嘗試爬了一下,雖然目前可以爬到數據,但是還不能夠大量的自動化爬取。原因是參數key值具有時效性(具體時間沒有驗證20分鐘的樣子),目前也不知道是如何生成的。
文章歷史列表爬取
首先先到的是搜狗微信,但是搜狗微信只能看到前十篇文章並且查不到閱讀量和在看的數量,嘗試爬取手機包,發現沒有抓取到資訊,後來才知道原因:
1、Android系統7.0以下,微信信任系統的證書。
2、Android系統7.0以上,微信7.0一下版本,微信信任系統提供的證書。
3、Android系統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中由於微信限制,我添加了鏈接兩個字,去掉就好了。