之前講過這個(gè),在這里:https://blog.51cto.com/steed/2071271
不過當(dāng)時(shí)沒講透,這次再展開一點(diǎn)點(diǎn)。
Web服務(wù)的通信本質(zhì)上就是通過socket發(fā)送字符串請(qǐng)求,然后也會(huì)返回響應(yīng)。
發(fā)送的請(qǐng)求有請(qǐng)求頭和請(qǐng)求體。返回的響應(yīng)也有響應(yīng)頭和響應(yīng)體。
格式:請(qǐng)求頭和請(qǐng)求體中間使用\r\n\r\n分隔。而請(qǐng)求頭之間會(huì)使用\r\n來分隔。響應(yīng)頭和響應(yīng)體類似。
改寫一下當(dāng)時(shí)用Socket模擬的Web服務(wù)的響應(yīng)內(nèi)容。原本返回的是一個(gè)響應(yīng)頭和一個(gè)響應(yīng)體。
這次返回301跳轉(zhuǎn)。然后把跳轉(zhuǎn)的url放到另外一個(gè)請(qǐng)求頭location里。最后再自定義了一個(gè)請(qǐng)求頭。之前的分隔符都是\r\n。最后用\r\n\r\n表示響應(yīng)頭結(jié)束,后面就是響應(yīng)體,不過301跳轉(zhuǎn)不需要響應(yīng)體就不寫了:
import socket
def handle_request(conn):
data = conn.recv(1024) # 接收數(shù)據(jù),隨便收到啥我們都回復(fù)Hello World
# conn.send('HTTP/1.1 200 OK\r\n\r\n'.encode('utf-8')) # 響應(yīng)頭以及響應(yīng)頭和響應(yīng)體之間的分隔符
# conn.send('Hello World'.encode('utf-8')) # 回復(fù)的內(nèi)容,就是網(wǎng)頁的內(nèi)容,也就是響應(yīng)體
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():
# 先起一個(gè)socket服務(wù)端
server = socket.socket()
server.bind(('localhost', 8000))
server.listen(5)
# 然后持續(xù)監(jiān)聽
while True:
conn, addr = server.accept() # 開啟監(jiān)聽
handle_request(conn) # 將連接傳遞給handle_request函數(shù)處理
conn.close() # 關(guān)閉連接
if __name__ == '__main__':
main()
上面的socket啟動(dòng)之后,使用瀏覽器訪問,會(huì)跳轉(zhuǎn)到指定的頁面,并且能在后臺(tái)查看到自定義的響應(yīng)頭的內(nèi)容。
再補(bǔ)充一個(gè)登錄GitHub的示例,這個(gè)是Form表單驗(yàn)證的。
GitHub的登錄驗(yàn)證使用的是Form表單。
驗(yàn)證登錄是否成功可以訪問這個(gè)頁面:https://github.com/settings/profile
如果沒有登錄,會(huì)跳轉(zhuǎn)到登錄頁面。如果頁面正常打開了,并且能讀取到里面的用戶信息了,說明登錄認(rèn)證成功。代碼如下:
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
這里講的對(duì)于GitHub這個(gè)網(wǎng)站不適用。
一般Form表單驗(yàn)證的頁面,如果驗(yàn)證失敗會(huì)刷新當(dāng)前頁面。如果驗(yàn)證成功,則會(huì)發(fā)一個(gè)跳轉(zhuǎn)。如果是跳轉(zhuǎn)的機(jī)制,就可以通過這個(gè)來判斷是否驗(yàn)證成功了。
關(guān)于重定向返回的響應(yīng)內(nèi)容,上面Web服務(wù)的本質(zhì)2里已經(jīng)演示的很清楚了。
可以判斷返回的狀態(tài)碼,重定向的狀態(tài)碼是301或302:
print(response.status_code)
另外重定向除了狀態(tài)碼,還有一個(gè)location,指向跳轉(zhuǎn)的地址:
location = response.headers.get('location') # 跳轉(zhuǎn)的url會(huì)在location里
有了location不但能判斷是否驗(yàn)證成功了,還能知道下一步默認(rèn)該往哪里發(fā)送請(qǐng)求。
Web登錄地址:https://wx.qq.com/
頁面打開后,會(huì)顯示一個(gè)二維碼,需要我們有手機(jī)微信掃一下。手機(jī)授權(quán)后,頁面會(huì)自動(dòng)跳轉(zhuǎn)完成登錄。這里雖然沒有我們?cè)跒g覽器上操作,但是一旦手機(jī)授權(quán)后,頁面就會(huì)自動(dòng)跳轉(zhuǎn)。這里是用長輪訓(xùn)的方法持續(xù)想服務(wù)器提交請(qǐng)求,直到收到服務(wù)器返回后執(zhí)行后會(huì)的操作。
先看一下長輪詢?cè)诤笈_(tái)的請(qǐng)求:
長輪詢:客戶端向服務(wù)器發(fā)送Ajax請(qǐng)求,服務(wù)器接到請(qǐng)求后hold住連接,直到有新消息才返回響應(yīng)信息并關(guān)閉連接,客戶端處理完響應(yīng)信息后再向服務(wù)器發(fā)送新的請(qǐng)求。
優(yōu)點(diǎn):在無消息的情況下不會(huì)頻繁的請(qǐng)求,耗費(fèi)資源小。
缺點(diǎn):服務(wù)器hold連接會(huì)消耗資源,返回?cái)?shù)據(jù)順序無保證,難于管理維護(hù)。
實(shí)例:WebQQ、Hi網(wǎng)頁版、Facebook IM。
合理選擇“心跳”頻率:
這里必須由客戶端不停地進(jìn)行請(qǐng)求來維持,所以在客戶端和服務(wù)器間保持正常的“心跳”至為關(guān)鍵,間隔時(shí)間應(yīng)小于WEB服務(wù)器的超時(shí)時(shí)間,一般建議在10~20秒左右。上面的截圖里是25秒。
長輪訓(xùn)是在服務(wù)端做的,客戶端只需要用個(gè)尾遞歸不停的調(diào)用自己發(fā)送get請(qǐng)求,get請(qǐng)求是阻塞的,服務(wù)器返回之前都會(huì)等在那里。拿到回復(fù)的數(shù)據(jù)后,再分析一下是調(diào)用自己遞歸還是進(jìn)入下一步處理。
二維碼就是要掃描的圖片,可以輕松的從前端代碼里找到img標(biāo)簽,也可以在后臺(tái)調(diào)試工具的網(wǎng)絡(luò)部分找到圖片的URL,大概的樣子如下:
https://login.weixin.qq.com/qrcode/xxxxxxxxxx==
這里可以看到關(guān)鍵URL最后的那部分,這部分參數(shù)之后就叫uuid。
但是用爬蟲直接爬 https://wx.qq.com/ 頁面的時(shí)候,返回的img標(biāo)簽里找不到這個(gè)關(guān)鍵的uuid。事實(shí)上哪里都沒找到。uuid是通過另外一個(gè)get請(qǐng)求獲取到的,請(qǐng)求的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
這個(gè)請(qǐng)求返回的uuid會(huì)在響應(yīng)體力,但是在Edge的后臺(tái)顯示是沒有響應(yīng)體的,可能是沒有沒有解析成功。用google瀏覽器的話應(yīng)該是能看到返回的數(shù)據(jù)的。get請(qǐng)求的所有參數(shù)里,這里只需要修改一個(gè)最后的時(shí)間戳,注意下時(shí)間戳的位數(shù),這里乘了1000。
下面是請(qǐng)求二維碼圖片,然后下載圖片的代碼:
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)
之后就是不停的發(fā)送那個(gè)長輪訓(xùn)請(qǐng)求了。
如果超時(shí),服務(wù)器會(huì)返回408狀態(tài)碼。這時(shí)就要再繼續(xù)發(fā)請(qǐng)求。
手機(jī)掃碼后則會(huì)返回201狀態(tài)碼,并且還有微信的頭像。這時(shí)就可以處理頭像了。頭像的圖片是base64編碼的,網(wǎng)上找一下就有轉(zhuǎn)碼的方法,如果是寫前端,直接把這段編碼設(shè)置為img標(biāo)簽的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
# 每次請(qǐng)求只是自增1,這樣就和準(zhǔn)確的時(shí)間有誤差了
# 應(yīng)該是用這個(gè)來控制長時(shí)間不掃碼,服務(wù)器就會(huì)拒絕請(qǐng)求
params['_'] += 1
# 是什么不知道,但是每次都是按時(shí)間戳的1000倍減少的
params['r'] = 1541893233750 - time.time() * 1000
# base64轉(zhuǎn)碼生成頭像的圖片
import base64
strs = userAvatar.replace("data:img/jpg;base64,", "")
imgdata = base64.b64decode(strs)
with open('頭像.jpg', 'wb') as f:
f.write(imgdata)
拿到了頭像之后,仍然會(huì)進(jìn)入一個(gè)發(fā)送長輪訓(xùn)的階段,等待手機(jī)再點(diǎn)一下登錄授權(quán)。現(xiàn)在的這個(gè)長輪訓(xùn)和之前的長輪訓(xùn)是一樣的,也就是上面的代碼不需要退出while循環(huán),而是在判斷返回的code是201的時(shí)候,拿到頭像,然后還是繼續(xù)循環(huán)發(fā)送長輪詢,等手機(jī)再點(diǎn)一下完成登錄授權(quán)后,返回的code是200,此就可以退出while循環(huán)了。
上面的代碼修改一下:
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收到響應(yīng)之后,繼續(xù)發(fā)送長輪詢
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]
# 每次請(qǐng)求只是自增1,這樣就和準(zhǔn)確的時(shí)間有誤差了
# 應(yīng)該是用這個(gè)來控制長時(shí)間不掃碼,服務(wù)器就會(huì)拒絕請(qǐng)求
params['_'] += 1
# 是什么不知道,但是每次都是按時(shí)間戳的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才繼續(xù)長輪訓(xùn),返回201,則收下頭像的圖片然后再發(fā)起一次長輪訓(xùn)(這部分代碼有點(diǎn)重復(fù),不過保證示例的整個(gè)過程清晰)。返回其他的code否退出循環(huán),這里正常會(huì)返回200。
上面的步驟最后會(huì)拿到一個(gè) redirect_uri ,值是一個(gè)url,可以直接訪問。不同實(shí)際在瀏覽器收到200返回碼之后發(fā)的請(qǐng)求的url有點(diǎn)小區(qū)別:
"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"
實(shí)際瀏覽器發(fā)送的請(qǐng)求會(huì)多兩個(gè)參數(shù),
如果用默認(rèn)的 redirect_uri 發(fā)送請(qǐng)求,返回的是一個(gè)html,這個(gè)應(yīng)該是Web微信的界面,但是不帶任何數(shù)據(jù),原因就是沒有認(rèn)證信息。
如果加上上面額外的參數(shù),則收到的信息像下面這個(gè)樣子:
0
@crypt_d1544694_9eb666666b490ff4444c94ab4444f0d2
tMlup2XXXXXX0pIp
1112345678
mFJdwSibpJ5R%2FbQ564HXXXXXOOOOO%2FEiEO86KPL3EI6F2poriL4OOOOOOXXXXXX%2B
1
上面這個(gè)就是XML格式的憑證,之后基于登錄后的操作,都要帶著憑證提交。類似Cookie,但是這里不用Cookie而是用這個(gè)。這里把XML也用BeautifulSoup解析一下,把憑證里所有的 key 、 value 保存為一個(gè)字典。
再發(fā)一次請(qǐng)求,redirect_uri 里加上2個(gè)參數(shù)。然后把返回的拼接解析后轉(zhuǎn)成字典打印出來:
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)
到此登錄告一段落,把最后的憑證保存好
在瀏覽器開發(fā)者模式的網(wǎng)絡(luò)分頁里,可以找到如下緊挨著的3個(gè)請(qǐng)求:
請(qǐng)求的代碼如下,拿到請(qǐng)求后要轉(zhuǎn)一下編碼,否則是亂碼:
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)
從返回的信息里看,有部分最近訂閱號(hào)和最近聯(lián)系人的信息。數(shù)據(jù)都是以JSON字符串的形式返回的。之后再繼續(xù)分析和處理之前,先執(zhí)行一步 jso.loads(r5.text)
反序列化轉(zhuǎn)成對(duì)象。
可用生成一個(gè)html來展示:
# 把頁面的內(nèi)容生成一個(gè)html來展示
import json
obj = json.loads(r5.text)
user = obj['User']
f = open('wx.html', 'w', encoding='utf-8')
f.write('\n')
f.write("Web 微信
\n")
f.write("用戶名:%s
\n" % user['NickName'])
contactList = obj['ContactList']
f.write("最近聯(lián)系人
\n")
f.write("\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("- %s
\n" % user_info)
f.write("
\n")
mpSubscribeMsgList = obj['MPSubscribeMsgList']
f.write("最近公眾號(hào)信息
\n")
f.write("\n")
for i in mpSubscribeMsgList:
# print(i)
f.write("- %s
\n" % i['NickName'])
f.write("\n")
for article in i['MPArticleList']:
f.write("- %s%s
\n" % (article['Url'], article['Title'], article['Digest']))
f.write("
\n")
f.write("
\n")
f.close()
這里拿到的信息只是概況,聯(lián)系人和公眾號(hào)都不全,都是最近的聯(lián)系人。
另外信息里面還有頭像和公眾號(hào)文章的圖片,下載沒問題,但是要在html里用img標(biāo)簽寫src是顯示不出來的。做了外鏈限制
繼續(xù)在瀏覽器開發(fā)者模式的網(wǎng)絡(luò)分頁里找,在憑證的后面是上面的POST的初始化請(qǐng)求webwxinit。繼續(xù)往后找,主要看響應(yīng)體,有很多圖片的請(qǐng)求是可以跳過的,都是下載頭像之類的。找到返回內(nèi)容最長的那個(gè)應(yīng)該就是聯(lián)系人列表了。另外還有一個(gè)返回的內(nèi)容也很多,可能是公眾號(hào),不過這里不管那個(gè)了。
獲取聯(lián)系人列表的代碼:
# 獲取所有聯(lián)系人信息,這個(gè)請(qǐng)求是會(huì)驗(yàn)證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 自動(dòng)獲取到的編碼是錯(cuò)的
# print(r6.apparent_encoding)
r6.encoding = "utf-8" # 直接指定"utf-8"就對(duì)了
# 自動(dòng)獲取到的編碼是"Windows-1254"這個(gè)是別名,正式名稱是"cp1254"。
# 寫哪個(gè)都一樣的,不過問題是,不能用,編碼是錯(cuò)的,大概就是誤導(dǎo)我們的
# Python36/Lib/encodings/aliases.py 這個(gè)文件里有所有編碼的別名的對(duì)應(yīng)關(guān)系
print(r6.text)
with open('contact.txt', 'w', encoding='utf-8') as f:
f.write(r6.text)
這里有幾個(gè)坑:
之后先要分析一波聯(lián)系人,把返回的內(nèi)容先保存到本地,之后不用再反復(fù)去請(qǐng)求了。
對(duì)文件的內(nèi)容解析,先看下有哪些字段:
import json
with open('contact.txt', encoding='utf-8') as f:
obj = json.load(f)
for i in obj:
print(i)
一共就4個(gè)key:
進(jìn)行到這里,已經(jīng)對(duì)自己所有的聯(lián)系人進(jìn)行一波統(tǒng)計(jì)分析了。比如男女比例,地區(qū)分布。不過數(shù)據(jù)分析不是這里的重點(diǎn)
到這里就不一點(diǎn)點(diǎn)分析了,下面的代碼,就能發(fā)消息了(中文還有問題):
# 找到聯(lián)系人信息
name = "這里填聯(lián)系人的名字"
msg = "Hello" # 發(fā)中文會(huì)有亂碼,不過這個(gè)是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("未找到聯(lián)系人")
# 發(fā)消息
url = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg"
params = {
'lang': 'zh_CN',
'pass_ticket': ticket['pass_ticket'],
}
# 這個(gè)字典之前用過,之前里面只有BaseRequest
# 現(xiàn)在保留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, # 這個(gè)是消息類型,1是文本
}
json_data['Scene'] = 0 # 不知道是啥,照著寫
r7 = s.post(url=url, params=params, json=json_data)
print(r7.text)
中文亂碼問題
如果發(fā)送“你好”,對(duì)方會(huì)收到“\u4f60\u597d”,這個(gè)是中文的Unicode編碼,是在json.dumps里變的:
>>> import json
>>> json.dumps("你好")
'"\\u4f60\\u597d"'
>>> json.dumps("Hello")
'"Hello"'
>>> json.dumps("你好", ensure_ascii=False)
'"你好"'
>>
中文在json序列化的時(shí)候,默認(rèn)會(huì)轉(zhuǎn)成Unicode,不過可以加上ensure_ascii參數(shù)不轉(zhuǎn)。
之前自己做寫django項(xiàng)目的時(shí)候,如果客戶端 josn.dumps 了,服務(wù)端再 json.loads 一下,中文就回來了?,F(xiàn)在服務(wù)端是人家的,只能讓客戶端不要對(duì)中文進(jìn)行轉(zhuǎn)碼
自己做json序列化就不能把參數(shù)傳給json了,否則還會(huì)把json字符串再序列化一次。data參數(shù)和json參數(shù)都是請(qǐng)求體,傳給json參數(shù)后,原本requests會(huì)幫我做一些事情,現(xiàn)在要自定義就得自己調(diào)整了。把自己序列化后的字符串傳給data,data就原樣接收了。但是要讓服務(wù)端把請(qǐng)求體(body)的內(nèi)容作為json字符串處理。修改請(qǐng)求頭的 'Content-Type' 的值。改一下之前的POST請(qǐng)求:
# r7 = s.post(url=url, params=params, json=json_data) # 這個(gè)不能發(fā)中文
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')
處理一下,否則會(huì)報(bào)錯(cuò)。如果直接給字符串的話,最終會(huì)執(zhí)行 body.encode("latin-1")
,這個(gè)編譯不了,所以就報(bào)錯(cuò)了,錯(cuò)誤信息會(huì)有提示。另外參考下面requests里的這小段代碼,json序列化之后,也是把字符串用encode轉(zhuǎn)成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')
下面是發(fā)送成功后返回的消息:
{
"BaseResponse": {
"Ret": 0,
"ErrMsg": ""
},
"MsgID": "9025779609933123936",
"LocalID": "1540098759694.243"
}
還是看瀏覽器開發(fā)者模式的網(wǎng)絡(luò)分頁,里面還是會(huì)有一個(gè)長輪訓(xùn)。不過實(shí)際上沒那么簡單,這里至少要處理2個(gè)請(qǐng)求。一個(gè)是長輪訓(xùn)請(qǐng)求,會(huì)有2種返回狀態(tài):
消息同步的POST請(qǐng)求會(huì)接收收到的消息,也可能是0條消息,但是還是得同步一次,否則長輪訓(xùn)會(huì)一直返回2。另外最初的 SyncKey 只有4個(gè),在 POST 之后還會(huì)多2個(gè),最好也更新到之后的請(qǐng)求里。
另外消息發(fā)送人和接收人,收到的都是一串類似id的東西,這個(gè)要去之前的聯(lián)系人列表里查找 "UserName" 然后獲取 "NickName" 。這里沒做,只是簡單的把發(fā)送人的id打印出來了。這個(gè)id不是固定的,每次連接web微信,返回的聯(lián)系人列表的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', # 這個(gè)一直在變,貌似沒啥影響
'_': int(time.time() * 1000) - 26846,
}
print("持續(xù)接收消息")
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("發(fā)起 r8 長輪訓(xùn)")
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"}' 則繼續(xù)長輪訓(xùn)
# 返回 'window.synccheck={retcode:"0",selector:"2"}' 則發(fā)起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的基礎(chǔ)上加一個(gè)SyncKey字典
r9 = s.post(sync_url, params=sync_params, json=json_data)
# r9.encoding = r9.apparent_encoding
print(r9.apparent_encoding) # 自動(dòng)獲取到的編碼還是有問題
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"] # 這里會(huì)多2條SyncKey
這里還有個(gè)坑,如果代碼運(yùn)行起來之后,馬上就有消息進(jìn)來(對(duì)方回復(fù)的太快),我測的時(shí)候會(huì)發(fā)生異常。也沒找到啥原因,而且如果是等一下再有消息來跑著也很正常。最后就用try把異常捕獲處理了。
另外消息數(shù)量會(huì)累加,可能還有一個(gè)已讀消息的請(qǐng)求,這個(gè)沒有繼續(xù)深入。
另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)scvps.cn,海內(nèi)外云服務(wù)器15元起步,三天無理由+7*72小時(shí)售后在線,公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國服務(wù)器、虛擬主機(jī)、免備案服務(wù)器”等云主機(jī)租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡單易用、服務(wù)可用性高、性價(jià)比高”等特點(diǎn)與優(yōu)勢,專為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應(yīng)用場景需求。