1. 程式人生 > >Python自動化開發學習-爬蟲2

Python自動化開發學習-爬蟲2

data unicode dump 自動 erro clas ldr check request

Web服務的本質2

之前講過這個,在這裏:http://blog.51cto.com/steed/2071271
不過當時沒講透,這次再展開一點點。
Web服務的通信本質上就是通過socket發送字符串請求,然後也會返回響應。
發送的請求有請求頭和請求體。返回的響應也有響應頭和響應體。

  • 請求頭:就是requests.hearders,瀏覽器後臺標頭裏的請求標頭
  • 請求體:就是POST請求的data(或者是json),瀏覽器後臺正文裏的請求正文。GET請求沒有請求體
  • 響應頭:就是response.headers,瀏覽器後臺標頭裏的響應標頭
  • 響應體:就是返回的html代碼,response.content,瀏覽器後臺正文裏的響應正文。響應301跳轉等會沒有響應體

格式:請求頭和請求體中間使用\r\n\r\n分隔。而請求頭之間會使用\r\n來分隔。響應頭和響應體類似。

改寫一下當時用Socket模擬的Web服務的響應內容。原本返回的是一個響應頭和一個響應體。
這次返回301跳轉。然後把跳轉的url放到另外一個請求頭location裏。最後再自定義了一個請求頭。之前的分隔符都是\r\n。最後用\r\n\r\n表示響應頭結束,後面就是響應體,不過301跳轉不需要響應體就不寫了:

import socket

def handle_request(conn):
    data = conn.recv(1024)  # 接收數據,隨便收到啥我們都回復Hello World
    # conn.send(‘HTTP/1.1 200 OK\r\n\r\n‘.encode(‘utf-8‘))  # 響應頭以及響應頭和響應體之間的分隔符
    # conn.send(‘Hello World‘.encode(‘utf-8‘))  # 回復的內容,就是網頁的內容,也就是響應體
    conn.send(‘HTTP/1.1 301 / Moved Permanently\r\n‘.encode(‘utf-8‘))
    conn.send(‘location: http://www.baidu.com\r\n‘.encode(‘utf-8‘))
    conn.send(‘MyKey: MyValue\r\n\r\n‘.encode(‘utf-8‘))

def main():
    # 先起一個socket服務端
    server = socket.socket()
    server.bind((‘localhost‘, 8000))
    server.listen(5)
    # 然後持續監聽
    while True:
        conn, addr = server.accept()  # 開啟監聽
        handle_request(conn)  # 將連接傳遞給handle_request函數處理
        conn.close()  # 關閉連接

if __name__ == ‘__main__‘:
    main()

上面的socket啟動之後,使用瀏覽器訪問,會跳轉到指定的頁面,並且能在後臺查看到自定義的響應頭的內容。

示例

再補充一個登錄GitHub的示例,這個是Form表單驗證的。

登錄GitHub

GitHub的登錄驗證使用的是Form表單。
驗證登錄是否成功可以訪問這個頁面:https://github.com/settings/profile
如果沒有登錄,會跳轉到登錄頁面。如果頁面正常打開了,並且能讀取到裏面的用戶信息了,說明登錄認證成功。代碼如下:

import requests
from bs4 import BeautifulSoup

s = requests.Session()
r1 = s.get(‘https://github.com/login‘)
r1.encoding = r1.apparent_encoding
bs1 = BeautifulSoup(r1.text, features=‘html.parser‘)
form = bs1.find(‘form‘)
input_list = form.find_all(‘input‘)
data = {}
for input in input_list:
    name = input.attrs.get(‘name‘)
    value = input.get(‘value‘)  # 和上面的方法效果是一樣的
    data[name] = value
# 不能把密碼上傳啊
with open(‘password/s3.txt‘) as f:
    auth = f.read()
    auth = auth.split(‘\n‘)
data[‘login‘] = auth[0]
data[‘password‘] = auth[1]
r2 = s.post(‘https://github.com/session‘, data=data)
bs2 = BeautifulSoup(r2.text, features=‘html.parser‘)
title = bs2.find(‘title‘)
print(title)  # 登錄成功返回的頁面
r3 = s.get(‘https://github.com/settings/profile‘)
r3.encoding = r3.apparent_encoding  # 獲取頁面的編碼,解決亂碼問題
bs3 = BeautifulSoup(r3.text, features=‘html.parser‘)
title = bs3.find(‘title‘)
print(title)  # 用戶信息頁面的title
name = bs3.find(‘input‘, id="user_profile_name")
print(name.get(‘value‘))  # 用戶的 Name

判斷登錄是否成功

這裏講的對於GitHub這個網站不適用。
一般Form表單驗證的頁面,如果驗證失敗會刷新當前頁面。如果驗證成功,則會發一個跳轉。如果是跳轉的機制,就可以通過這個來判斷是否驗證成功了。
關於重定向返回的響應內容,上面Web服務的本質2裏已經演示的很清楚了。
可以判斷返回的狀態碼,重定向的狀態碼是301或302:

print(response.status_code)

另外重定向除了狀態碼,還有一個location,指向跳轉的地址:

location = response.headers.get(‘location‘)  # 跳轉的url會在location裏

有了location不但能判斷是否驗證成功了,還能知道下一步默認該往哪裏發送請求。

Web 微信

Web登錄地址:https://wx.qq.com/
頁面打開後,會顯示一個二維碼,需要我們有手機微信掃一下。手機授權後,頁面會自動跳轉完成登錄。這裏雖然沒有我們在瀏覽器上操作,但是一旦手機授權後,頁面就會自動跳轉。這裏是用長輪訓的方法持續想服務器提交請求,直到收到服務器返回後執行後會的操作。

長輪訓

先看一下長輪詢在後臺的請求:
技術分享圖片

長輪詢:客戶端向服務器發送Ajax請求,服務器接到請求後hold住連接,直到有新消息才返回響應信息並關閉連接,客戶端處理完響應信息後再向服務器發送新的請求。
優點:在無消息的情況下不會頻繁的請求,耗費資源小。
缺點:服務器hold連接會消耗資源,返回數據順序無保證,難於管理維護。
實例:WebQQ、Hi網頁版、Facebook IM。

合理選擇“心跳”頻率:
這裏必須由客戶端不停地進行請求來維持,所以在客戶端和服務器間保持正常的“心跳”至為關鍵,間隔時間應小於WEB服務器的超時時間,一般建議在10~20秒左右。上面的截圖裏是25秒。
長輪訓是在服務端做的,客戶端只需要用個尾遞歸不停的調用自己發送get請求,get請求是阻塞的,服務器返回之前都會等在那裏。拿到回復的數據後,再分析一下是調用自己遞歸還是進入下一步處理。

獲取二維碼

二維碼就是要掃描的圖片,可以輕松的從前端代碼裏找到img標簽,也可以在後臺調試工具的網絡部分找到圖片的URL,大概的樣子如下:

https://login.weixin.qq.com/qrcode/xxxxxxxxxx==

這裏可以看到關鍵URL最後的那部分,這部分參數之後就叫uuid。
但是用爬蟲直接爬 https://wx.qq.com/ 頁面的時候,返回的img標簽裏找不到這個關鍵的uuid。事實上哪裏都沒找到。uuid是通過另外一個get請求獲取到的,請求的URL如下:

https://login.wx.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=zh_CN&_=1539869227976

這個請求返回的uuid會在響應體力,但是在Edge的後臺顯示是沒有響應體的,可能是沒有沒有解析成功。用google瀏覽器的話應該是能看到返回的數據的。get請求的所有參數裏,這裏只需要修改一個最後的時間戳,註意下時間戳的位數,這裏乘了1000。
下面是請求二維碼圖片,然後下載圖片的代碼:

import requests
import time
import re

s = requests.Session()
params = {
    ‘appid‘: ‘wx782c26e4c19acffb‘,
    ‘redirect_uri‘: ‘https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage‘,
    ‘fun‘: ‘new‘,
    ‘lang‘: ‘zh_CN‘,
    ‘_‘: int(time.time() * 1000)
}
r1 = s.get(‘https://login.wx.qq.com/jslogin‘, params=params)
print(r1.text)
uuid = re.findall(‘window.QRLogin.uuid = "(.*)"‘, r1.text)
uuid = uuid[0]
print(uuid)
r2 = s.get(‘https://login.weixin.qq.com/qrcode/‘ + uuid)
with open(‘%s.jpeg‘ % uuid, ‘wb‘) as f:
    f.write(r2.content)

獲取頭像

之後就是不停的發送那個長輪訓請求了。
如果超時,服務器會返回408狀態碼。這時就要再繼續發請求。
手機掃碼後則會返回201狀態碼,並且還有微信的頭像。這時就可以處理頭像了。頭像的圖片是base64編碼的,網上找一下就有轉碼的方法,如果是寫前端,直接把這段編碼設置為img標簽的src屬性就行了。
接著上面的編碼:

r = 1541893233750 - time.time() * 1000
params = {
    ‘loginicon‘: ‘true‘,
    ‘uuid‘: uuid,
    ‘tip‘: ‘0‘,
    ‘r‘: r,
    ‘_‘: time.time() * 1000
}
while True:
    r3 = s.get(‘https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login‘, params=params)
    print(r3.text)
    code = re.findall("window.code=(\d\d\d)", r3.text)
    code = code[0]
    if code == ‘201‘:
        userAvatar = re.findall("window.userAvatar = ‘(.*)‘;", r3.text)
        userAvatar = userAvatar[0]
        break
    # 每次請求只是自增1,這樣就和準確的時間有誤差了
    # 應該是用這個來控制長時間不掃碼,服務器就會拒絕請求
    params[‘_‘] += 1
    # 是什麽不知道,但是每次都是按時間戳的1000倍減少的
    params[‘r‘] = 1541893233750 - time.time() * 1000

# base64轉碼生成頭像的圖片
import base64
strs = userAvatar.replace("data:img/jpg;base64,", "")
imgdata = base64.b64decode(strs)
with open(‘頭像.jpg‘, ‘wb‘) as f:
    f.write(imgdata)

拿到了頭像之後,仍然會進入一個發送長輪訓的階段,等待手機再點一下登錄授權。現在的這個長輪訓和之前的長輪訓是一樣的,也就是上面的代碼不需要退出while循環,而是在判斷返回的code是201的時候,拿到頭像,然後還是繼續循環發送長輪詢,等手機再點一下完成登錄授權後,返回的code是200,此就可以退出while循環了。
上面的代碼修改一下:

r = 1541893233750 - time.time() * 1000
params = {
    ‘loginicon‘: ‘true‘,
    ‘uuid‘: uuid,
    ‘tip‘: ‘0‘,
    ‘r‘: r,
    ‘_‘: time.time() * 1000
}
code = ‘408‘
r3 = None
while code == ‘408‘:
    r3 = s.get(‘https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login‘, params=params)
    print(r3.text)
    code = re.findall("window.code=(\d\d\d)", r3.text)
    code = code[0]
    if code == ‘201‘:
        userAvatar = re.findall("window.userAvatar = ‘(.*)‘;", r3.text)
        userAvatar = userAvatar[0]
        import base64
        strs = userAvatar.replace("data:img/jpg;base64,", "")
        imgdata = base64.b64decode(strs)
        with open(‘頭像.jpg‘, ‘wb‘) as f:
            f.write(imgdata)
        # 201收到響應之後,繼續發送長輪詢
        params[‘_‘] += 1
        params[‘r‘] = 1541893233750 - time.time() * 1000
        r3 = s.get(‘https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login‘, params=params)
        code = re.findall("window.code=(\d\d\d)", r3.text)
        code = code[0]
    # 每次請求只是自增1,這樣就和準確的時間有誤差了
    # 應該是用這個來控制長時間不掃碼,服務器就會拒絕請求
    params[‘_‘] += 1
    # 是什麽不知道,但是每次都是按時間戳的1000倍減少的
    params[‘r‘] = 1541893233750 - time.time() * 1000

print(r3.text)
redirect_uri = re.findall("window.redirect_uri=\"(.*)\";", r3.text)[0]
print(redirect_uri)

之後返回code是408才繼續長輪訓,返回201,則收下頭像的圖片然後再發起一次長輪訓(這部分代碼有點重復,不過保證示例的整個過程清晰)。返回其他的code否退出循環,這裏正常會返回200。

驗證的憑證

上面的步驟最後會拿到一個 redirect_uri ,值是一個url,可以直接訪問。不同實際在瀏覽器收到200返回碼之後發的請求的url有點小區別:

"https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=XXXXXXXXXXXXOOOOOOOOOOOO@qrticket_0&uuid=XXXXXXXXXX==&lang=zh_CN&scan=153xxxx221"
"https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=XXXXXXXXXXXXOOOOOOOOOOOO@qrticket_0&uuid=XXXXXXXXXX==&lang=zh_CN&scan=153xxxx221&fun=new&version=v2"

實際瀏覽器發送的請求會多兩個參數,
如果用默認的 redirect_uri 發送請求,返回的是一個html,這個應該是Web微信的界面,但是不帶任何數據,原因就是沒有認證信息。
如果加上上面額外的參數,則收到的信息像下面這個樣子:

<error>
    <ret>0</ret>
    <message></message>
    <skey>@crypt_d1544694_9eb666666b490ff4444c94ab4444f0d2</skey>
    <wxsid>tMlup2XXXXXX0pIp</wxsid>
    <wxuin>1112345678</wxuin>
    <pass_ticket>mFJdwSibpJ5R%2FbQ564HXXXXXOOOOO%2FEiEO86KPL3EI6F2poriL4OOOOOOXXXXXX%2B</pass_ticket>
    <isgrayscale>1</isgrayscale>
</error>

上面這個就是XML格式的憑證,之後基於登錄後的操作,都要帶著憑證提交。類似Cookie,但是這裏不用Cookie而是用這個。這裏把XML也用BeautifulSoup解析一下,把憑證裏所有的 key 、 value 保存為一個字典。
再發一次請求,redirect_uri 裏加上2個參數。然後把返回的拼接解析後轉成字典打印出來:

params = {
    ‘fun‘: ‘new‘,
    ‘version‘: ‘v2‘
}
r4 = s.get(redirect_uri, params=params)
print(r4.text)
soup = BeautifulSoup(r4.text, features=‘html.parser‘)
target = soup.find(‘error‘)
ticket = {}
for item in target.children:
    ticket[item.name] = item.text
print(ticket)

到此登錄告一段落,把最後的憑證保存好

獲取用戶信息

在瀏覽器開發者模式的網絡分頁裏,可以找到如下緊挨著的3個請求:

  • 響應 redirect_uri 的 GET 請求,手機掃碼再點登陸後返回 code=200 和 redirect_uri 。上上節做的
  • 響應 XML 憑證的 GET 請求,向 redirect_uri 提交請求拿到憑證。上一節做的
  • 獲取用戶信息的 POST 請求。現在要處理的,要把憑證的信息加到 url 參數以及 POST 的請求體裏。

請求的代碼如下,拿到請求後要轉一下編碼,否則是亂碼:

url = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit"
params = {
    # ‘r‘: ‘1976951002‘,  # 這是什麽不知道,不加也沒問題
    ‘lang‘: ‘zh_CN‘,
    ‘pass_ticket‘: ticket[‘pass_ticket‘],
}
json_data = {"BaseRequest": {
    "Uin": ticket[‘wxuin‘],
    "Sid": ticket[‘wxsid‘],
    "Skey": ticket[‘skey‘],
    "DeviceID": "e189955857229638",
}}
r5 = s.post(url, params=params, json=json_data)
r5.encoding = r5.apparent_encoding
print(r5.apparent_encoding)
print(r5.text)

從返回的信息裏看,有部分最近訂閱號和最近聯系人的信息。數據都是以JSON字符串的形式返回的。之後再繼續分析和處理之前,先執行一步 jso.loads(r5.text) 反序列化轉成對象。
可用生成一個html來展示:

# 把頁面的內容生成一個html來展示
import json
obj = json.loads(r5.text)
user = obj[‘User‘]
f = open(‘wx.html‘, ‘w‘, encoding=‘utf-8‘)
f.write(‘<meta charset="UTF-8">\n‘)
f.write("<h1>Web 微信</h1>\n")
f.write("<h3>用戶名:%s</h3>\n" % user[‘NickName‘])
contactList = obj[‘ContactList‘]
f.write("<h3>最近聯系人</h3>\n")
f.write("<ul>\n")
for i in contactList:
    # print(i)
    user_info = i[‘RemarkName‘] or i[‘NickName‘]
    if i[‘Sex‘]:
        sex = "男" if i[‘Sex‘] == 1 else "女"
        user_info = "%s(%s)" % (user_info, sex)
    if i[‘Signature‘]:
        user_info = "%s: %s" % (user_info, i[‘Signature‘])
    f.write("<li>%s</li>\n" % user_info)
f.write("</ul>\n")
mpSubscribeMsgList = obj[‘MPSubscribeMsgList‘]
f.write("<h3>最近公眾號信息</h3>\n")
f.write("<ul>\n")
for i in mpSubscribeMsgList:
    # print(i)
    f.write("<li>%s</li>\n" % i[‘NickName‘])
    f.write("<ul>\n")
    for article in i[‘MPArticleList‘]:
        f.write("<li><a href=‘%s‘>%s</a></br>%s</li>\n" % (article[‘Url‘], article[‘Title‘], article[‘Digest‘]))
    f.write("</ul>\n")
f.write("</ul>\n")
f.close()

這裏拿到的信息只是概況,聯系人和公眾號都不全,都是最近的聯系人。
另外信息裏面還有頭像和公眾號文章的圖片,下載沒問題,但是要在html裏用img標簽寫src是顯示不出來的。做了外鏈限制

獲取聯系人列表

繼續在瀏覽器開發者模式的網絡分頁裏找,在憑證的後面是上面的POST的初始化請求webwxinit。繼續往後找,主要看響應體,有很多圖片的請求是可以跳過的,都是下載頭像之類的。找到返回內容最長的那個應該就是聯系人列表了。另外還有一個返回的內容也很多,可能是公眾號,不過這裏不管那個了。
獲取聯系人列表的代碼:

# 獲取所有聯系人信息,這個請求是會驗證cookie的
url = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact"
params = {
    ‘pass_ticket‘: ticket[‘pass_ticket‘],
    ‘r‘: int(time.time() * 1000),
    ‘seq‘: ‘0‘,
    ‘skey‘: ticket[‘skey‘]
}
r6 = s.get(url, params=params)
# r6.encoding = r6.apparent_encoding  # apparent_encoding 自動獲取到的編碼是錯的
# print(r6.apparent_encoding)
r6.encoding = "utf-8"  # 直接指定"utf-8"就對了
# 自動獲取到的編碼是"Windows-1254"這個是別名,正式名稱是"cp1254"。
# 寫哪個都一樣的,不過問題是,不能用,編碼是錯的,大概就是誤導我們的
# Python36/Lib/encodings/aliases.py 這個文件裏有所有編碼的別名的對應關系
print(r6.text)
with open(‘contact.txt‘, ‘w‘, encoding=‘utf-8‘) as f:
    f.write(r6.text)

這裏有幾個坑:

  • 這個請求需要Cookie,一直使用最開始的Session對象的就不會有問題
  • 編碼問題,apparent_encoding拿到的不對,直接指定"utf-8"

之後先要分析一波聯系人,把返回的內容先保存到本地,之後不用再反復去請求了。
對文件的內容解析,先看下有哪些字段:

import json

with open(‘contact.txt‘, encoding=‘utf-8‘) as f:
    obj = json.load(f)
for i in obj:
    print(i)

一共就4個key:

  • BaseResponse,沒啥用
  • MemberCount,一共有多少聯系人
  • MemberList,一個列表,列表裏面是一個個字典,每個字典就是一個聯系人信息
  • Seq,也是沒啥用

進行到這裏,已經對自己所有的聯系人進行一波統計分析了。比如男女比例,地區分布。不過數據分析不是這裏的重點

發送消息

到這裏就不一點點分析了,下面的代碼,就能發消息了(中文還有問題):

# 找到聯系人信息
name = "這裏填聯系人的名字"
msg = "Hello"  # 發中文會有亂碼,不過這個是json序列化的問題
to_user_obj = None
obj = json.loads(r6.text)
for member in obj[‘MemberList‘]:
    if name in member["NickName"] or name == member["RemarkName"]:
        to_user_obj = member
        break
if to_user_obj:
    print(to_user_obj["Signature"])
else:
    to_user_obj = user
    print("未找到聯系人")
# 發消息
url = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg"
params = {
    ‘lang‘: ‘zh_CN‘,
    ‘pass_ticket‘: ticket[‘pass_ticket‘],
}
# 這個字典之前用過,之前裏面只有BaseRequest
# 現在保留BaseRequest,還要加上Msg
time_stamp = time.time() * 1000
json_data[‘Msg‘] = {
    ‘ClientMsgId‘: time_stamp,
    ‘Content‘: msg,
    ‘FromUserName‘: user["UserName"],  # 之前獲取用戶信息裏拿到的
    ‘LocalID‘: time_stamp,
    ‘ToUserName‘: to_user_obj["UserName"],
    ‘Type‘: 1,  # 這個是消息類型,1是文本
}
json_data[‘Scene‘] = 0  # 不知道是啥,照著寫
r7 = s.post(url=url, params=params, json=json_data)
print(r7.text)

中文亂碼問題
如果發送“你好”,對方會收到“\u4f60\u597d”,這個是中文的Unicode編碼,是在json.dumps裏變的:

>>> import json
>>> json.dumps("你好")
‘"\\u4f60\\u597d"‘
>>> json.dumps("Hello")
‘"Hello"‘
>>> json.dumps("你好", ensure_ascii=False)
‘"你好"‘
>> 

中文在json序列化的時候,默認會轉成Unicode,不過可以加上ensure_ascii參數不轉。
之前自己做寫django項目的時候,如果客戶端 josn.dumps 了,服務端再 json.loads 一下,中文就回來了。現在服務端是人家的,只能讓客戶端不要對中文進行轉碼
自己做json序列化就不能把參數傳給json了,否則還會把json字符串再序列化一次。data參數和json參數都是請求體,傳給json參數後,原本requests會幫我做一些事情,現在要自定義就得自己調整了。把自己序列化後的字符串傳給data,data就原樣接收了。但是要讓服務端把請求體(body)的內容作為json字符串處理。修改請求頭的 ‘Content-Type‘ 的值。改一下之前的POST請求:

# r7 = s.post(url=url, params=params, json=json_data)  # 這個不能發中文
headers = s.headers
headers[‘Content-Type‘] = ‘application/json‘
data = json.dumps(json_data, ensure_ascii=False).encode(‘utf-8‘)
r7 = s.post(url=url, params=params, headers=headers, data=data)

上面在傳參給data之前還要還要 data.encode(‘utf-8‘) 處理一下,否則會報錯。如果直接給字符串的話,最終會執行 body.encode("latin-1") ,這個編譯不了,所以就報錯了,錯誤信息會有提示。另外參考下面requests裏的這小段代碼,json序列化之後,也是把字符串用encode轉成bytes類型的。所以直接給bytes類型。

        if not data and json is not None:
            # urllib3 requires a bytes-like body. Python 2‘s json.dumps
            # provides this natively, but Python 3 gives a Unicode string.
            content_type = ‘application/json‘
            body = complexjson.dumps(json)
            if not isinstance(body, bytes):
                body = body.encode(‘utf-8‘)

下面是發送成功後返回的消息:

{
    "BaseResponse": {
        "Ret": 0,
        "ErrMsg": ""
    },
    "MsgID": "9025779609933123936",
    "LocalID": "1540098759694.243"
}

接收消息

還是看瀏覽器開發者模式的網絡分頁,裏面還是會有一個長輪訓。不過實際上沒那麽簡單,這裏至少要處理2個請求。一個是長輪訓請求,會有2種返回狀態:

  • ‘window.synccheck={retcode:"0",selector:"0"}‘ : 繼續下一次長輪訓
  • ‘window.synccheck={retcode:"0",selector:"2"}‘ : 則發起另外一個POST的消息同步請求

消息同步的POST請求會接收收到的消息,也可能是0條消息,但是還是得同步一次,否則長輪訓會一直返回2。另外最初的 SyncKey 只有4個,在 POST 之後還會多2個,最好也更新到之後的請求裏。
另外消息發送人和接收人,收到的都是一串類似id的東西,這個要去之前的聯系人列表裏查找 "UserName" 然後獲取 "NickName" 。這裏沒做,只是簡單的把發送人的id打印出來了。這個id不是固定的,每次連接web微信,返回的聯系人列表的id都不一樣。
接收消息的代碼如下:

# 收消息
url = "https://webpush.wx.qq.com/cgi-bin/mmwebwx-bin/synccheck"
sync_key = json.loads(r5.text)["SyncKey"]
params = {
    ‘skey‘: ticket[‘skey‘],
    ‘sid‘: ticket[‘wxsid‘],
    ‘uin‘: ticket[‘wxuin‘],
    ‘deviceid‘: ‘e941046347280021‘,  # 這個一直在變,貌似沒啥影響
    ‘_‘: int(time.time() * 1000) - 26846,
}
print("持續接收消息")
while True:
    sync_key_list = []
    for item in sync_key["List"]:
        sync_key_list.append("%s_%s" % (item["Key"], item["Val"]))
    synckey = "|".join(sync_key_list)
    params_update = {
        ‘synckey‘: synckey,
        ‘_‘: params[‘_‘] + 1,
        ‘r‘: int(time.time() * 1000),
    }
    params.update(params_update)
    print("發起 r8 長輪訓")
    try:
        r8 = s.get(url=url, params=params)
        print(r8.text)
    except requests.exceptions.ConnectionError as e:
        print("捕獲到異常")
        params[‘_‘] -= 1
        continue
    # 返回 ‘window.synccheck={retcode:"0",selector:"0"}‘ 則繼續長輪訓
    # 返回 ‘window.synccheck={retcode:"0",selector:"2"}‘ 則發起POST
    if r8.text == ‘window.synccheck={retcode:"0",selector:"2"}‘:
        print("POST同步:webwxsync")
        sync_url = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync"
        sync_params = {
            ‘lang‘: ‘zh_CN‘,
            ‘skey‘: ticket[‘skey‘],
            ‘sid‘: ticket[‘wxsid‘],
            ‘pass_ticket‘: ticket[‘pass_ticket‘],
        }
        json_data["SyncKey"] = json.loads(r5.text)["SyncKey"]  # 在之前r5的基礎上加一個SyncKey字典
        r9 = s.post(sync_url, params=sync_params, json=json_data)
        # r9.encoding = r9.apparent_encoding
        print(r9.apparent_encoding)  # 自動獲取到的編碼還是有問題
        r9.encoding = ‘utf-8‘
        # print(r9.text)
        r9_obj = json.loads(r9.text)
        add_msg_count = r9_obj[‘AddMsgCount‘]
        print("你有 %s 條消息" % add_msg_count)
        add_msg_list = r9_obj[‘AddMsgList‘]
        for add_msg in add_msg_list:
            content = add_msg["Content"]
            from_user_name = add_msg["FromUserName"]
            print(content, "<==", from_user_name)
        sync_key = json.loads(r9.text)["SyncKey"]  # 這裏會多2條SyncKey

這裏還有個坑,如果代碼運行起來之後,馬上就有消息進來(對方回復的太快),我測的時候會發生異常。也沒找到啥原因,而且如果是等一下再有消息來跑著也很正常。最後就用try把異常捕獲處理了。
另外消息數量會累加,可能還有一個已讀消息的請求,這個沒有繼續深入。

Python自動化開發學習-爬蟲2