Python 爬虫五 进阶案例-web微信登陆与消息发送
首先回顧下網(wǎng)頁微信登陸的一般流程
1、打開瀏覽器輸入網(wǎng)址
2、使用手機微信掃碼登陸
3、進入用戶界面
1、打開瀏覽器輸入網(wǎng)址
首先打開瀏覽器輸入web微信網(wǎng)址,并進行監(jiān)控:
https://wx.qq.com/
可以發(fā)現(xiàn)網(wǎng)頁中包含了一個新的url,而這個url就是二維碼的來源。
https://login.weixin.qq.com/qrcode/wbfd1Z-a0g==
可以猜測一下獲取url的一般網(wǎng)址就是https://login.weixin.qq.com/qrcode,而wbfd1Z-a0g==肯定就是通過請求前面的url后傳入的參數(shù),最終生成二維碼圖片。
此時再監(jiān)控此次請求的network,去尋找,究竟是什么時候返回給客戶端wbfd1Z-a0g==字符串的。
最終在網(wǎng)址: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&_=1532592134738的response里面找到了二維碼參數(shù)。
這個網(wǎng)址里面_=1532592134738里面?zhèn)魅氲目粗褚粋€時間戳。此時我們先新建一個Django項目,在login頁面向上面的網(wǎng)址發(fā)送請求。取得二維碼參數(shù),再傳給前臺的img標(biāo)簽,就可以做出二維碼登陸頁面。
新建一個Django項目、并新建一個app wechat:
將主url路由指向wechat app里面的url, 并寫好一個login url,讓用戶從我們這邊開始登陸微信。
wechat urls:
views內(nèi)的login函數(shù):
from django.shortcuts import render, HttpResponse, redirect
import requests, time
import re, json
from bs4 import BeautifulSoup
# Create your views here.
def login(req):
ctime = time.time() * 1000 # 模擬一個相同的時間戳
base_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&_={0}'
url = base_url.format(ctime) # 字符串拼接,生成新的url
response = requests.get(url) # 向新的url發(fā)送get請求
xcode_list = re.findall('window.QRLogin.uuid = "(.*)";', response.text) # 通過正則表達式,提取到對于的參數(shù)列表['YZzTdz9m_A==', 'YZzTdz9m_A==']
req.session['xcode'] = xcode_list[0] # 獲取到參數(shù),存入session內(nèi)
return render(req, 'wechat/login.html', {'xcode': xcode_list[0]}) # 返回給login頁面此參數(shù)
在templates目錄里面創(chuàng)建一個wechat目錄,在wechat目錄創(chuàng)建一個login.html頁面,傳入?yún)?shù)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<img id="xcode" src="https://login.weixin.qq.com/qrcode/{{ xcode }}">
</body>
</html>
因為加入了session功能需要先run一下
python3 manage.py makemigrations python3 mange.py migrate
運行并訪問login:
此時,就完成第一個二維碼頁面了。
第二步,有了二維碼,手機掃描是如何進行交互的?是不是這個網(wǎng)頁有個后臺一直在請求某個網(wǎng)頁,一旦掃碼驗證就進行下一步操作?繼續(xù)監(jiān)控請求
思路漸漸清晰了,在請求登陸的時候大概是如下的流程:
打開一個失效的url:
再查看掃描完的URL的返回:
最后確認(rèn)登陸:
再次觀察,會發(fā)現(xiàn)這個url其實是一個長輪詢,一直在請求認(rèn)證結(jié)果。pending的是在等待的當(dāng)前請求。而在我們的項目內(nèi)部,因為瀏覽器存在一個同源策略,所以無法完成在前端直接獲取結(jié)果。此時改變一下思路:可以通過login內(nèi)部寫個url 偷偷的訪問后端的視圖,視圖層再通過requests模塊去發(fā)送請求獲取結(jié)果返回給前端。
另外關(guān)于url的三種返回:
1、408代表沒有人掃碼(需要重新發(fā)送) 2、201代表已經(jīng)有用戶掃碼,后面的參數(shù)返回的是用戶頭像 3、200代表用戶已經(jīng)登陸
定義一下檢查的url:
path('check_login/', views.check_login),
login.html頁面加入javascript
<script src="/static/jquery.min.js"></script>
<script>
tip = 1
function checkLogin() {
$.ajax({
url:'/wechat/check_login/',
data: {'tip': tip},
type: 'GET',
dataType: 'JSON',
success:function (arg) {
if (arg.code == 201){
// 有人掃碼了
$('#xcode').attr('src', arg.data);
checkLogin();
tip = 0;
}else if (arg.code == 408){
checkLogin();
}else if (arg.code == 200){
window.location.href='/wechat/index/'
}
}
})
}
checkLogin();
</script>
這邊需要提醒的是當(dāng)用戶掃碼之后的url發(fā)送的參數(shù)中有一個tip參數(shù)有變動,從1變成0.
def check_login(req):
tip = req.GET.get('tip') # 標(biāo)記是否掃碼
# 自定義返回的json數(shù)據(jù)格式
ret = {
'code': 408, # 初始值408代表沒有任何操作
'data': None
}
ctime = time.time() * 1000 # 根據(jù)得到的url,偽造匹配的時間戳
base_url = 'https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid={0}&tip={1}&r=903313058&_={2}'
url1 = base_url.format(req.session['xcode'], tip, ctime) # 字符串修飾
r1 = requests.get(url1) # 獲取響應(yīng)
if 'window.code=201' in r1.text:
# 有人掃碼
v = re.findall("window.userAvatar = '(.*)';", r1.text)
avatar = v[0]
ret['code'] = 201 # 狀態(tài)碼,代表有人掃碼了
ret['data'] = avatar # 用戶頭像
elif 'window.code=200;' in r1.text:
# 掃碼之后,點擊確定登錄
req.session['login_cookie'] = r1.cookies.get_dict() # 獲取確認(rèn)登陸的cookie
uri = re.findall('window.redirect_uri="(.*)";', r1.text) # 之前的圖片,已經(jīng)發(fā)現(xiàn)這里是一個重定向路由,所以獲取重定向的路由
# https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=AfpJ_ZmEJGcJ8iU62SiPyuTo@qrticket_0&uuid=gZHgYImvDQ==&lang=zh_CN&scan=1532410951
redirect_url = '{0}&fun=new&version=v2'.format(uri[0]) # 字符串拼接,形成新的url
# 獲取憑證
r2 = requests.get(redirect_url) # 網(wǎng)頁重定向的時候,會返回憑證用來進行之后的驗證
ticket_dict = {}
soup = BeautifulSoup(r2.text, 'html.parser') # 標(biāo)簽文本實例化bs對象
for item in soup.find(name='error').children: # 找到需要的憑證
ticket_dict[item.name] = item.text # 憑證存入字典
req.session['ticket_dict'] = ticket_dict # 憑證存入session
req.session['ticket_cookie'] = r2.cookies.get_dict() # session中存入獲取重定向的cookie
ret['code'] = 200 # 200表示確定登陸了
req.session['is_login'] = True # 給后面的url判斷是否登陸
return HttpResponse(json.dumps(ret)) # Json序列化返回
最后創(chuàng)建一個新的路由:
path('index/', views.index),
創(chuàng)建一個新的html和視圖函數(shù)index:
def index(req):
return render(req, 'wechat/index.html')
此時,便可以完成web微信登陸功能。
留一份確認(rèn)登陸的憑證:
<error>
<ret>0</ret>
<message></message>
<skey>@crypt_41bad7c6_8bff0cc40ih18fba431b5a7e87l63bc7</skey>
<wxsid>LkgBf9UVGIkHdg80</wxsid>
<wxuin>15749213440</wxuin>
<pass_ticket>QvkUzyBHDWQ8HJDowkqw6rmqQ48weyrwDiNPTbn%2FnZl7tk%3D</pass_ticket>
<isgrayscale>1</isgrayscale>
</error>
獲取登陸信息
根據(jù)平時登陸web wechat的常識,登陸確認(rèn)之后肯定是跳往目的路由,初始化一些用戶信息,獲取用戶的各種資料等等。這只是猜測怎么辦?打開web 微信繼續(xù)檢測network。這一次從確認(rèn)登陸完成進行截圖分析。
url: https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=696489291&lang=zh_CN&pass_ticket=QvkUzyBiMqnrFcw6rmqQ4I7E2DiNxTUDPTbn%252FnZl7tk%253D
url里面有pass_ticket參數(shù).
看到憑證組成的form_data,另外此次是POST請求。由于response數(shù)據(jù)太多了,在index里面我們先寫一個for循環(huán)看看都有哪些key值,看看能不能根據(jù)key值的命名規(guī)范,找到規(guī)律。
視圖函數(shù)index:
def index(req):
# 判斷是否已經(jīng)登陸
if not req.session.get('is_login'):
return redirect('/wechat/login/')
# 發(fā)送post請求,根據(jù)ticket_dict進行構(gòu)造數(shù)據(jù)
# https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=892259194&pass_ticket=TS7TEfumVaVzKhn%252FrnLKS2zZyhixJDEYxlXqGgQVplQ%253D
base_url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=892259194&pass_ticket={0}'
url = base_url.format(req.session['ticket_dict']['pass_ticket'])
# BaseRequest
# :
# {Uin: "184513440", Sid: "U4WojQwDRwKqdeMs", Skey: "", DeviceID: "e409571391728320"} # 偽造的數(shù)據(jù)格式樣板
form_data = { # 偽造數(shù)據(jù)
'BaseRequest': {
'DeviceID': "e409571391728320",
'Sid': req.session['ticket_dict']['wxsid'],
'Skey': req.session['ticket_dict']['skey'],
'Uin': req.session['ticket_dict']['wxuin']
}
}
r1 = requests.post(
url=url,
json=form_data
)
r1.encoding = r1.apparent_encoding # 使用默認(rèn)編碼原則
user_info = json.loads(r1.content)
for key in user_info:
print(key)
return render(req, 'wechat/index.html', {"user_info": user_info})
結(jié)果:
BaseResponse Count ContactList SyncKey User ChatSet SKey ClientVersion SystemTime GrayScale InviteStartCount MPSubscribeMsgCount MPSubscribeMsgList ClickReportInterval
可以猜測ContactList應(yīng)該是最近聯(lián)系人;User應(yīng)該是用戶本身,MPSubscribeMsgList應(yīng)該是訂閱號信息。
再打印一下User里面的key看一下,視圖函數(shù)改一句就好了:
for key in user_info['User']:
print(key)
結(jié)果key:
Uin UserName NickName HeadImgUrl RemarkName PYInitial PYQuanPin RemarkPYInitial RemarkPYQuanPin HideInputBarFlag StarFriend Sex Signature AppAccountFlag VerifyFlag ContactFlag WebWxPluginSwitch HeadImgFlag SnsFlag
果然,現(xiàn)在這么看應(yīng)該是登陸用戶的信息了。
同理測試一下訂閱號:
{
'UserName': '@8fbbad518f4d3cf22ebd0a1b0b6d2cb5',
'MPArticleCount': 4,
'MPArticleList': [
{
'Title': '80%的愛情還沒開始就會終結(jié)。關(guān)于錯過的遺憾,20 年前幾米就教給你了',
'Digest': '你活得不開心,因為你太把自己當(dāng)大人。',
'Cover': 'http://mmbiz.qpic.cn/mmbiz_jpg/ib0l8DHhOSLuh527WuVHWnnIXwxR5s37pgacZriaKQPUkKEqArKEa9v6tH8buhrNHZB3IMMgf33RCKxtziazCTPrQ/640?wxtype=jpeg&wxfrom=0',
'Url': 'http://mp.weixin.qq.com/s?__biz=MzIzMzk1MzE0NQ==&mid=2247485983&idx=1&sn=6e268e95cdaf04c21114b53f1285bba4&chksm=e8fc8809df8b011fba6a8092400b312102e247780168d56f69c6863e31e7c4f3945253b1f259&scene=0#rd'
},
{...}
],
'Time': 1532608404,
'NickName': '新世相讀書會'
}
此時我們對index進行處理:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>個人信息:{{ user_info.User.NickName }}</h1>
<ul>
{% for user in user_info.ContactList %}
<li>{{ user.NickName }}</li>
{% endfor %}
</ul>
<a href="/wechat/contact_all/">查看更多聯(lián)系人</a>
<h3>公眾號信息</h3>
{% for msg in user_info.MPSubscribeMsgList %}
<div>
<h3>{{ msg.NickName }}</h3>
<ul>
{% for article in msg.MPArticleList %}
<li><a href="{{ article.Url }}">{{ article.Title }}</a></li>
{% endfor %}
</ul>
</div>
{% endfor %}
</body>
</html>
中間建立了一個新的url,用來顯示所有聯(lián)系人。先建立一個路由跟視圖函數(shù),其他暫時不管。
稍微修改了一下視圖函數(shù),加了個User字典到session里面,后面要用到:
def index(req):
# 判斷是否已經(jīng)登陸
if not req.session.get('is_login'):
return redirect('/wechat/login/')
# 獲取最新聯(lián)系人、并展示
# 發(fā)送post請求,根據(jù)ticket_dict進行構(gòu)造數(shù)據(jù)
# https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=892259194&pass_ticket=TS7TEfumVaVzKhn%252FrnLKS2zZyhixJDEYxlXqGgQVplQ%253D
base_url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=892259194&pass_ticket={0}'
url = base_url.format(req.session['ticket_dict']['pass_ticket'])
# BaseRequest
# :
# {Uin: "184513440", Sid: "U4WojQwDRwKqdeMs", Skey: "", DeviceID: "e409571391728320"}
form_data = {
'BaseRequest': {
'DeviceID': "e409571391728320",
'Sid': req.session['ticket_dict']['wxsid'],
'Skey': req.session['ticket_dict']['skey'],
'Uin': req.session['ticket_dict']['wxuin']
}
}
r1 = requests.post(
url=url,
json=form_data
)
r1.encoding = r1.apparent_encoding
user_info = json.loads(r1.content)
req.session['current_user_info'] = user_info['User']
# for k, v in user_info.items():
# print(k, v)
# for user in user_info['ContactList']:
# print(user['NickName'])
# for msg in user_info['MPSubscribeMsgList']:
# print(msg['NickName'])
return render(req, 'wechat/index.html', {"user_info": user_info})
結(jié)果示意圖:
此時解決查看聯(lián)系人的問題,還是老方法,繼續(xù)監(jiān)控network。這里就不再賣關(guān)子了。getcontact這個就是獲取所有聯(lián)系人的url。
不必多說,繼續(xù)偽造:
path('contact_all/', views.contact_all),
視圖函數(shù):
def contact_all(req):
# https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?pass_ticket=z%252BTVxEioLl3A5arKy%252BUMHbeTME%252BmAkJEulNYIYgGcpw%253D&r=1532421128264&seq=0&skey=@crypt_41bed7c6_e213390395ab3a4ef54bfef5d004f719
base_url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?pass_ticket={0}&r={1}&seq=0&skey={2}'
url = base_url.format(
req.session['ticket_dict']['pass_ticket'],
time.time() * 1000,
req.session['ticket_dict']['skey'],
) # url拼接
all_cookies = {}
all_cookies.update(req.session['login_cookie'])
all_cookies.update(req.session['ticket_cookie']) # 帶入所有的cookies
r1 = requests.get(url, cookies=all_cookies)
r1.encoding = r1.apparent_encoding
contact_dict = json.loads(r1.content)
for item in contact_dict:
print(item)
# for item in contact_dict['MemberList']:
# # if item['RemarkName'] == '宇宙第一帥':
# # print(item)
return render(req, 'wechat/contact_all.html', {'contact_dict': contact_dict})
index.html
打印了所有的最外層的key:
BaseResponse MemberCount MemberList Seq
上面被注釋的那段代碼,可以用來查找你備注的某個人的信息。監(jiān)控里面看到的username其實是用戶在微信里面的唯一ID。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<div>
<h1>聯(lián)系人列表</h1>
<ul>
{% for item in contact_dict.MemberList %}
<li>{{ item.UserName|safe }} --- {{ item.NickName|safe }}</li>
{% endfor %}
</ul>
</div>
</body>
</html>
截圖:
好了,現(xiàn)在到最緊張的最后一步了,如何發(fā)送消息?
現(xiàn)在只需要偽造信息就好了,這里我們可以看見就是根據(jù)username的唯一id來發(fā)送信息的。
在contact網(wǎng)頁里面添加如下代碼:
<div>
<h1>發(fā)送消息</h1>
<p>接受者:<input type="text" id="recv" /></p>
<p>內(nèi)容:<input type="text" id="content" /></p>
<input type="button" id="btn" value="Send" />
</div>
<script src="/static/jquery.min.js"></script>
<script>
$(function() {
$('#btn').click(function() {
var recv = $('#recv').val();
var content = $('#content').val();
$.ajax({
url: '/wechat/send_msg/',
type: 'GET',
data: {'recv': recv, 'content': content},
success: function(arg) {
console.log(arg)
}
})
})
})
</script>
路由:
path('send_msg/', views.send_msg),
視圖函數(shù):
def send_msg(req):
recv = req.GET.get('recv')
content = req.GET.get('content')
# https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?pass_ticket=wtwzy%252F7fxQgJaTA511weqPXIkIGSJmZdCRATgZdIfYY%253D
base_url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?pass_ticket={0}'
url = base_url.format(req.session['ticket_dict']['pass_ticket'])
ctime = time.time() * 1000
form_data = { # 偽造數(shù)據(jù)格式
'BaseRequest': {
'DeviceID': "e939509344931677",
'Sid': req.session['ticket_dict']['wxsid'],
'Skey': req.session['ticket_dict']['skey'],
'Uin': req.session['ticket_dict']['wxuin']
},
'Msg': {
'ClientMsgId': ctime,
'Content': content,
'FromUserName': req.session['current_user_info']['UserName'],
'LocalID': ctime,
'ToUserName': recv,
'Type': 1, # 文本
},
'Scene': 0
}
all_cookies = {}
all_cookies.update(req.session['login_cookie'])
all_cookies.update(req.session['ticket_cookie']) # 帶入cookie,試驗過證明是需要的
r1 = requests.post(
url=url,
data=bytes(json.dumps(form_data, ensure_ascii=False), encoding='utf-8'),
cookies=all_cookies,
headers={
'Content-Type': 'application/json' # 這句話用來表示,需要序列化成json數(shù)據(jù);也可以去掉data跟headers直接用json=form_data來實現(xiàn)
}
)
print(r1.text)
return HttpResponse('.....')
測試一下:
em。。。。。。
對,然后被拉黑了。至于接收消息,發(fā)送圖片什么的其實都可以自己通過網(wǎng)絡(luò)監(jiān)控做到的。這里就不再多提了。
收工!
總結(jié)
以上是生活随笔為你收集整理的Python 爬虫五 进阶案例-web微信登陆与消息发送的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 坐标轴的旋转及绕某一点旋转后坐标值求解
- 下一篇: Windows系统配置OutLook邮箱