如果使用 Python3(Flask) 一步一步模拟一个网页微信客户端
Web Weixin Pipeline
+--------------+ +---------------+ +---------------+
| | | | | |
| Get UUID | | Get Contact | | Status Notify |
| | | | | |
+-------+------+ +-------^-------+ +-------^-------+
| | |
| +-------+ +--------+
| | |
+-------v------+ +-----+--+------+ +--------------+
| | | | | |
| Get QRCode | | Weixin Init +------> Sync Check <----+
| | | | | | |
+-------+------+ +-------^-------+ +-------+------+ |
| | | |
| | +-----------+
| | |
+-------v------+ +-------+--------+ +-------v-------+
| | Confirm Login | | | |
+------> Login +---------------> New Login Page | | Weixin Sync |
| | | | | | |
| +------+-------+ +----------------+ +---------------+
| |
|QRCode Scaned|
+-------------+
一、获取登录的二维码
1.1、打开浏览器输入下面网址
https://wx.qq.com/
按下F12
打开开发调试模式。
我们可以看到产生二维码的图片的URL为https://login.weixin.qq.com/qrcode/wbO9FUkKHg==
,但是需要后面的一个参数wbO9FUkKHg==
,这个随机码是怎么产生的呢,我们再继续寻找。
现在我们应该清楚了,浏览器先请求这个地址获取生成二维码的uuid
,然后再把uuid
传入之前的url
生成二维码。
1.2、梳理原理
获取uuid
的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&_=1548209541594
生成二维码的URL
如下,后面的参数就是上面URL
产生的uuid
:
https://login.weixin.qq.com/qrcode/weE4D106jA==
API | 获取 UUID |
---|---|
url | https://login.weixin.qq.com/jslogin |
method | POST |
data | URL Encode |
params | appid: 应用ID fun: new 应用类型 lang: zh_CN 语言 _: 时间戳 |
返回数据(String):
window.QRLogin.code = 200; window.QRLogin.uuid = "xxx"
注:这里的appid就是在微信开放平台注册的应用的AppID。网页版微信有两个AppID,早期的是
wx782c26e4c19acffb
,在微信客户端上显示为应用名称为Web微信
;现在用的是wxeb7ec651dd0aefa9
,显示名称为微信网页版
。
API | 生成二维码 |
---|---|
url | https://login.weixin.qq.com/l/ uuid |
method | GET |
1.3、代码实现
要实现这个功能,需要对 Python 的模块 Flask 有一些了解。
我们创建一个wechat.py
,代码如下:
#!/usr/bin/python3.6
# -*- coding: UTF-8 -*-
# wangzan18@126.com
# 2018-10-16
from flask import Flask, render_template
import time
import requests
import re
app = Flask(__name__)
@app.route('/login', methods=['GET', 'POST'])
def login():
"""
获取登录的二维码
:return:
"""
if request.method == 'GET':
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发送get请求
qcode = re.findall('uuid = "(.*)";', response.text)[0] # 获取二维码的参数uuid
return render_template('login.html', qcode=qcode) # 把uuid传给前端模板
else:
pass
if __name__ == '__main__':
app.run(debug=True, host="0.0.0.0")
因为要把参数传给前端模板,所以我们要创建模板文件,按照 Flask 的格式要求,我们在当前目录创建一个文件夹templates
,在文件夹下面创建login.html
,内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Wechat_login</title>
</head>
<body>
<div style="width: 300px;margin: 0 auto">
<h1 style="text-align: center">微信登录</h1>
<img id="img" style="height: 300px;width: 300px;" src="https://login.weixin.qq.com/qrcode/{{qcode}}" alt="">
</div>
</body>
</html>
1.4、启动测试
现在我们启动微信,访问http://192.168.1.86:5000/login
。
每次刷新都是可以变更二维码的。
二、扫码成功
2.1、扫码状态
目前我们扫码之后页面没有任何变化,那到底是因为触发了页面的变化呢,我们继续来探究,我们看到生成二维码之后,浏览器一直请求某个地址,这个地址其实就是服务器返回我们的扫码状态。
开始请求一直处于pending
状态,当在一定时间内(25s)探测不要用户扫码就中断,并给出一个返回值408
,然后发起下一个探测。
通过我们测试下来,返回值主要有三种状态,如下
- 用户为扫码:返回值为
window.code=408
; - 用户扫码,没有点击登录:返回值为
window.code=201;window.userAvatar = xxxx
; - 用户扫码,并点击登录:
window.code=200;window.redirect_uri= xxxx
。
API | 二维码扫描登录 |
---|---|
url | https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login |
method | GET |
params | tip: 1 未扫描 0 已扫描 uuid: xxx _: 时间戳 |
返回数据(String):
window.code=xxx;
xxx:
408 登陆超时
201 扫描成功
200 确认登录
当返回200时,还会有
window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=xxx&uuid=xxx&lang=xxx&scan=xxx";
2.2、原理状态梳理
用户没有扫码,超时状态。
用户扫码,没有登录,返回一个用户头像的数据,并且立刻重新发起请求,探测用户是否点击确定。
扫码之后头像的数据如下:
2.3、代码实现
探测是否有用户扫码的url
如下:
https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=YagAJmDN2w==&tip=0&r=-2025309768&_=1548213510530
url
里面主要的几个参数,如uuid
,时间戳等我们都是可以通过前面获取到的,在之前的wechat.py
增加一个函数check_login
和对应的 api 接口来检查扫码状态:
#!/usr/bin/python3.6
# -*- coding: UTF-8 -*-
# wangzan18@126.com
# 2018-10-16
from flask import Flask, render_template, request, session, jsonify
import time
import requests
import re
app = Flask(__name__)
app.secret_key = 'wangzan18'
@app.route('/login', methods=['GET', 'POST'])
def login():
"""
获取登录的二维码
:return:
"""
if request.method == 'GET':
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发送get请求
qcode = re.findall('uuid = "(.*)";', response.text)[0] # 获取二维码的参数uuid
session['qcode'] = qcode
return render_template('login.html', qcode=qcode) # 把uuid传给前端模板
else:
pass
@app.route('/check_login')
def check_login():
"""
检查用户是否扫码登录
:return:
"""
response = {'code': 408} # 默认用户没有扫码
qcode = session.get('qcode') # 获取用户前面获取的 uuid,用于放到下面的 url 里面
ctime = time.time() * 1000
check_url = "https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid={0}&tip=0&r=-1437802572&_={1}".format(qcode, ctime)
ret = requests.get(check_url) # 扫码返回值
if 'code=201' in ret.text:
# 扫码成功
src = re.findall("userAvatar = '(.*)';", ret.text)[0] # 获取扫码用户头像
response['code'] = 201 # 获取扫码的返回值,放到字典response
response['src'] = src # 获取用户头像数据,放到字典response
return jsonify(response)
if __name__ == '__main__':
app.run(debug=True, host="0.0.0.0")
函数已经写好了,那函数怎么调用了,我们需要在login.html
增加一段ajax代码,去请求调用我们api接口,login.html
代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Wechat_login</title>
</head>
<body>
<div style="width: 300px;margin: 0 auto">
<h1 style="text-align: center">微信登录</h1>
<img id="img" style="height: 300px;width: 300px;" src="https://login.weixin.qq.com/qrcode/{{qcode}}" alt="">
</div>
<script src="/static/jquery-1.12.4.js"></script>
<script>
$(function () {
checkLogin();
})
function checkLogin() {
$.ajax({
url:'/check_login',
type:'GET',
dataType:'JSON',
success:function (arg) {
if(arg.code === 201){
// 扫码
$('#img').attr('src',arg.src);
checkLogin();
}else{
checkLogin();
}
}
})
}
</script>
</body>
</html>
我们在ajax代码中看到这里已经写了,用户扫码登录之后就跳转到index
,目前这个我们还没有写,先不去验证。
注意代码里面我们使用到一个/static/jquery-1.12.4.js
,这个是一个公共标准的 js 文件,大家可以去互联网获取。
2.4、扫码验证
我们同样打开我们的地址,并且打开F12
调试按钮,然后进行一下扫码,看看如何变化。
我们可以看到,浏览器一直在进行探测登录状态,和官方的一样,那我们扫码查看一下。
我们可以看到,扫码之后自动获取了头像并且展示出来,因为我们还没有设定登录成功之后的 api,即使登录也不会有什么跳转。
三、确认登录
3.1、页面调试
我们继续打开官方的微信地址,并且打开调试模式,我们查看一下登录成功之后进行了哪些操作。
用户登录之后,会让我们重定向到一个新的地址获取初始化的一些参数。
跳转到新的地址之后,我们获取到一个 xml 结构的数据,这个数据是用来进行初始化需要的。
请求的地址为https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=AXwgRhcMRSlLwikCn2I_PPn5@qrticket_0&uuid=gdH7PHqg4A==&lang=zh_CN&scan=1548214671&fun=new&version=v2&lang=zh_CN
。
API | webwxnewloginpage |
---|---|
url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage |
method | GET |
params | ticket: xxx uuid: xxx lang: zh_CN 语言 scan: xxx fun: new |
返回数据(XML):
<error>
<ret>0</ret>
<message>OK</message>
<skey>xxx</skey>
<wxsid>xxx</wxsid>
<wxuin>xxx</wxuin>
<pass_ticket>xxx</pass_ticket>
<isgrayscale>1</isgrayscale>
</error>
API | webwxinit微信初始化 |
---|---|
url | https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?pass_ticket=xxx&skey=xxx&r=xxx |
method | POST |
data | JSON |
header | ContentType: application/json; charset=UTF-8 |
params | { BaseRequest: { Uin: xxx, Sid: xxx, Skey: xxx, DeviceID: xxx, } } |
返回数据(JSON):
{
"BaseResponse": {
"Ret": 0,
"ErrMsg": ""
},
"Count": 11,
"ContactList": [...],
"SyncKey": {
"Count": 4,
"List": [
{
"Key": 1,
"Val": 635705559
},
...
]
},
"User": {
"Uin": xxx,
"UserName": xxx,
"NickName": xxx,
"HeadImgUrl": xxx,
"RemarkName": "",
"PYInitial": "",
"PYQuanPin": "",
"RemarkPYInitial": "",
"RemarkPYQuanPin": "",
"HideInputBarFlag": 0,
"StarFriend": 0,
"Sex": 1,
"Signature": "Apt-get install B",
"AppAccountFlag": 0,
"VerifyFlag": 0,
"ContactFlag": 0,
"WebWxPluginSwitch": 0,
"HeadImgFlag": 1,
"SnsFlag": 17
},
"ChatSet": xxx,
"SKey": xxx,
"ClientVersion": 369297683,
"SystemTime": 1453124908,
"GrayScale": 1,
"InviteStartCount": 40,
"MPSubscribeMsgCount": 2,
"MPSubscribeMsgList": [...],
"ClickReportInterval": 600000
}
进行初始化,初始化的 url 如下:
https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=-2026305663&lang=zh_CN&pass_ticket=Ja8PaHs1heLZzSQihsRnNF%252Fu%252FzoHJQ%252BUHNV7u7N13K9iTJVaM70wfaINLcm4dqcF
这是一个POST
请求,需要的参数正好是我们上面获取到的,如下图所示:
并且初始化之后返回用户通讯录的一些信息,如下:
3.2、代码实现
我们需要在函数check_login
里面新增扫码成功之后的调整,以及跳转之后的页面index
,wechat.py
代码如下:
from flask import Flask, render_template, request, session, jsonify
import time
import requests
import re
from bs4 import BeautifulSoup
app = Flask(__name__)
app.secret_key = 'wangzan18'
def xml_parser(text):
"""
格式化xml数据,修改成我们需要的格式
:param text:
:return:
"""
dic = {}
soup = BeautifulSoup(text, 'html.parser')
div = soup.find(name='error')
for item in div.find_all(recursive=False):
dic[item.name] = item.text
return dic
@app.route('/login', methods=['GET', 'POST'])
def login():
"""
获取登录的二维码
:return:
"""
if request.method == 'GET':
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发送get请求
qcode = re.findall('uuid = "(.*)";', response.text)[0] # 获取二维码的参数
session['qcode'] = qcode
return render_template('login.html', qcode=qcode)
else:
pass
@app.route('/check_login')
def check_login():
"""
检查用户是否扫码登录
:return:
"""
response = {'code': 408} # 默认用户没有扫码
qcode = session.get('qcode') # 获取用户前面获取的 uuid,用于放到下面的 url 里面
ctime = time.time() * 1000
check_url = "https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid={0}&tip=0&r=-1437802572&_={1}".format(qcode, ctime)
ret = requests.get(check_url) # 扫码返回值
if 'code=201' in ret.text:
# 扫码成功
src = re.findall("userAvatar = '(.*)';", ret.text)[0] # 获取扫码用户头像
response['code'] = 201 # 获取扫码的返回值,放到字典response
response['src'] = src # 获取用户头像数据,放到字典response
elif 'code=200' in ret.text:
# 确认登录
redirect_uri = re.findall('redirect_uri="(.*)";', ret.text)[0] # 获取跳转的url
# 向redirect_uri地址发送请求,获取凭证相关信息
redirect_uri = redirect_uri + "&fun=new&version=v2&lang=zh_CN"
ticket_ret = requests.get(redirect_uri) # 获取xml参数
ticket_dict = xml_parser(ticket_ret.text) # 解析参数变成我们需要的格式
session['ticket_dict'] = ticket_dict # 或许我们初始化需要的ticket
response['code'] = 200
return jsonify(response)
@app.route('/index')
def index():
ticket_dict = session.get('ticket_dict') # 获取初始化需要的ticket
init_url = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=-1438401779&lang=zh_CN&pass_ticket={0}".format(
ticket_dict.get('pass_ticket')) # 调整好初始化的url
data_dict = {
"BaseRequest": {
"Sid": ticket_dict.get('wxsid'),
"Uin": ticket_dict.get('wxuin'),
"Skey": ticket_dict.get('skey'),
}
} # post需要的参数
init_ret = requests.post(url=init_url, json=data_dict) # 微信登录初始化
init_ret.encoding = 'utf-8'
user_dict = init_ret.json() # 储存返回的json数据
return render_template('index.html', user_dict=user_dict) # 把返回的数据传给index.html
if __name__ == '__main__':
app.run(debug=True, host="0.0.0.0")
然后我们需要在login.html
里面添加登录成功之后跳转到接口/index
,接口我们已经写好,完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Wechat_login</title>
</head>
<body>
<div style="width: 300px;margin: 0 auto">
<h1 style="text-align: center">微信登录</h1>
<img id="img" style="height: 300px;width: 300px;" src="https://login.weixin.qq.com/qrcode/{{qcode}}" alt="">
</div>
<script src="/static/jquery-1.12.4.js"></script>
<script>
$(function () {
checkLogin();
})
function checkLogin() {
$.ajax({
url:'/check_login',
type:'GET',
dataType:'JSON',
success:function (arg) {
if(arg.code === 201){
// 扫码
$('#img').attr('src',arg.src);
checkLogin();
}else if(arg.code === 200){
// 重定向到用户列表
location.href = '/index'
}else{
checkLogin();
}
}
})
}
</script>
</body>
</html>
用户登录成功之后调用接口/index
,接口会把页面定向到index.html
,其index.html
完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Wechat</title>
</head>
<body>
<h1>欢迎登录:{{user_dict.User.NickName}}</h1>
<h3>最近联系人</h3>
<ul>
{% for user in user_dict.ContactList%}
<li>{{user.NickName}}</li>
{% endfor %}
</ul>
<h3>微信订阅号</h3>
<ul>
{% for sub in user_dict.MPSubscribeMsgList %}
{% for artlist in sub.MPArticleList %}
<li>{{artlist.Title}}</li>
{% endfor %}
{% endfor %}
</ul>
</body>
</html>
3.3、登录查看
我们点击确认登录之后,就跳转到我们的index.html
页面,效果如下:
如果使用 Python3(Flask) 一步一步模拟一个网页微信客户端的更多相关文章
- 一步一步教你编写与搭建自动化测试框架——python篇
[本文出自天外归云的博客园] 这两天用python写了一个自动化测试框架,取名为Auty.准备用来做Web方面的接口测试,以下为Auty框架一步一步的搭建过程——
- 一步一步理解 python web 框架,才不会从入门到放弃 -- 开始使用 Django
背景知识 要使用 Django,首先必须先安装 Django. 下图是 Django 官网的版本支持,我们可以看到上面有一个 LTS 存在.什么是 LTS 呢?LTS ,long-term suppo ...
- jumpservice一步一步安装
一步一步安装 (CentOS) 本文档旨在帮助用户了解各组件之间的关系, 生产环境部署建议参考 进阶安装文档 云服务器快速部署参考 极速安装 安装过程中遇到问题可参考 安装过程中常见的问题 测试推荐环 ...
- 如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑
阅读目录 前言 场景1的思考 场景2的思考 避坑方式 实践 结语 一.前言 在上一篇中(如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成),有一行注释的代码: public interfa ...
- 如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成
阅读目录 前言 建模 实现 结语 一.前言 前面几篇已经实现了一个基本的购买+售价计算的过程,这次再让售价丰满一些,增加一个会员价的概念.会员价在现在的主流电商中,是一个不大常见的模式,其带来的问题是 ...
- 如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车
阅读目录 前言 回顾 梳理 实现 结语 一.前言 之前的文章中已经涉及到了购买商品加入购物车,购物车内购物项的金额计算等功能.本篇准备把剩下的购物车的基本概念一次处理完. 二.回顾 在动手之前我对之 ...
- 如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文
阅读目录 前言 明确业务细节 建模 实现 结语 一.前言 上一篇我们已经确立的购买上下文和销售上下文的交互方式,传送门在此:http://www.cnblogs.com/Zachary-Fan/p/D ...
- 如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文
阅读目录 前言 如何在一个项目中实现多个上下文的业务 售价上下文与购买上下文的集成 结语 一.前言 前几篇已经实现了一个最简单的购买过程,这次开始往这个过程中增加一些东西.比如促销.会员价等,在我们的 ...
- 如何一步一步用DDD设计一个电商网站(五)—— 停下脚步,重新出发
阅读目录 前言 单元测试 纠正错误,重新出发 结语 一.前言 实际编码已经写了2篇了,在这过程中非常感谢有听到观点不同的声音,借着这个契机,今天这篇就把大家提出的建议一个个的过一遍,重新整理,重新出发 ...
随机推荐
- python学习之基础入门,安装,字符串,数据转换,三元运算符
python基础 我们要开始学习新的编程语言了,加油~~ python是“世界上最好的语言”,学习它当然是认为它是最好的所以我们才学(人生苦短我学python),python运用于不同的领域,采集分析 ...
- IDEA创建类似于Eclipse的source folder
1.新建普通文件夹目录directory 2.当前Module右键Open Mudule Settings(F12) 3.选中新建的文件夹并单击上面的Sources,看到文件夹颜色变化即成功.
- linux重启之后No CUDA-supporting devices found!
实验室做并行计算的服务重启后,采用cuda接口的应用程序vasp_gpu,运行时提示: CUDA Error in cuda_main.cu, line 144: unknown error No C ...
- 【题解】[Nwerc 2006]escape -C++
Description 给出数字N(1<=N<=10000),X(1<=x<=1000),Y(1<=Y<=1000),代表有N个敌人分布一个X行Y列的矩阵上 矩形的 ...
- Shell 03 for while case 函数 中断及退出
一.for循环 1.脚本1,通过循环批量显示5个hello world ( in 1 2 3 4 5 ) 2.脚本2,通过循环批量显示10个hello world ( in {1..10} ...
- 【csp模拟赛6】树上统计-启发式合并,线段树合并
30%:暴力 40%:枚举L,R从L~n枚举,R每增大一个,更新需要的边(bfs实现)60%:枚举每条边, 计算每条边的贡献另外20%的数据:枚举每条边,计算每条边的贡献100%:对于每一条边统计 有 ...
- [Qt Quick] qmlscene 工具的使用
qmlscene是Qt 5提供的一个查看qml文件效果的工具.特点是不需要编译应用程序. qmlscene = qml + scene (场景) qmlscene.exe位于Qt的安装目录下 (类似/ ...
- 【概率论】1-1:概率定义(Definition of Probability)
title: [概率论]1-1:概率定义(Definition of Probability) categories: Mathematic Probability keywords: Sample ...
- Try-Catch-Finally代码块中的return
测试类的原型是这样子的 public class TryCatchFinallyToReturn { public static void main(String[] args) { System.o ...
- codeforces gym #101987B- Cosmetic Survey(floyd)
题目链接: https://codeforces.com/gym/101987/my 题意: 顶点数为$n$,边数为$m$ 求出每个点对$(a,b)$,$a$到$b$的最小路径的最大值 数据范围: $ ...