溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Python自動(dòng)化開(kāi)發(fā)學(xué)習(xí)-爬蟲(chóng)2

發(fā)布時(shí)間:2020-04-28 08:18:36 來(lái)源:網(wǎng)絡(luò) 閱讀:1449 作者:騎士救兵 欄目:編程語(yǔ)言

Web服務(wù)的本質(zhì)2

之前講過(guò)這個(gè),在這里:https://blog.51cto.com/steed/2071271
不過(guò)當(dāng)時(shí)沒(méi)講透,這次再展開(kāi)一點(diǎn)點(diǎn)。
Web服務(wù)的通信本質(zhì)上就是通過(guò)socket發(fā)送字符串請(qǐng)求,然后也會(huì)返回響應(yīng)。
發(fā)送的請(qǐng)求有請(qǐng)求頭和請(qǐng)求體。返回的響應(yīng)也有響應(yīng)頭和響應(yīng)體。

  • 請(qǐng)求頭:就是requests.hearders,瀏覽器后臺(tái)標(biāo)頭里的請(qǐng)求標(biāo)頭
  • 請(qǐng)求體:就是POST請(qǐng)求的data(或者是json),瀏覽器后臺(tái)正文里的請(qǐng)求正文。GET請(qǐng)求沒(méi)有請(qǐng)求體
  • 響應(yīng)頭:就是response.headers,瀏覽器后臺(tái)標(biāo)頭里的響應(yīng)標(biāo)頭
  • 響應(yīng)體:就是返回的html代碼,response.content,瀏覽器后臺(tái)正文里的響應(yīng)正文。響應(yīng)301跳轉(zhuǎn)等會(huì)沒(méi)有響應(yīng)體

格式:請(qǐng)求頭和請(qǐng)求體中間使用\r\n\r\n分隔。而請(qǐng)求頭之間會(huì)使用\r\n來(lái)分隔。響應(yīng)頭和響應(yīng)體類似。

改寫(xiě)一下當(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)體,不過(guò)301跳轉(zhuǎn)不需要響應(yīng)體就不寫(xiě)了:

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)頁(yè)的內(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)聽(tīng)
    while True:
        conn, addr = server.accept()  # 開(kāi)啟監(jiān)聽(tīng)
        handle_request(conn)  # 將連接傳遞給handle_request函數(shù)處理
        conn.close()  # 關(guān)閉連接

if __name__ == '__main__':
    main()

上面的socket啟動(dòng)之后,使用瀏覽器訪問(wèn),會(huì)跳轉(zhuǎn)到指定的頁(yè)面,并且能在后臺(tái)查看到自定義的響應(yīng)頭的內(nèi)容。

示例

再補(bǔ)充一個(gè)登錄GitHub的示例,這個(gè)是Form表單驗(yàn)證的。

登錄GitHub

GitHub的登錄驗(yàn)證使用的是Form表單。
驗(yàn)證登錄是否成功可以訪問(wèn)這個(gè)頁(yè)面:https://github.com/settings/profile
如果沒(méi)有登錄,會(huì)跳轉(zhuǎn)到登錄頁(yè)面。如果頁(yè)面正常打開(kāi)了,并且能讀取到里面的用戶信息了,說(shuō)明登錄認(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)  # 登錄成功返回的頁(yè)面
r3 = s.get('https://github.com/settings/profile')
r3.encoding = r3.apparent_encoding  # 獲取頁(yè)面的編碼,解決亂碼問(wèn)題
bs3 = BeautifulSoup(r3.text, features='html.parser')
title = bs3.find('title')
print(title)  # 用戶信息頁(yè)面的title
name = bs3.find('input', id="user_profile_name")
print(name.get('value'))  # 用戶的 Name

判斷登錄是否成功

這里講的對(duì)于GitHub這個(gè)網(wǎng)站不適用。
一般Form表單驗(yàn)證的頁(yè)面,如果驗(yàn)證失敗會(huì)刷新當(dāng)前頁(yè)面。如果驗(yàn)證成功,則會(huì)發(fā)一個(gè)跳轉(zhuǎn)。如果是跳轉(zhuǎn)的機(jī)制,就可以通過(guò)這個(gè)來(lái)判斷是否驗(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 微信

Web登錄地址:https://wx.qq.com/
頁(yè)面打開(kāi)后,會(huì)顯示一個(gè)二維碼,需要我們有手機(jī)微信掃一下。手機(jī)授權(quán)后,頁(yè)面會(huì)自動(dòng)跳轉(zhuǎn)完成登錄。這里雖然沒(méi)有我們?cè)跒g覽器上操作,但是一旦手機(jī)授權(quán)后,頁(yè)面就會(huì)自動(dòng)跳轉(zhuǎn)。這里是用長(zhǎng)輪訓(xùn)的方法持續(xù)想服務(wù)器提交請(qǐng)求,直到收到服務(wù)器返回后執(zhí)行后會(huì)的操作。

長(zhǎng)輪訓(xùn)

先看一下長(zhǎng)輪詢?cè)诤笈_(tái)的請(qǐng)求:
Python自動(dòng)化開(kāi)發(fā)學(xué)習(xí)-爬蟲(chóng)2

長(zhǎng)輪詢:客戶端向服務(wù)器發(fā)送Ajax請(qǐng)求,服務(wù)器接到請(qǐng)求后hold住連接,直到有新消息才返回響應(yīng)信息并關(guān)閉連接,客戶端處理完響應(yīng)信息后再向服務(wù)器發(fā)送新的請(qǐng)求。
優(yōu)點(diǎn):在無(wú)消息的情況下不會(huì)頻繁的請(qǐng)求,耗費(fèi)資源小。
缺點(diǎn):服務(wù)器hold連接會(huì)消耗資源,返回?cái)?shù)據(jù)順序無(wú)保證,難于管理維護(hù)。
實(shí)例:WebQQ、Hi網(wǎng)頁(yè)版、Facebook IM。

合理選擇“心跳”頻率:
這里必須由客戶端不停地進(jìn)行請(qǐng)求來(lái)維持,所以在客戶端和服務(wù)器間保持正常的“心跳”至為關(guān)鍵,間隔時(shí)間應(yīng)小于WEB服務(wù)器的超時(shí)時(shí)間,一般建議在10~20秒左右。上面的截圖里是25秒。
長(zhǎng)輪訓(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。
但是用爬蟲(chóng)直接爬 https://wx.qq.com/ 頁(yè)面的時(shí)候,返回的img標(biāo)簽里找不到這個(gè)關(guān)鍵的uuid。事實(shí)上哪里都沒(méi)找到。uuid是通過(guò)另外一個(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)顯示是沒(méi)有響應(yīng)體的,可能是沒(méi)有沒(méi)有解析成功。用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è)長(zhǎng)輪訓(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)碼的方法,如果是寫(xiě)前端,直接把這段編碼設(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è)來(lái)控制長(zhǎng)時(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ā)送長(zhǎng)輪訓(xùn)的階段,等待手機(jī)再點(diǎn)一下登錄授權(quán)?,F(xiàn)在的這個(gè)長(zhǎng)輪訓(xùn)和之前的長(zhǎng)輪訓(xùn)是一樣的,也就是上面的代碼不需要退出while循環(huán),而是在判斷返回的code是201的時(shí)候,拿到頭像,然后還是繼續(xù)循環(huán)發(fā)送長(zhǎng)輪詢,等手機(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ā)送長(zhǎng)輪詢
        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è)來(lái)控制長(zhǎng)時(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ù)長(zhǎng)輪訓(xùn),返回201,則收下頭像的圖片然后再發(fā)起一次長(zhǎng)輪訓(xùn)(這部分代碼有點(diǎn)重復(fù),不過(guò)保證示例的整個(gè)過(guò)程清晰)。返回其他的code否退出循環(huán),這里正常會(huì)返回200。

驗(yàn)證的憑證

上面的步驟最后會(huì)拿到一個(gè) redirect_uri ,值是一個(gè)url,可以直接訪問(wèn)。不同實(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ù),原因就是沒(méi)有認(rèn)證信息。
如果加上上面額外的參數(shù),則收到的信息像下面這個(gè)樣子:

<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>

上面這個(gè)就是XML格式的憑證,之后基于登錄后的操作,都要帶著憑證提交。類似Cookie,但是這里不用Cookie而是用這個(gè)。這里把XML也用BeautifulSoup解析一下,把憑證里所有的 key 、 value 保存為一個(gè)字典。
再發(fā)一次請(qǐng)求,redirect_uri 里加上2個(gè)參數(shù)。然后把返回的拼接解析后轉(zhuǎn)成字典打印出來(lái):

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)

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

獲取用戶信息

在瀏覽器開(kāi)發(fā)者模式的網(wǎng)絡(luò)分頁(yè)里,可以找到如下緊挨著的3個(gè)請(qǐng)求:

  • 響應(yīng) redirect_uri 的 GET 請(qǐng)求,手機(jī)掃碼再點(diǎn)登陸后返回 code=200 和 redirect_uri 。上上節(jié)做的
  • 響應(yīng) XML 憑證的 GET 請(qǐng)求,向 redirect_uri 提交請(qǐng)求拿到憑證。上一節(jié)做的
  • 獲取用戶信息的 POST 請(qǐng)求。現(xiàn)在要處理的,要把憑證的信息加到 url 參數(shù)以及 POST 的請(qǐng)求體里。

請(qǐng)求的代碼如下,拿到請(qǐng)求后要轉(zhuǎn)一下編碼,否則是亂碼:

url = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit"
params = {
    # 'r': '1976951002',  # 這是什么不知道,不加也沒(méi)問(wèn)題
    '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來(lái)展示:

# 把頁(yè)面的內(nèi)容生成一個(gè)html來(lái)展示
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>最近聯(lián)系人</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>最近公眾號(hào)信息</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()

這里拿到的信息只是概況,聯(lián)系人和公眾號(hào)都不全,都是最近的聯(lián)系人。
另外信息里面還有頭像和公眾號(hào)文章的圖片,下載沒(méi)問(wèn)題,但是要在html里用img標(biāo)簽寫(xiě)src是顯示不出來(lái)的。做了外鏈限制

獲取聯(lián)系人列表

繼續(xù)在瀏覽器開(kāi)發(fā)者模式的網(wǎng)絡(luò)分頁(yè)里找,在憑證的后面是上面的POST的初始化請(qǐng)求webwxinit。繼續(xù)往后找,主要看響應(yīng)體,有很多圖片的請(qǐng)求是可以跳過(guò)的,都是下載頭像之類的。找到返回內(nèi)容最長(zhǎng)的那個(gè)應(yīng)該就是聯(lián)系人列表了。另外還有一個(gè)返回的內(nèi)容也很多,可能是公眾號(hào),不過(guò)這里不管那個(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"。
# 寫(xiě)哪個(gè)都一樣的,不過(guò)問(wèn)題是,不能用,編碼是錯(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è)坑:

  • 這個(gè)請(qǐng)求需要Cookie,一直使用最開(kāi)始的Session對(duì)象的就不會(huì)有問(wèn)題
  • 編碼問(wèn)題,apparent_encoding拿到的不對(duì),直接指定"utf-8"

之后先要分析一波聯(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:

  • BaseResponse,沒(méi)啥用
  • MemberCount,一共有多少聯(lián)系人
  • MemberList,一個(gè)列表,列表里面是一個(gè)個(gè)字典,每個(gè)字典就是一個(gè)聯(lián)系人信息
  • Seq,也是沒(méi)啥用

進(jìn)行到這里,已經(jīng)對(duì)自己所有的聯(lián)系人進(jìn)行一波統(tǒng)計(jì)分析了。比如男女比例,地區(qū)分布。不過(guò)數(shù)據(jù)分析不是這里的重點(diǎn)

發(fā)送消息

到這里就不一點(diǎn)點(diǎn)分析了,下面的代碼,就能發(fā)消息了(中文還有問(wèn)題):

# 找到聯(lián)系人信息
name = "這里填聯(lián)系人的名字"
msg = "Hello"  # 發(fā)中文會(huì)有亂碼,不過(guò)這個(gè)是json序列化的問(wèn)題
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è)字典之前用過(guò),之前里面只有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  # 不知道是啥,照著寫(xiě)
r7 = s.post(url=url, params=params, json=json_data)
print(r7.text)

中文亂碼問(wèn)題
如果發(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,不過(guò)可以加上ensure_ascii參數(shù)不轉(zhuǎn)。
之前自己做寫(xiě)django項(xiàng)目的時(shí)候,如果客戶端 josn.dumps 了,服務(wù)端再 json.loads 一下,中文就回來(lái)了?,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"
}

接收消息

還是看瀏覽器開(kāi)發(fā)者模式的網(wǎng)絡(luò)分頁(yè),里面還是會(huì)有一個(gè)長(zhǎng)輪訓(xùn)。不過(guò)實(shí)際上沒(méi)那么簡(jiǎn)單,這里至少要處理2個(gè)請(qǐng)求。一個(gè)是長(zhǎng)輪訓(xùn)請(qǐng)求,會(huì)有2種返回狀態(tài):

  • 'window.synccheck={retcode:"0",selector:"0"}' : 繼續(xù)下一次長(zhǎng)輪訓(xùn)
  • 'window.synccheck={retcode:"0",selector:"2"}' : 則發(fā)起另外一個(gè)POST的消息同步請(qǐng)求

消息同步的POST請(qǐng)求會(huì)接收收到的消息,也可能是0條消息,但是還是得同步一次,否則長(zhǎng)輪訓(xùn)會(huì)一直返回2。另外最初的 SyncKey 只有4個(gè),在 POST 之后還會(huì)多2個(gè),最好也更新到之后的請(qǐng)求里。
另外消息發(fā)送人和接收人,收到的都是一串類似id的東西,這個(gè)要去之前的聯(lián)系人列表里查找 "UserName" 然后獲取 "NickName" 。這里沒(méi)做,只是簡(jiǎn)單的把發(fā)送人的id打印出來(lái)了。這個(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è)一直在變,貌似沒(méi)啥影響
    '_': 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 長(zhǎng)輪訓(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ù)長(zhǎng)輪訓(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)獲取到的編碼還是有問(wèn)題
        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)行起來(lái)之后,馬上就有消息進(jìn)來(lái)(對(duì)方回復(fù)的太快),我測(cè)的時(shí)候會(huì)發(fā)生異常。也沒(méi)找到啥原因,而且如果是等一下再有消息來(lái)跑著也很正常。最后就用try把異常捕獲處理了。
另外消息數(shù)量會(huì)累加,可能還有一個(gè)已讀消息的請(qǐng)求,這個(gè)沒(méi)有繼續(xù)深入。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI