前言
分享一個筆者最近寫的 Python 相關的小 demo,爬取某個公眾號的所有歷史文章,並匯出到本地,方便之後線上下環境直接觀看。參考了劉志軍的小冊基於Python實現微信公眾號爬蟲,有興趣的同學也可以自行購買。
這個功能還是有一定實際用途的,需求和功能雖然簡單明確,但我在開發的過程中,也是遇到了一定的問題,可以說好好的把 Python 爬蟲方面的知識複習了一遍。我也將從最基礎的抓包開始講起,希望能提供一個完整爬取流程的簡單教程。
抓包
在 Windows 平臺,我們經常使用 Fiddler 來進行抓包,筆者使用 Mac,所以比較習慣使用 Charles 來進行抓包。不僅如此,這類工具在開發過程中是非常重要的工具,筆者平時在客戶端開發過程中,如果服務端介面還沒完成,只要定義好資料結構,通過這類工具的重定向功能,就可以輕易的自己模擬資料來開發了。
用手機抓取 https 介面,需要在手機裡安裝證照,網上方法很多,我這裡就不費篇幅了。
我們開啟微信歷史訊息介面,然後在 Charles 裡面找尋介面,通過觀察 Response 返回的內容,我們發現了我們需要的請求:
第一頁爬取
我們在 Python 中記錄下這個 url 和 Header,需要注意的是,這個url請求的資料只是第一頁的資料,上拉載入的url介面形式是完全不同的。
url = "https://mp.weixin.qq.com/mp/profile_ext?" \
"action=home&" \
"__biz=MjM5ODIyMTE0MA==&" \
"scene=124&" \
"devicetype=android-23&" \
"version=26060532&" \
"lang=zh_CN&" \
"nettype=WIFI&" \
"a8scene=3&" \
"pass_ticket=Pu%2FH3aPR7f%2FzgA52T%2Bv4fU9wSWkY5tgGGgAXWewji2dqpMDrjaxUbBR%2Fmo2e%2FaMX&wx_header=1"
headers = """
Host: mp.weixin.qq.com
Connection: keep-alive
User-Agent: Mozilla/5.0 (Linux; Android 6.0.1; NX531J Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132 MQQBrowser/6.2 TBS/044030 Mobile Safari/537.36 MicroMessenger/6.6.5.1280(0x26060532) NetType/WIFI Language/zh_CN
x-wechat-key: b97b0a94956e0cb26093b03bcbfb059796e335db8c12a8036cdff0191103874cee2a5045062b4058d71c848ab74c8b256570c9a9547fe2eb9572b1a762f9cea43f91428b4a31bf5618a8c61c00da7287
x-wechat-uin: MTMzNjE3ODYyMQ%3D%3D
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,image/wxpic,image/sharpp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,en-US;q=0.8
Cookie: sd_userid=96301522585723838; sd_cookie_crttime=1522585723838; pgv_pvid=1151171120; tvfe_boss_uuid=92a513a6354c3896; rewardsn=; wxtokenkey=777; wxuin=1336178621; devicetype=android-23; version=26060532; lang=zh_CN; pass_ticket=Pu/H3aPR7f/zgA52T+v4fU9wSWkY5tgGGgAXWewji2dqpMDrjaxUbBR/mo2e/aMX; wap_sid2=CL3vkf0EEnBSWHFYWmVoZVpOMjU0cnBpSUhiLWF2cmZHVVVLVWZrWUp4QVRlclVVOTRwS1hmMGNUR0VJaXp1RlVzbGViM2wtZnVfakZVd21RcGxxbzI3U3R3cmtYYlUycXpLU0FzcGJFSm1ESkZsYVhzSzhBd0FBMLbQ5dcFOA1AlU4=
Q-UA2: QV=3&PL=ADR&PR=WX&PP=com.tencent.mm&PPVN=6.6.5&TBSVC=43603&CO=BK&COVC=044030&PB=GE&VE=GA&DE=PHONE&CHID=0&LCID=9422&MO= NX531J &RL=1080*1920&OS=6.0.1&API=23
Q-GUID: 6a875f18ea5ba76bb6afb9ca13b788cb
Q-Auth: 31045b957cf33acf31e40be2f3e71c5217597676a9729f1b
"""
複製程式碼
這個請求返回的是一個 H5 介面,並不是我們期望的是一個 JSON 檔案,但沒關係,通過我們的 xml 解析器,我們始終可以通過細緻的觀察,找尋我們需要的資訊。我們發現,下面這段程式碼,隱藏著前十條文章的資料列表。
def extract_data(html_content):
rex = "msgList = '({.*?})'"
pattern = re.compile(pattern=rex, flags=re.S)
match = pattern.search(html_content)
if match:
data = match.group(1)
data = html.unescape(data)
data = json.loads(data)
articles = data.get("list")
articles_lists = dict()
for item in articles:
if item.get("app_msg_ext_info"):
articles_lists[item["app_msg_ext_info"]["title"]] = item["app_msg_ext_info"]["content_url"]
return articles_lists
複製程式碼
我們可以通過正則來提取出這部分資料,儲存在 json 中,現在讓我們來分析下資料:
article = {'app_msg_ext_info':
{'title': '那些對印度的誤解與偏見',
'copyright_stat': 11,
'is_multi': 1,
'content': '',
'author': 'WeaponZhi',
'subtype': 9,
'del_flag': 1,
'fileid': ,
'content_url': 'http:\\/\\/mp....',
''
'digest': '提到印度,你首先會想到什麼',
'cover': 'http:\\/\\/mmbiz.qpic.cn\\...',
'multi_app_msg_item_list': [{'fileid': 861719336,
'content_url': 'http:\\/\\/mp...',
'content': '', 'copyright_stat': 11,
'cover': 'http:\\/\\/mmbiz.qpic.cn',
'del_flag': 1,
'digest': '攜程再努努力就快趕上百度了',
'...
複製程式碼
根據我們歷史文章的樣式,以及我們需要的資料和需求,筆者抽取了每篇文章的幾個重要欄位:
- title:文章標題
- content_url:文章連結
- digest:摘要
- multi_app_msg_item_list:同時傳送的其他文章的欄位列表
multi_app_msg_item_list
欄位就是一組多圖文資料列表,在微信裡的展現形式是這樣的
現在我們拿到了每篇文章的具體連結 content_url,後面我們需要做的就是請求這個url,從中抽取文章內容,再把內容以一定的格式儲存在檔案中即可。
headers = headers_to_dict(headers)
response = requests.get(url, headers=headers, verify=False)
if '<title>驗證</title>' in response.text:
raise Exception("獲取微信公眾號文章失敗,可能是因為你的請求引數有誤,請重新獲取")
data = extract_data(response.text)
rex = r'\\/'
for item in data:
pattern = re.sub(rex, '/', html.unescape(data.get(item)))
response = requests.get(pattern, headers=headers, verify=False)
parser_text_to_file(item, response.text)
def parser_text_to_file(title, article_content):
soup = BeautifulSoup(article_content, 'html.parser', from_encoding='utf8')
node = soup.find(id="js_content")
write_text_to_file(title, node)
def write_text_to_file(title, node):
contents = node.descendants
for item in contents:
if isinstance(item, NavigableString):
with open(title, "a", encoding="utf-8") as f:
f.write(str(item))
f.write('\n\n')
複製程式碼
這裡我們寫了一個驗證判斷,為了防止過度的爬取操作,爬取歷史文章的介面 Cookie 只能有一定的有效時間,如果你在爬取過程中發現獲取資料失敗,那你就需要重新進入介面然後更新程式碼中的 Header 中的Cookie了,我們在後面的文章中對這個問題將進行具體解析。
實際上上面的程式碼就是這個小 demo 的業務核心了,我們現在只處理了歷史文章前十篇的內容,下篇文章我將通過載入更多介面,將一個公眾號所有文章爬取出來,更有意思的自然還在後面。
推薦閱讀