您好,登錄后才能下訂單哦!
之前講過這個,在這里:https://blog.51cto.com/steed/2071271
不過當時沒講透,這次再展開一點點。
Web服務的通信本質上就是通過socket發送字符串請求,然后也會返回響應。
發送的請求有請求頭和請求體。返回的響應也有響應頭和響應體。
格式:請求頭和請求體中間使用\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的登錄驗證使用的是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登錄地址: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個請求:
請求的代碼如下,拿到請求后要轉一下編碼,否則是亂碼:
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("<h2>Web 微信</h2>\n")
f.write("<h4>用戶名:%s</h4>\n" % user['NickName'])
contactList = obj['ContactList']
f.write("<h4>最近聯系人</h4>\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("<h4>最近公眾號信息</h4>\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)
這里有幾個坑:
之后先要分析一波聯系人,把返回的內容先保存到本地,之后不用再反復去請求了。
對文件的內容解析,先看下有哪些字段:
import json
with open('contact.txt', encoding='utf-8') as f:
obj = json.load(f)
for i in obj:
print(i)
一共就4個key:
進行到這里,已經對自己所有的聯系人進行一波統計分析了。比如男女比例,地區分布。不過數據分析不是這里的重點
到這里就不一點點分析了,下面的代碼,就能發消息了(中文還有問題):
# 找到聯系人信息
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種返回狀態:
消息同步的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把異常捕獲處理了。
另外消息數量會累加,可能還有一個已讀消息的請求,這個沒有繼續深入。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。