Compare commits
198 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e13823d88 | ||
|
|
481a9c0310 | ||
|
|
43caf8fdc8 | ||
|
|
ac4abae019 | ||
|
|
08799d61f0 | ||
|
|
6e5ceca134 | ||
|
|
db34583c7d | ||
|
|
4f3abc9446 | ||
|
|
c98c423c5e | ||
|
|
f7c8ff4daa | ||
|
|
68a508e30a | ||
|
|
49d35aabdc | ||
|
|
9e529c1f9f | ||
|
|
6b6bf41a51 | ||
|
|
badc8b8c31 | ||
|
|
fbeb7835e5 | ||
|
|
f9a0f20e41 | ||
|
|
fd8c500128 | ||
|
|
da1a0903c5 | ||
|
|
8d0a6f05b7 | ||
|
|
c82a94de39 | ||
|
|
0316e52e90 | ||
|
|
390fcecdd2 | ||
|
|
12581dc03d | ||
|
|
095dbfc21d | ||
|
|
eee53461cf | ||
|
|
95e9e8a89b | ||
|
|
998b387875 | ||
|
|
9a42e0e8d7 | ||
|
|
681157c40b | ||
|
|
4468aa2277 | ||
|
|
c6ec668fad | ||
|
|
3a795492de | ||
|
|
006fd48d71 | ||
|
|
ee50ad213d | ||
|
|
6c7c83e53d | ||
|
|
6113b8519f | ||
|
|
a3286fc2c2 | ||
|
|
5bcb078124 | ||
|
|
7ec0cbd235 | ||
|
|
9531f774c2 | ||
|
|
e3038a3f21 | ||
|
|
1ddd6cdd69 | ||
|
|
a258780185 | ||
|
|
5d0b14ce85 | ||
|
|
db7e88a6a6 | ||
|
|
0578f2e3a8 | ||
|
|
3b9b15deba | ||
|
|
91c0a44a32 | ||
|
|
f377c7a849 | ||
|
|
7136474a6b | ||
|
|
563b051799 | ||
|
|
3a31349662 | ||
|
|
cc67987bb3 | ||
|
|
64a52559f3 | ||
|
|
4966d45f96 | ||
|
|
cb8e0aada6 | ||
|
|
6e8cd9f902 | ||
|
|
8f984cd751 | ||
|
|
0bd6417ff4 | ||
|
|
619a96ed5d | ||
|
|
83cc5e6d6c | ||
|
|
b848a49720 | ||
|
|
707fd5bd8a | ||
|
|
8cabf157f6 | ||
|
|
eeea27403c | ||
|
|
4ac5fc3403 | ||
|
|
e56b545392 | ||
|
|
1e3ea59bb6 | ||
|
|
42f2a640f6 | ||
|
|
a3f185e3a8 | ||
|
|
ad5e071080 | ||
|
|
2578be4965 | ||
|
|
365453e0b3 | ||
|
|
733d512f72 | ||
|
|
c430a65cdd | ||
|
|
5a97d11a09 | ||
|
|
df7467e18d | ||
|
|
969bc99e87 | ||
|
|
9c63063d50 | ||
|
|
08ad7598db | ||
|
|
afd8c35afb | ||
|
|
419495ce84 | ||
|
|
6a17dab7b0 | ||
|
|
55fb26176a | ||
|
|
61961e515c | ||
|
|
fe5e82f2ff | ||
|
|
dac614663d | ||
|
|
9476052000 | ||
|
|
1431c577f9 | ||
|
|
0908da55c5 | ||
|
|
c71547e3db | ||
|
|
91763f3141 | ||
|
|
5a1e34b6fa | ||
|
|
1a617db658 | ||
|
|
e8abbd57bb | ||
|
|
cc3426c05f | ||
|
|
7762c88db6 | ||
|
|
51ab20f7cf | ||
|
|
bdc670a86d | ||
|
|
7c21310ea3 | ||
|
|
7863cd2863 | ||
|
|
a9287eb7e8 | ||
|
|
bef3e95cec | ||
|
|
f00f1922a5 | ||
|
|
e91ad184bb | ||
|
|
556a8fc7c3 | ||
|
|
bfff29cd67 | ||
|
|
98e5d8f95a | ||
|
|
8032422f7d | ||
|
|
a6081ec0b5 | ||
|
|
0681cef3db | ||
|
|
eb8d1f5a97 | ||
|
|
3e7616ccea | ||
|
|
a21bf2264e | ||
|
|
1c4e832d87 | ||
|
|
2f98504328 | ||
|
|
0da42b9f26 | ||
|
|
1387cbede0 | ||
|
|
59f5b8cb49 | ||
|
|
b46ca021a9 | ||
|
|
3b3dd8337d | ||
|
|
059e5b5d1c | ||
|
|
0524f5fc27 | ||
|
|
682613ca31 | ||
|
|
2f61002562 | ||
|
|
c2ce769a92 | ||
|
|
ddfd38e0a9 | ||
|
|
af71d0a921 | ||
|
|
1c98e2743a | ||
|
|
d4d4ee2139 | ||
|
|
8df3c4032d | ||
|
|
884b0f30db | ||
|
|
f3b0cbee24 | ||
|
|
0d52efb1da | ||
|
|
4bd5cdb89e | ||
|
|
bedeb20085 | ||
|
|
00d5ea4be6 | ||
|
|
2c9fa98efc | ||
|
|
901864e12f | ||
|
|
09ba6fef40 | ||
|
|
25d7169afe | ||
|
|
ae70de23ae | ||
|
|
45d3e91391 | ||
|
|
80990eb02a | ||
|
|
95925ba9e7 | ||
|
|
8109d87c5f | ||
|
|
dbe98ca867 | ||
|
|
ff8a09e3fb | ||
|
|
559dabe0b5 | ||
|
|
d0b61852d3 | ||
|
|
ef7ec01329 | ||
|
|
550c87d77a | ||
|
|
26d0f5a1de | ||
|
|
88ddc87f03 | ||
|
|
eb167538d2 | ||
|
|
047024d239 | ||
|
|
751c01298f | ||
|
|
ea10558bff | ||
|
|
e252a41fd3 | ||
|
|
994c211e2f | ||
|
|
54eb32d40f | ||
|
|
2396a40911 | ||
|
|
a3e1544072 | ||
|
|
e722eacbb8 | ||
|
|
3c982e4ac4 | ||
|
|
9b46ee8a85 | ||
|
|
0dc4eee1a8 | ||
|
|
cb4fd195a1 | ||
|
|
a0f2bc7913 | ||
|
|
6004d78e90 | ||
|
|
bcab8c4e30 | ||
|
|
41f4628eb9 | ||
|
|
df41b8d8c2 | ||
|
|
111541a1e8 | ||
|
|
48a1f6a06a | ||
|
|
a4b355bdf1 | ||
|
|
6c8d5e3142 | ||
|
|
c52d91c5e8 | ||
|
|
e510de878d | ||
|
|
e17b48a0ce | ||
|
|
1be623032f | ||
|
|
0670e19b05 | ||
|
|
a05f1a910a | ||
|
|
c3f3ba9ffc | ||
|
|
0d7558afeb | ||
|
|
f47368206c | ||
|
|
632256caf7 | ||
|
|
668c4ae8ce | ||
|
|
7d5b8e2b80 | ||
|
|
8f681c5e30 | ||
|
|
6d18a8d11b | ||
|
|
6dc3005cd9 | ||
|
|
a96e08efeb | ||
|
|
a6feda0a41 | ||
|
|
2a734bdfe6 | ||
|
|
8bcbaf4cde | ||
|
|
5b296694a9 |
14
README.md
14
README.md
@@ -1,10 +1,6 @@
|
|||||||
# 🚂 py12306 购票助手
|
# 🚂 py12306 购票助手
|
||||||
分布式,多账号,多任务购票
|
分布式,多账号,多任务购票
|
||||||
|
|
||||||
## 前言
|
|
||||||
今年回家的票明显要难买很多,早早就答应了父母今年的票没问题,到现在一张票没买到,虽然家里已经订了汽车票,让我不用操心,但是想想他们一行还有小孩,心还是很伤的。
|
|
||||||
这段时间从 12306Bypass 到 testerSunshine 大佬写的 [12306](https://github.com/testerSunshine/12306),还是没买到票,索性就自己写了一个,希望也能帮助到更多人
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- [x] 多日期查询余票
|
- [x] 多日期查询余票
|
||||||
- [x] 自动打码下单
|
- [x] 自动打码下单
|
||||||
@@ -18,7 +14,7 @@
|
|||||||
- [x] 邮件通知
|
- [x] 邮件通知
|
||||||
- [x] Web 管理页面
|
- [x] Web 管理页面
|
||||||
- [x] 微信消息通知
|
- [x] 微信消息通知
|
||||||
- [ ] 代理池支持
|
- [ ] 代理池支持 ([pyproxy-async](https://github.com/pjialin/pyproxy-async))
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
py12306 需要运行在 python 3.6 以上版本(其它版本暂未测试)
|
py12306 需要运行在 python 3.6 以上版本(其它版本暂未测试)
|
||||||
@@ -36,9 +32,8 @@ cp env.py.example env.py
|
|||||||
```
|
```
|
||||||
自动打码
|
自动打码
|
||||||
|
|
||||||
目前支持免费打码,和若快打码
|
(若快已停止服务,目前只能设置**free**打码模式)
|
||||||
|
free 已对接到打码共享平台,[https://py12306-helper.pjialin.com](https://py12306-helper.pjialin.com/),欢迎参与分享
|
||||||
注:免费打码无法保证持续可用,如失效请手动切换到若快平台,需要先到 [http://www.ruokuai.com](http://www.ruokuai.com/login) 注册一个账号后填写到配置中
|
|
||||||
|
|
||||||
语音通知
|
语音通知
|
||||||
|
|
||||||
@@ -148,7 +143,7 @@ docker-compose up -d
|
|||||||
### 关于防封
|
### 关于防封
|
||||||
目前查询和登录操作是分开的,查询是不依赖用户是否登录,放在 A 云 T 云容易被限制 ip,建议在其它网络环境下运行
|
目前查询和登录操作是分开的,查询是不依赖用户是否登录,放在 A 云 T 云容易被限制 ip,建议在其它网络环境下运行
|
||||||
|
|
||||||
交流群 [274781597](http://shang.qq.com/wpa/qunwpa?idkey=8eab0b6402096266a62263c1cd452149926adb5cba7a2b7a98a5adc65869addf)
|
QQ 交流群 [780289875](https://jq.qq.com/?_wv=1027&k=5PgzDwV),TG 群 [Py12306 交流](https://t.me/joinchat/F3sSegrF3x8KAmsd1mTu7w)
|
||||||
|
|
||||||
### Online IDE
|
### Online IDE
|
||||||
[](https://gitpod.io#https://github.com/pjialin/py12306)
|
[](https://gitpod.io#https://github.com/pjialin/py12306)
|
||||||
@@ -156,6 +151,7 @@ docker-compose up -d
|
|||||||
## Thanks
|
## Thanks
|
||||||
- 感谢大佬 [testerSunshine](https://github.com/testerSunshine/12306),借鉴了部分实现
|
- 感谢大佬 [testerSunshine](https://github.com/testerSunshine/12306),借鉴了部分实现
|
||||||
- 感谢所有提供 pr 的大佬
|
- 感谢所有提供 pr 的大佬
|
||||||
|
- 感谢大佬 [zhaipro](https://github.com/zhaipro/easy12306) 的验证码本地识别模型与算法
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
48
data/cdn.txt
48
data/cdn.txt
@@ -2218,3 +2218,51 @@
|
|||||||
117.27.241.218
|
117.27.241.218
|
||||||
112.65.92.116
|
112.65.92.116
|
||||||
52.114.128.43
|
52.114.128.43
|
||||||
|
183.66.109.254
|
||||||
|
60.28.100.248
|
||||||
|
111.161.122.240
|
||||||
|
121.31.28.101
|
||||||
|
222.218.87.252
|
||||||
|
113.16.212.251
|
||||||
|
58.18.254.253
|
||||||
|
124.225.107.254
|
||||||
|
14.204.186.174
|
||||||
|
14.204.185.254
|
||||||
|
14.204.185.123
|
||||||
|
220.165.142.253
|
||||||
|
42.123.108.8
|
||||||
|
42.123.107.43
|
||||||
|
120.241.66.115
|
||||||
|
112.90.135.229
|
||||||
|
183.56.172.113
|
||||||
|
27.155.108.102
|
||||||
|
27.155.108.93
|
||||||
|
61.132.238.115
|
||||||
|
113.194.59.199
|
||||||
|
218.64.94.181
|
||||||
|
122.191.168.109
|
||||||
|
42.49.185.169
|
||||||
|
42.49.185.170
|
||||||
|
175.154.187.252
|
||||||
|
118.123.233.254
|
||||||
|
118.123.237.245
|
||||||
|
123.138.157.122
|
||||||
|
113.142.80.223
|
||||||
|
117.23.2.252
|
||||||
|
218.26.75.236
|
||||||
|
218.26.75.206
|
||||||
|
183.131.124.249
|
||||||
|
36.25.241.251
|
||||||
|
153.99.235.91
|
||||||
|
221.230.143.254
|
||||||
|
120.221.24.14
|
||||||
|
218.58.205.182
|
||||||
|
182.34.127.253
|
||||||
|
150.138.214.124
|
||||||
|
61.54.7.192
|
||||||
|
115.54.16.245
|
||||||
|
218.12.228.246
|
||||||
|
121.22.247.254
|
||||||
|
124.236.28.230
|
||||||
|
218.60.185.251
|
||||||
|
42.101.72.9
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -6,12 +6,14 @@ USER_ACCOUNTS = [
|
|||||||
{
|
{
|
||||||
'key': 0, # 如使用多个账号 key 不能重复
|
'key': 0, # 如使用多个账号 key 不能重复
|
||||||
'user_name': 'your user name',
|
'user_name': 'your user name',
|
||||||
'password': 'your password'
|
'password': '忽略',
|
||||||
|
'type': 'qr' # qr 为扫码登录,填写其他为密码登录
|
||||||
},
|
},
|
||||||
# {
|
# {
|
||||||
# 'key': 'wangwu',
|
# 'key': 'wangwu',
|
||||||
# 'user_name': 'wangwu@qq.com',
|
# 'user_name': 'wangwu@qq.com',
|
||||||
# 'password': 'wangwu'
|
# 'password': 'wangwu',
|
||||||
|
# 'type': ''
|
||||||
# }
|
# }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -28,7 +30,8 @@ QUERY_JOB_THREAD_ENABLED = 0 # 是否开启多线程查询,开启后第个任
|
|||||||
|
|
||||||
# 打码平台账号
|
# 打码平台账号
|
||||||
# 目前只支持免费打码接口 和 若快打码,注册地址:http://www.ruokuai.com/login
|
# 目前只支持免费打码接口 和 若快打码,注册地址:http://www.ruokuai.com/login
|
||||||
AUTO_CODE_PLATFORM = 'free' # 免费填写 free 若快 ruokuai # 免费打码无法保证持续可用,如失效请手动切换
|
AUTO_CODE_PLATFORM = 'free' # 免费填写 free 若快 ruokuai # 免费打码无法保证持续可用,如失效请手动切换 #个人本地打码填写 user,并修改 API_USER_CODE_QCR_API
|
||||||
|
API_USER_CODE_QCR_API = ''
|
||||||
AUTO_CODE_ACCOUNT = {
|
AUTO_CODE_ACCOUNT = {
|
||||||
'user': 'your user name',
|
'user': 'your user name',
|
||||||
'pwd': 'your password'
|
'pwd': 'your password'
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ USER_ACCOUNTS = [
|
|||||||
{
|
{
|
||||||
'key': 0, # 如使用多个账号 key 不能重复
|
'key': 0, # 如使用多个账号 key 不能重复
|
||||||
'user_name': 'your user name',
|
'user_name': 'your user name',
|
||||||
'password': 'your password'
|
'password': '忽略',
|
||||||
|
'type': 'qr' # qr 为扫码登录,填写其他为密码登录
|
||||||
},
|
},
|
||||||
# {
|
# {
|
||||||
# 'key': 'wangwu',
|
# 'key': 'wangwu',
|
||||||
# 'user_name': 'wangwu@qq.com',
|
# 'user_name': 'wangwu@qq.com',
|
||||||
# 'password': 'wangwu'
|
# 'password': 'wangwu',
|
||||||
|
# 'type': ''
|
||||||
# }
|
# }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -28,7 +30,8 @@ QUERY_JOB_THREAD_ENABLED = 0 # 是否开启多线程查询,开启后第个任
|
|||||||
|
|
||||||
# 打码平台账号
|
# 打码平台账号
|
||||||
# 目前只支持免费打码接口 和 若快打码,注册地址:http://www.ruokuai.com/login
|
# 目前只支持免费打码接口 和 若快打码,注册地址:http://www.ruokuai.com/login
|
||||||
AUTO_CODE_PLATFORM = 'free' # 免费填写 free 若快 ruokuai # 免费打码无法保证持续可用,如失效请手动切换
|
AUTO_CODE_PLATFORM = 'free' # 免费填写 free 若快 ruokuai # 免费打码无法保证持续可用,如失效请手动切换; 个人打码填写 user 并修改API_USER_CODE_QCR_API 为自己地址
|
||||||
|
API_USER_CODE_QCR_API = ''
|
||||||
AUTO_CODE_ACCOUNT = { # 使用 free 可用省略
|
AUTO_CODE_ACCOUNT = { # 使用 free 可用省略
|
||||||
'user': 'your user name',
|
'user': 'your user name',
|
||||||
'pwd': 'your password'
|
'pwd': 'your password'
|
||||||
@@ -109,14 +112,19 @@ WEB_PORT = 8008 # 监听端口
|
|||||||
CDN_ENABLED = 0
|
CDN_ENABLED = 0
|
||||||
CDN_CHECK_TIME_OUT = 1 # 检测单个 cdn 是否可用超时时间
|
CDN_CHECK_TIME_OUT = 1 # 检测单个 cdn 是否可用超时时间
|
||||||
|
|
||||||
|
# 是否使用浏览器缓存中的RAIL_EXPIRATION 和 RAIL_DEVICEID
|
||||||
|
CACHE_RAIL_ID_ENABLED = 0
|
||||||
|
RAIL_EXPIRATION = '' #浏览12306 网站中的Cache的RAIL_EXPIRATION 值
|
||||||
|
RAIL_DEVICEID = '' #浏览12306 网站中的Cache的RAIL_DEVICEID 值
|
||||||
|
|
||||||
# 查询任务
|
# 查询任务
|
||||||
QUERY_JOBS = [
|
QUERY_JOBS = [
|
||||||
{
|
{
|
||||||
# 'job_name': 'bj -> sz', # 任务名称,不填默认会以车站名命名,不可重复
|
# 'job_name': 'bj -> sz', # 任务名称,不填默认会以车站名命名,不可重复
|
||||||
'account_key': 0, # 将会使用指定账号下单
|
'account_key': 0, # 将会使用指定账号下单
|
||||||
'left_dates': [ # 出发日期 :Array
|
'left_dates': [ # 出发日期 :Array
|
||||||
"2019-01-25",
|
"2020-01-25",
|
||||||
"2019-01-26",
|
"2020-01-26",
|
||||||
],
|
],
|
||||||
'stations': { # 车站 支持多个车站同时查询 :Dict or :List
|
'stations': { # 车站 支持多个车站同时查询 :Dict or :List
|
||||||
'left': '北京',
|
'left': '北京',
|
||||||
@@ -132,7 +140,7 @@ QUERY_JOBS = [
|
|||||||
# }],
|
# }],
|
||||||
'members': [ # 乘客姓名,会根据当前账号自动识别乘客类型 购买儿童票 设置两个相同的姓名即可,程序会自动识别 如 ['张三', '张三']
|
'members': [ # 乘客姓名,会根据当前账号自动识别乘客类型 购买儿童票 设置两个相同的姓名即可,程序会自动识别 如 ['张三', '张三']
|
||||||
"张三",
|
"张三",
|
||||||
"王五",
|
#"*王五", #在姓名前加*表示学生购买成人票
|
||||||
# 7, # 支持通过序号确定唯一乘客,序号查看可通过 python main.py -t 登录成功之后在 runtime/user/ 下找到对应的 用户名_passengers.json 文件,找到对应的 code 填入
|
# 7, # 支持通过序号确定唯一乘客,序号查看可通过 python main.py -t 登录成功之后在 runtime/user/ 下找到对应的 用户名_passengers.json 文件,找到对应的 code 填入
|
||||||
],
|
],
|
||||||
'allow_less_member': 0, # 是否允许余票不足时提交部分乘客
|
'allow_less_member': 0, # 是否允许余票不足时提交部分乘客
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import os
|
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -11,16 +10,19 @@ from py12306.log.order_log import OrderLog
|
|||||||
|
|
||||||
|
|
||||||
def app_available_check():
|
def app_available_check():
|
||||||
# return True # Debug
|
|
||||||
if Config().IS_DEBUG:
|
if Config().IS_DEBUG:
|
||||||
return True
|
return True
|
||||||
now = time_now()
|
now = time_now()
|
||||||
if now.hour >= 23 or now.hour < 6:
|
if now.weekday() == 1 and (now.hour > 23 and now.minute > 30 or now.hour < 5):
|
||||||
CommonLog.add_quick_log(CommonLog.MESSAGE_12306_IS_CLOSED.format(time_now())).flush()
|
CommonLog.add_quick_log(CommonLog.MESSAGE_12306_IS_CLOSED.format(time_now())).flush()
|
||||||
open_time = datetime.datetime(now.year, now.month, now.day, 6)
|
open_time = datetime.datetime(now.year, now.month, now.day, 5)
|
||||||
if open_time < now:
|
if open_time < now:
|
||||||
open_time += datetime.timedelta(1)
|
open_time += datetime.timedelta(1)
|
||||||
sleep((open_time - now).seconds)
|
sleep((open_time - now).seconds)
|
||||||
|
elif 1 < now.hour < 5:
|
||||||
|
CommonLog.add_quick_log(CommonLog.MESSAGE_12306_IS_CLOSED.format(time_now())).flush()
|
||||||
|
open_time = datetime.datetime(now.year, now.month, now.day, 5)
|
||||||
|
sleep((open_time - now).seconds)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -81,7 +83,7 @@ class App:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_auto_code(cls):
|
def check_auto_code(cls):
|
||||||
if Config().AUTO_CODE_PLATFORM == 'free': return True
|
if Config().AUTO_CODE_PLATFORM == 'free' or Config().AUTO_CODE_PLATFORM == 'user': return True
|
||||||
if not Config().AUTO_CODE_ACCOUNT.get('user') or not Config().AUTO_CODE_ACCOUNT.get('pwd'):
|
if not Config().AUTO_CODE_ACCOUNT.get('user') or not Config().AUTO_CODE_ACCOUNT.get('pwd'):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ class Config:
|
|||||||
QUERY_JOB_THREAD_ENABLED = 0
|
QUERY_JOB_THREAD_ENABLED = 0
|
||||||
# 打码平台账号
|
# 打码平台账号
|
||||||
AUTO_CODE_PLATFORM = ''
|
AUTO_CODE_PLATFORM = ''
|
||||||
|
#用户打码平台地址
|
||||||
|
API_USER_CODE_QCR_API = ''
|
||||||
AUTO_CODE_ACCOUNT = {'user': '', 'pwd': ''}
|
AUTO_CODE_ACCOUNT = {'user': '', 'pwd': ''}
|
||||||
# 输出日志到文件
|
# 输出日志到文件
|
||||||
OUT_PUT_LOG_TO_FILE_ENABLED = 0
|
OUT_PUT_LOG_TO_FILE_ENABLED = 0
|
||||||
@@ -94,6 +96,10 @@ class Config:
|
|||||||
CDN_ITEM_FILE = PROJECT_DIR + 'data/cdn.txt'
|
CDN_ITEM_FILE = PROJECT_DIR + 'data/cdn.txt'
|
||||||
CDN_ENABLED_AVAILABLE_ITEM_FILE = QUERY_DATA_DIR + 'available.json'
|
CDN_ENABLED_AVAILABLE_ITEM_FILE = QUERY_DATA_DIR + 'available.json'
|
||||||
|
|
||||||
|
CACHE_RAIL_ID_ENABLED = 0
|
||||||
|
RAIL_EXPIRATION = ''
|
||||||
|
RAIL_DEVICEID = ''
|
||||||
|
|
||||||
# Default time out
|
# Default time out
|
||||||
TIME_OUT_OF_REQUEST = 5
|
TIME_OUT_OF_REQUEST = 5
|
||||||
|
|
||||||
@@ -213,6 +219,10 @@ class Config:
|
|||||||
def is_cdn_enabled():
|
def is_cdn_enabled():
|
||||||
return Config().CDN_ENABLED
|
return Config().CDN_ENABLED
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_cache_rail_id_enabled():
|
||||||
|
return Config().CACHE_RAIL_ID_ENABLED
|
||||||
|
|
||||||
|
|
||||||
class EnvLoader:
|
class EnvLoader:
|
||||||
envs = []
|
envs = []
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import math
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from py12306.config import Config
|
from py12306.config import Config
|
||||||
from py12306.helpers.api import *
|
from py12306.helpers.api import API_FREE_CODE_QCR_API
|
||||||
from py12306.helpers.request import Request
|
from py12306.helpers.request import Request
|
||||||
from py12306.log.common_log import CommonLog
|
from py12306.log.common_log import CommonLog
|
||||||
from py12306.vender.ruokuai.main import RKClient
|
from py12306.vender.ruokuai.main import RKClient
|
||||||
@@ -25,7 +25,7 @@ class OCR:
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
self = cls()
|
self = cls()
|
||||||
if Config().AUTO_CODE_PLATFORM == 'free':
|
if Config().AUTO_CODE_PLATFORM == 'free' or Config().AUTO_CODE_PLATFORM == 'user':
|
||||||
return self.get_image_by_free_site(img)
|
return self.get_image_by_free_site(img)
|
||||||
return self.get_img_position_by_ruokuai(img)
|
return self.get_img_position_by_ruokuai(img)
|
||||||
|
|
||||||
@@ -56,22 +56,16 @@ class OCR:
|
|||||||
|
|
||||||
def get_image_by_free_site(self, img):
|
def get_image_by_free_site(self, img):
|
||||||
data = {
|
data = {
|
||||||
'base64': img
|
'img': img
|
||||||
}
|
}
|
||||||
response = self.session.post(API_FREE_CODE_QCR_API, json=data)
|
if Config().AUTO_CODE_PLATFORM == 'free':
|
||||||
|
response = self.session.post(API_FREE_CODE_QCR_API, data=data, timeout=30)
|
||||||
|
else:
|
||||||
|
response = self.session.post(Config().API_USER_CODE_QCR_API, data=data, timeout=30)
|
||||||
result = response.json()
|
result = response.json()
|
||||||
if result.get('success') and result.get('data.check'):
|
if result.get('msg') == 'success':
|
||||||
check_data = {
|
pos = result.get('result')
|
||||||
'check': result.get('data.check'),
|
return self.get_image_position_by_offset(pos)
|
||||||
'img_buf': img,
|
|
||||||
'logon': 1,
|
|
||||||
'type': 'D'
|
|
||||||
}
|
|
||||||
check_response = self.session.post(API_FREE_CODE_QCR_API_CHECK, json=check_data)
|
|
||||||
check_result = check_response.json()
|
|
||||||
if check_result.get('res'):
|
|
||||||
position = check_result.get('res')
|
|
||||||
return position.replace('(', '').replace(')', '').split(',')
|
|
||||||
|
|
||||||
CommonLog.print_auto_code_fail(CommonLog.MESSAGE_GET_RESPONSE_FROM_FREE_AUTO_CODE)
|
CommonLog.print_auto_code_fail(CommonLog.MESSAGE_GET_RESPONSE_FROM_FREE_AUTO_CODE)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
# 查询余票
|
|
||||||
import time
|
|
||||||
|
|
||||||
HOST_URL_OF_12306 = 'kyfw.12306.cn'
|
HOST_URL_OF_12306 = 'kyfw.12306.cn'
|
||||||
BASE_URL_OF_12306 = 'https://' + HOST_URL_OF_12306
|
BASE_URL_OF_12306 = 'https://' + HOST_URL_OF_12306
|
||||||
@@ -15,6 +13,18 @@ API_BASE_LOGIN = {
|
|||||||
|
|
||||||
API_USER_LOGIN_CHECK = BASE_URL_OF_12306 + '/otn/login/conf'
|
API_USER_LOGIN_CHECK = BASE_URL_OF_12306 + '/otn/login/conf'
|
||||||
|
|
||||||
|
API_AUTH_QRCODE_BASE64_DOWNLOAD = {
|
||||||
|
'url': BASE_URL_OF_12306 + '/passport/web/create-qr64'
|
||||||
|
}
|
||||||
|
|
||||||
|
API_AUTH_QRCODE_CHECK = {
|
||||||
|
'url': BASE_URL_OF_12306 + '/passport/web/checkqr'
|
||||||
|
}
|
||||||
|
|
||||||
|
API_USER_LOGIN = {
|
||||||
|
'url': BASE_URL_OF_12306 + '/otn/login/userLogin'
|
||||||
|
}
|
||||||
|
|
||||||
API_AUTH_CODE_DOWNLOAD = {
|
API_AUTH_CODE_DOWNLOAD = {
|
||||||
'url': BASE_URL_OF_12306 + '/passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&_={random}'
|
'url': BASE_URL_OF_12306 + '/passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&_={random}'
|
||||||
}
|
}
|
||||||
@@ -40,12 +50,11 @@ API_GET_QUEUE_COUNT = BASE_URL_OF_12306 + '/otn/confirmPassenger/getQueueCount'
|
|||||||
API_CONFIRM_SINGLE_FOR_QUEUE = BASE_URL_OF_12306 + '/otn/confirmPassenger/confirmSingleForQueue'
|
API_CONFIRM_SINGLE_FOR_QUEUE = BASE_URL_OF_12306 + '/otn/confirmPassenger/confirmSingleForQueue'
|
||||||
API_QUERY_ORDER_WAIT_TIME = BASE_URL_OF_12306 + '/otn/confirmPassenger/queryOrderWaitTime?{}' # 排队查询
|
API_QUERY_ORDER_WAIT_TIME = BASE_URL_OF_12306 + '/otn/confirmPassenger/queryOrderWaitTime?{}' # 排队查询
|
||||||
API_QUERY_INIT_PAGE = BASE_URL_OF_12306 + '/otn/leftTicket/init'
|
API_QUERY_INIT_PAGE = BASE_URL_OF_12306 + '/otn/leftTicket/init'
|
||||||
|
# API_GET_BROWSER_DEVICE_ID = BASE_URL_OF_12306 + '/otn/HttpZF/logdevice'
|
||||||
|
API_GET_BROWSER_DEVICE_ID = 'https://12306-rail-id-v2.pjialin.com/'
|
||||||
|
API_FREE_CODE_QCR_API = 'https://12306-ocr.pjialin.com/check/'
|
||||||
|
|
||||||
API_NOTIFICATION_BY_VOICE_CODE = 'http://ali-voice.showapi.com/sendVoice?'
|
API_NOTIFICATION_BY_VOICE_CODE = 'http://ali-voice.showapi.com/sendVoice?'
|
||||||
API_NOTIFICATION_BY_VOICE_CODE_DINGXIN = 'http://yuyin2.market.alicloudapi.com/dx/voice_notice'
|
API_NOTIFICATION_BY_VOICE_CODE_DINGXIN = 'http://yuyin2.market.alicloudapi.com/dx/voice_notice'
|
||||||
|
|
||||||
# API_FREE_CODE_QCR_API = 'http://60.205.200.159/api' # 19-03-07 接口已失效
|
|
||||||
API_FREE_CODE_QCR_API = 'https://12306.jiedanba.cn/api/v2/getCheck'
|
|
||||||
API_FREE_CODE_QCR_API_CHECK = 'http://check.huochepiao.360.cn/img_vcode'
|
|
||||||
|
|
||||||
API_CHECK_CDN_AVAILABLE = 'https://{}/otn/dynamicJs/omseuuq'
|
API_CHECK_CDN_AVAILABLE = 'https://{}/otn/dynamicJs/omseuuq'
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class AuthCode:
|
|||||||
return self.retry_get_auth_code()
|
return self.retry_get_auth_code()
|
||||||
|
|
||||||
answer = ','.join(map(str, position))
|
answer = ','.join(map(str, position))
|
||||||
|
|
||||||
if not self.check_code(answer):
|
if not self.check_code(answer):
|
||||||
return self.retry_get_auth_code()
|
return self.retry_get_auth_code()
|
||||||
return position
|
return position
|
||||||
@@ -46,6 +47,7 @@ class AuthCode:
|
|||||||
url = API_AUTH_CODE_BASE64_DOWNLOAD.format(random=random.random())
|
url = API_AUTH_CODE_BASE64_DOWNLOAD.format(random=random.random())
|
||||||
# code_path = self.data_path + 'code.png'
|
# code_path = self.data_path + 'code.png'
|
||||||
try:
|
try:
|
||||||
|
self.session.cookies.clear_session_cookies()
|
||||||
UserLog.add_quick_log(UserLog.MESSAGE_DOWNLAODING_THE_CODE).flush()
|
UserLog.add_quick_log(UserLog.MESSAGE_DOWNLAODING_THE_CODE).flush()
|
||||||
# response = self.session.save_to_file(url, code_path) # TODO 返回错误情况
|
# response = self.session.save_to_file(url, code_path) # TODO 返回错误情况
|
||||||
response = self.session.get(url)
|
response = self.session.get(url)
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ class Notification():
|
|||||||
self = cls()
|
self = cls()
|
||||||
self.send_email_by_smtp(to, title, content)
|
self.send_email_by_smtp(to, title, content)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def send_email_with_qrcode(cls, to, title='', qrcode_path=''):
|
||||||
|
self = cls()
|
||||||
|
self.send_email_by_smtp_with_qrcode(to, title, qrcode_path)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def send_to_telegram(cls, content=''):
|
def send_to_telegram(cls, content=''):
|
||||||
self = cls()
|
self = cls()
|
||||||
@@ -125,15 +130,55 @@ class Notification():
|
|||||||
message.set_content(content)
|
message.set_content(content)
|
||||||
try:
|
try:
|
||||||
server = smtplib.SMTP(Config().EMAIL_SERVER_HOST)
|
server = smtplib.SMTP(Config().EMAIL_SERVER_HOST)
|
||||||
server.login(Config().EMAIL_SERVER_USER, Config().EMAIL_SERVER_PASSWORD)
|
|
||||||
server.ehlo()
|
server.ehlo()
|
||||||
server.starttls()
|
server.starttls()
|
||||||
|
server.login(Config().EMAIL_SERVER_USER, Config().EMAIL_SERVER_PASSWORD)
|
||||||
server.send_message(message)
|
server.send_message(message)
|
||||||
server.quit()
|
server.quit()
|
||||||
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_EMAIL_SUCCESS).flush()
|
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_EMAIL_SUCCESS).flush()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_EMAIL_FAIL.format(e)).flush()
|
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_EMAIL_FAIL.format(e)).flush()
|
||||||
|
|
||||||
|
def send_email_by_smtp_with_qrcode(self, to, title, qrcode_path):
|
||||||
|
import smtplib
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.image import MIMEImage
|
||||||
|
to = to if isinstance(to, list) else [to]
|
||||||
|
message = MIMEMultipart()
|
||||||
|
message['Subject'] = title
|
||||||
|
message['From'] = Config().EMAIL_SENDER
|
||||||
|
message['To'] = ", ".join(to)
|
||||||
|
htmlFile = """
|
||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
这是你的二维码
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<br /><img src="cid:0", width=200, height=200 ></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
htmlApart = MIMEText(htmlFile, 'html')
|
||||||
|
imageFile = qrcode_path
|
||||||
|
imageApart = MIMEImage(open(imageFile, 'rb').read(), imageFile.split('.')[-1])
|
||||||
|
imageApart.add_header('Content-ID', '<0>')
|
||||||
|
message.attach(imageApart)
|
||||||
|
message.attach(htmlApart)
|
||||||
|
try:
|
||||||
|
server = smtplib.SMTP(Config().EMAIL_SERVER_HOST)
|
||||||
|
server.ehlo()
|
||||||
|
server.starttls()
|
||||||
|
server.login(Config().EMAIL_SERVER_USER, Config().EMAIL_SERVER_PASSWORD)
|
||||||
|
server.send_message(message)
|
||||||
|
server.quit()
|
||||||
|
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_EMAIL_WITH_QRCODE_SUCCESS).flush()
|
||||||
|
self.push_bark(CommonLog.MESSAGE_SEND_EMAIL_WITH_QRCODE_SUCCESS)
|
||||||
|
except Exception as e:
|
||||||
|
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_EMAIL_FAIL.format(e)).flush()
|
||||||
|
|
||||||
def send_dingtalk_by_webbook(self, content):
|
def send_dingtalk_by_webbook(self, content):
|
||||||
from dingtalkchatbot.chatbot import DingtalkChatbot
|
from dingtalkchatbot.chatbot import DingtalkChatbot
|
||||||
webhook = Config().DINGTALK_WEBHOOK
|
webhook = Config().DINGTALK_WEBHOOK
|
||||||
|
|||||||
106
py12306/helpers/qrcode.py
Normal file
106
py12306/helpers/qrcode.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import png
|
||||||
|
|
||||||
|
|
||||||
|
def print_qrcode(path):
|
||||||
|
"""
|
||||||
|
将二维码输出到控制台
|
||||||
|
需要终端尺寸足够大才能显示
|
||||||
|
|
||||||
|
:param path: 二维码图片路径 (PNG 格式)
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
reader = png.Reader(path)
|
||||||
|
width, height, rows, info = reader.read()
|
||||||
|
lines = list(rows)
|
||||||
|
|
||||||
|
planes = info['planes'] # 通道数
|
||||||
|
threshold = (2 ** info['bitdepth']) / 2 # 色彩阈值
|
||||||
|
|
||||||
|
# 识别二维码尺寸
|
||||||
|
x_flag = -1 # x 边距标志
|
||||||
|
y_flag = -1 # y 边距标志
|
||||||
|
x_white = -1 # 定位图案白块 x 坐标
|
||||||
|
y_white = -1 # 定位图案白块 y 坐标
|
||||||
|
|
||||||
|
i = y_flag
|
||||||
|
while i < height:
|
||||||
|
if y_white > 0 and x_white > 0:
|
||||||
|
break
|
||||||
|
j = x_flag
|
||||||
|
while j < width:
|
||||||
|
total = 0
|
||||||
|
for k in range(planes):
|
||||||
|
px = lines[i][j * planes + k]
|
||||||
|
total += px
|
||||||
|
avg = total / planes
|
||||||
|
black = avg < threshold
|
||||||
|
if y_white > 0 and x_white > 0:
|
||||||
|
break
|
||||||
|
if x_flag > 0 > x_white and not black:
|
||||||
|
x_white = j
|
||||||
|
if x_flag == -1 and black:
|
||||||
|
x_flag = j
|
||||||
|
if y_flag > 0 > y_white and not black:
|
||||||
|
y_white = i
|
||||||
|
if y_flag == -1 and black:
|
||||||
|
y_flag = i
|
||||||
|
if x_flag > 0 and y_flag > 0:
|
||||||
|
i += 1
|
||||||
|
j += 1
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
assert y_white - y_flag == x_white - x_flag
|
||||||
|
scale = y_white - y_flag
|
||||||
|
|
||||||
|
assert width - x_flag == height - y_flag
|
||||||
|
module_count = int((width - x_flag * 2) / scale)
|
||||||
|
|
||||||
|
whole_white = '█'
|
||||||
|
whole_black = ' '
|
||||||
|
down_black = '▀'
|
||||||
|
up_black = '▄'
|
||||||
|
|
||||||
|
dual_flag = False
|
||||||
|
last_line = []
|
||||||
|
output = '\n'
|
||||||
|
for i in range(module_count + 2):
|
||||||
|
output += up_black
|
||||||
|
output += '\n'
|
||||||
|
i = y_flag
|
||||||
|
while i < height - y_flag:
|
||||||
|
if dual_flag:
|
||||||
|
output += whole_white
|
||||||
|
t = 0
|
||||||
|
j = x_flag
|
||||||
|
while j < width - x_flag:
|
||||||
|
total = 0
|
||||||
|
for k in range(planes):
|
||||||
|
px = lines[i][j * planes + k]
|
||||||
|
total += px
|
||||||
|
avg = total / planes
|
||||||
|
black = avg < threshold
|
||||||
|
if dual_flag:
|
||||||
|
last_black = last_line[t]
|
||||||
|
if black and last_black:
|
||||||
|
output += whole_black
|
||||||
|
elif black and not last_black:
|
||||||
|
output += down_black
|
||||||
|
elif not black and last_black:
|
||||||
|
output += up_black
|
||||||
|
elif not black and not last_black:
|
||||||
|
output += whole_white
|
||||||
|
else:
|
||||||
|
last_line[t:t+1] = [black]
|
||||||
|
t = t + 1
|
||||||
|
j += scale
|
||||||
|
if dual_flag:
|
||||||
|
output += whole_white + '\n'
|
||||||
|
dual_flag = not dual_flag
|
||||||
|
i += scale
|
||||||
|
output += whole_white
|
||||||
|
for i in range(module_count):
|
||||||
|
output += up_black if last_line[i] else whole_white
|
||||||
|
output += whole_white + '\n'
|
||||||
|
print(output, flush=True)
|
||||||
@@ -33,8 +33,9 @@ class Request(HTMLSession):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
def add_response_hook(self, hook):
|
def add_response_hook(self, hook):
|
||||||
exist_hooks = self.hooks['response']
|
hooks = self.hooks['response']
|
||||||
if not isinstance(exist_hooks, list): hooks = [exist_hooks]
|
if not isinstance(hooks, list):
|
||||||
|
hooks = [hooks]
|
||||||
hooks.append(hook)
|
hooks.append(hook)
|
||||||
self.hooks['response'] = hooks
|
self.hooks['response'] = hooks
|
||||||
return self
|
return self
|
||||||
@@ -76,3 +77,18 @@ class Request(HTMLSession):
|
|||||||
url = url.replace(HOST_URL_OF_12306, cdn)
|
url = url.replace(HOST_URL_OF_12306, cdn)
|
||||||
|
|
||||||
return self.request(method, url, headers={'Host': HOST_URL_OF_12306}, verify=False, **kwargs)
|
return self.request(method, url, headers={'Host': HOST_URL_OF_12306}, verify=False, **kwargs)
|
||||||
|
|
||||||
|
def dump_cookies(self):
|
||||||
|
cookies = []
|
||||||
|
for _, item in self.cookies._cookies.items():
|
||||||
|
for _, urls in item.items():
|
||||||
|
for _, cookie in urls.items():
|
||||||
|
from http.cookiejar import Cookie
|
||||||
|
assert isinstance(cookie, Cookie)
|
||||||
|
if cookie.domain:
|
||||||
|
cookies.append({
|
||||||
|
'name': cookie.name,
|
||||||
|
'value': cookie.value,
|
||||||
|
'url': 'https://' + cookie.domain + cookie.path,
|
||||||
|
})
|
||||||
|
return cookies
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class CommonLog(BaseLog):
|
|||||||
MESSAGE_SEND_EMAIL_SUCCESS = '邮件发送成功,请检查收件箱'
|
MESSAGE_SEND_EMAIL_SUCCESS = '邮件发送成功,请检查收件箱'
|
||||||
MESSAGE_SEND_EMAIL_FAIL = '邮件发送失败,请手动检查配置,错误原因 {}'
|
MESSAGE_SEND_EMAIL_FAIL = '邮件发送失败,请手动检查配置,错误原因 {}'
|
||||||
|
|
||||||
|
MESSAGE_SEND_EMAIL_WITH_QRCODE_SUCCESS = '二维码邮件发送成功,请检查收件箱扫描登陆'
|
||||||
|
|
||||||
MESSAGE_SEND_TELEGRAM_SUCCESS = 'Telegram推送成功'
|
MESSAGE_SEND_TELEGRAM_SUCCESS = 'Telegram推送成功'
|
||||||
MESSAGE_SEND_TELEGRAM_FAIL = 'Telegram推送失败,错误原因 {}'
|
MESSAGE_SEND_TELEGRAM_FAIL = 'Telegram推送失败,错误原因 {}'
|
||||||
|
|
||||||
@@ -94,15 +96,24 @@ class CommonLog(BaseLog):
|
|||||||
self.add_quick_log('多线程查询: {}'.format(get_true_false_text(Config().QUERY_JOB_THREAD_ENABLED, enable, disable)))
|
self.add_quick_log('多线程查询: {}'.format(get_true_false_text(Config().QUERY_JOB_THREAD_ENABLED, enable, disable)))
|
||||||
self.add_quick_log('CDN 状态: {}'.format(get_true_false_text(Config().CDN_ENABLED, enable, disable))).flush()
|
self.add_quick_log('CDN 状态: {}'.format(get_true_false_text(Config().CDN_ENABLED, enable, disable))).flush()
|
||||||
self.add_quick_log('通知状态:')
|
self.add_quick_log('通知状态:')
|
||||||
self.add_quick_log(
|
if Config().NOTIFICATION_BY_VOICE_CODE:
|
||||||
'语音验证码: {}'.format(get_true_false_text(Config().NOTIFICATION_BY_VOICE_CODE, enable, disable)))
|
self.add_quick_log(
|
||||||
self.add_quick_log('邮件通知: {}'.format(get_true_false_text(Config().EMAIL_ENABLED, enable, disable)))
|
'语音验证码: {}'.format(get_true_false_text(Config().NOTIFICATION_BY_VOICE_CODE, enable, disable)))
|
||||||
self.add_quick_log('钉钉通知: {}'.format(get_true_false_text(Config().DINGTALK_ENABLED, enable, disable)))
|
if Config().EMAIL_ENABLED:
|
||||||
self.add_quick_log('Telegram通知: {}'.format(get_true_false_text(Config().TELEGRAM_ENABLED, enable, disable)))
|
self.add_quick_log('邮件通知: {}'.format(get_true_false_text(Config().EMAIL_ENABLED, enable, disable)))
|
||||||
self.add_quick_log('ServerChan通知: {}'.format(get_true_false_text(Config().SERVERCHAN_ENABLED, enable, disable)))
|
if Config().DINGTALK_ENABLED:
|
||||||
self.add_quick_log('Bark通知: {}'.format(get_true_false_text(Config().BARK_ENABLED, enable, disable)))
|
self.add_quick_log('钉钉通知: {}'.format(get_true_false_text(Config().DINGTALK_ENABLED, enable, disable)))
|
||||||
self.add_quick_log(
|
if Config().TELEGRAM_ENABLED:
|
||||||
'PushBear通知: {}'.format(get_true_false_text(Config().PUSHBEAR_ENABLED, enable, disable))).flush(sep='\t\t')
|
self.add_quick_log('Telegram通知: {}'.format(get_true_false_text(Config().TELEGRAM_ENABLED, enable, disable)))
|
||||||
|
if Config().SERVERCHAN_ENABLED:
|
||||||
|
self.add_quick_log(
|
||||||
|
'ServerChan通知: {}'.format(get_true_false_text(Config().SERVERCHAN_ENABLED, enable, disable)))
|
||||||
|
if Config().BARK_ENABLED:
|
||||||
|
self.add_quick_log('Bark通知: {}'.format(get_true_false_text(Config().BARK_ENABLED, enable, disable)))
|
||||||
|
if Config().PUSHBEAR_ENABLED:
|
||||||
|
self.add_quick_log(
|
||||||
|
'PushBear通知: {}'.format(get_true_false_text(Config().PUSHBEAR_ENABLED, enable, disable)))
|
||||||
|
self.add_quick_log().flush(sep='\t\t')
|
||||||
self.add_quick_log('查询间隔: {} 秒'.format(Config().QUERY_INTERVAL))
|
self.add_quick_log('查询间隔: {} 秒'.format(Config().QUERY_INTERVAL))
|
||||||
self.add_quick_log('用户心跳检测间隔: {} 秒'.format(Config().USER_HEARTBEAT_INTERVAL))
|
self.add_quick_log('用户心跳检测间隔: {} 秒'.format(Config().USER_HEARTBEAT_INTERVAL))
|
||||||
self.add_quick_log('WEB 管理页面: {}'.format(get_true_false_text(Config().WEB_ENABLE, enable, disable)))
|
self.add_quick_log('WEB 管理页面: {}'.format(get_true_false_text(Config().WEB_ENABLE, enable, disable)))
|
||||||
@@ -130,3 +141,10 @@ class CommonLog(BaseLog):
|
|||||||
self.add_quick_log('打码失败: 错误原因 {reason}'.format(reason=reason))
|
self.add_quick_log('打码失败: 错误原因 {reason}'.format(reason=reason))
|
||||||
self.flush()
|
self.flush()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def print_auth_code_info(cls, reason):
|
||||||
|
self = cls()
|
||||||
|
self.add_quick_log('打码信息: {reason}'.format(reason=reason))
|
||||||
|
self.flush()
|
||||||
|
return self
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ class UserLog(BaseLog):
|
|||||||
MESSAGE_DOWNLAODING_THE_CODE = '正在下载验证码...'
|
MESSAGE_DOWNLAODING_THE_CODE = '正在下载验证码...'
|
||||||
MESSAGE_CODE_AUTH_FAIL = '验证码验证失败 错误原因: {}'
|
MESSAGE_CODE_AUTH_FAIL = '验证码验证失败 错误原因: {}'
|
||||||
MESSAGE_CODE_AUTH_SUCCESS = '验证码验证成功 开始登录...'
|
MESSAGE_CODE_AUTH_SUCCESS = '验证码验证成功 开始登录...'
|
||||||
|
MESSAGE_QRCODE_DOWNLOADING = '正在下载二维码...'
|
||||||
|
MESSAGE_QRCODE_DOWNLOADED = '二维码保存在: {},请使用手机客户端扫描'
|
||||||
|
MESSAGE_QRCODE_FAIL = '二维码获取失败: {}, {} 秒后重试'
|
||||||
MESSAGE_LOGIN_FAIL = '登录失败 错误原因: {}'
|
MESSAGE_LOGIN_FAIL = '登录失败 错误原因: {}'
|
||||||
MESSAGE_LOADED_USER = '正在尝试恢复用户: {}'
|
MESSAGE_LOADED_USER = '正在尝试恢复用户: {}'
|
||||||
MESSAGE_LOADED_USER_SUCCESS = '用户恢复成功: {}'
|
MESSAGE_LOADED_USER_SUCCESS = '用户恢复成功: {}'
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import asyncio
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
# from py12306.config import UserType
|
# from py12306.config import UserType
|
||||||
|
from pyppeteer import launch
|
||||||
|
|
||||||
from py12306.config import Config
|
from py12306.config import Config
|
||||||
from py12306.helpers.api import *
|
from py12306.helpers.api import *
|
||||||
from py12306.helpers.func import *
|
from py12306.helpers.func import *
|
||||||
@@ -10,6 +13,73 @@ from py12306.log.common_log import CommonLog
|
|||||||
from py12306.log.order_log import OrderLog
|
from py12306.log.order_log import OrderLog
|
||||||
|
|
||||||
|
|
||||||
|
class DomBounding:
|
||||||
|
def __init__(self, rect: dict) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.x = rect['x']
|
||||||
|
self.y = rect['y']
|
||||||
|
self.width = rect['width']
|
||||||
|
self.height = rect['height']
|
||||||
|
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
class Browser:
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def request_init_slide(self, session, html):
|
||||||
|
""" 处理滑块,拿到 session_id, sig """
|
||||||
|
OrderLog.add_quick_log('正在识别滑动验证码...').flush()
|
||||||
|
return asyncio.get_event_loop_policy().new_event_loop().run_until_complete(
|
||||||
|
self.__request_init_slide(session, html))
|
||||||
|
|
||||||
|
async def __request_init_slide(self, session, html):
|
||||||
|
""" 异步获取 """
|
||||||
|
browser = await launch(headless=True, autoClose=True, handleSIGINT=False, handleSIGTERM=False,
|
||||||
|
handleSIGHUP=False)
|
||||||
|
page = await browser.newPage()
|
||||||
|
await page.setViewport({'width': 1200, 'height': 1080})
|
||||||
|
await page.setRequestInterception(True)
|
||||||
|
load_js = """() => {
|
||||||
|
__old = navigator.userAgent; navigator.__defineGetter__('userAgent', () => __old.replace('Headless', ''));
|
||||||
|
__old = navigator.appVersion; navigator.__defineGetter__('appVersion', () => __old.replace('Headless', ''));
|
||||||
|
var __newProto = navigator.__proto__; delete __newProto.webdriver; navigator.__proto__ = __newProto;
|
||||||
|
}"""
|
||||||
|
source_url = 'https://kyfw.12306.cn/otn'
|
||||||
|
html = html.replace('href="/otn', f'href="{source_url}').replace('src="/otn', f'src="{source_url}')
|
||||||
|
|
||||||
|
@page.on('framenavigated')
|
||||||
|
async def on_frame_navigated(_):
|
||||||
|
await page.evaluate(load_js)
|
||||||
|
|
||||||
|
@page.on('request')
|
||||||
|
async def on_request(req):
|
||||||
|
if req.url.startswith(API_INITDC_URL):
|
||||||
|
if req.isNavigationRequest():
|
||||||
|
await page.setCookie(*session.dump_cookies())
|
||||||
|
return await req.respond({'body': html})
|
||||||
|
return await req.continue_()
|
||||||
|
|
||||||
|
await page.goto(API_INITDC_URL, timeout=30000)
|
||||||
|
slide_btn = await page.waitForSelector('#slide_passcode .nc-lang-cnt', timeout=30000)
|
||||||
|
rect = await slide_btn.boundingBox()
|
||||||
|
pos = DomBounding(rect)
|
||||||
|
pos.x += 5
|
||||||
|
pos.y += 10
|
||||||
|
await page.mouse.move(pos.x, pos.y)
|
||||||
|
await page.mouse.down()
|
||||||
|
await page.mouse.move(pos.x + pos.width, pos.y, steps=30)
|
||||||
|
await page.mouse.up()
|
||||||
|
# 等待获取 session id
|
||||||
|
await page.evaluate(
|
||||||
|
'async () => {let i = 3 * 10; while (!csessionid && i >= 0) await new Promise(resolve => setTimeout(resolve, 100), i--);}')
|
||||||
|
ret = await page.evaluate('JSON.stringify({session_id: csessionid, sig: sig})')
|
||||||
|
await page.close()
|
||||||
|
await browser.close()
|
||||||
|
return json.loads(ret)
|
||||||
|
|
||||||
|
|
||||||
class Order:
|
class Order:
|
||||||
"""
|
"""
|
||||||
处理下单
|
处理下单
|
||||||
@@ -41,6 +111,7 @@ class Order:
|
|||||||
assert isinstance(user, UserJob)
|
assert isinstance(user, UserJob)
|
||||||
self.query_ins = query
|
self.query_ins = query
|
||||||
self.user_ins = user
|
self.user_ins = user
|
||||||
|
self.is_slide = False
|
||||||
|
|
||||||
self.make_passenger_ticket_str()
|
self.make_passenger_ticket_str()
|
||||||
|
|
||||||
@@ -63,10 +134,25 @@ class Order:
|
|||||||
return self.order_did_success()
|
return self.order_did_success()
|
||||||
elif not order_request_res:
|
elif not order_request_res:
|
||||||
return
|
return
|
||||||
if not self.user_ins.request_init_dc_page(): return
|
init_res, self.is_slide, init_html = self.user_ins.request_init_dc_page()
|
||||||
if not self.check_order_info(): return
|
if not init_res:
|
||||||
if not self.get_queue_count(): return
|
return
|
||||||
if not self.confirm_single_for_queue(): return
|
slide_info = {}
|
||||||
|
if self.is_slide:
|
||||||
|
try:
|
||||||
|
slide_info = Browser().request_init_slide(self.session, init_html)
|
||||||
|
if not slide_info.get('session_id') or not slide_info.get('sig'):
|
||||||
|
raise Exception()
|
||||||
|
except Exception:
|
||||||
|
OrderLog.add_quick_log('滑动验证码识别失败').flush()
|
||||||
|
return
|
||||||
|
OrderLog.add_quick_log('滑动验证码识别成功').flush()
|
||||||
|
if not self.check_order_info(slide_info):
|
||||||
|
return
|
||||||
|
if not self.get_queue_count():
|
||||||
|
return
|
||||||
|
if not self.confirm_single_for_queue():
|
||||||
|
return
|
||||||
order_id = self.query_order_wait_time()
|
order_id = self.query_order_wait_time()
|
||||||
if order_id: # 发送通知
|
if order_id: # 发送通知
|
||||||
self.order_id = order_id
|
self.order_id = order_id
|
||||||
@@ -85,7 +171,8 @@ class Order:
|
|||||||
# num = 0 # 通知次数
|
# num = 0 # 通知次数
|
||||||
# sustain_time = self.notification_sustain_time
|
# sustain_time = self.notification_sustain_time
|
||||||
info_message = OrderLog.get_order_success_notification_info(self.query_ins)
|
info_message = OrderLog.get_order_success_notification_info(self.query_ins)
|
||||||
normal_message = OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_OF_EMAIL_CONTENT.format(self.order_id, self.user_ins.user_name)
|
normal_message = OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_OF_EMAIL_CONTENT.format(self.order_id,
|
||||||
|
self.user_ins.user_name)
|
||||||
if Config().EMAIL_ENABLED: # 邮件通知
|
if Config().EMAIL_ENABLED: # 邮件通知
|
||||||
Notification.send_email(Config().EMAIL_RECEIVER, OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_TITLE,
|
Notification.send_email(Config().EMAIL_RECEIVER, OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_TITLE,
|
||||||
normal_message + info_message)
|
normal_message + info_message)
|
||||||
@@ -100,7 +187,7 @@ class Order:
|
|||||||
Notification.push_bear(Config().PUSHBEAR_KEY, OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_TITLE,
|
Notification.push_bear(Config().PUSHBEAR_KEY, OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_TITLE,
|
||||||
normal_message + info_message)
|
normal_message + info_message)
|
||||||
if Config().BARK_ENABLED:
|
if Config().BARK_ENABLED:
|
||||||
Notification.push_bark(normal_message+info_message)
|
Notification.push_bark(normal_message + info_message)
|
||||||
|
|
||||||
if Config().NOTIFICATION_BY_VOICE_CODE: # 语音通知
|
if Config().NOTIFICATION_BY_VOICE_CODE: # 语音通知
|
||||||
if Config().NOTIFICATION_VOICE_CODE_TYPE == 'dingxin':
|
if Config().NOTIFICATION_VOICE_CODE_TYPE == 'dingxin':
|
||||||
@@ -138,7 +225,7 @@ class Order:
|
|||||||
}
|
}
|
||||||
response = self.session.post(API_SUBMIT_ORDER_REQUEST, data)
|
response = self.session.post(API_SUBMIT_ORDER_REQUEST, data)
|
||||||
result = response.json()
|
result = response.json()
|
||||||
if result.get('data') == 'N':
|
if result.get('data') == '0':
|
||||||
OrderLog.add_quick_log(OrderLog.MESSAGE_SUBMIT_ORDER_REQUEST_SUCCESS).flush()
|
OrderLog.add_quick_log(OrderLog.MESSAGE_SUBMIT_ORDER_REQUEST_SUCCESS).flush()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@@ -152,7 +239,7 @@ class Order:
|
|||||||
result.get('messages', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR))).flush()
|
result.get('messages', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR))).flush()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_order_info(self):
|
def check_order_info(self, slide_info=None):
|
||||||
"""
|
"""
|
||||||
cancel_flag=2
|
cancel_flag=2
|
||||||
bed_level_order_num=000000000000000000000000000000
|
bed_level_order_num=000000000000000000000000000000
|
||||||
@@ -175,6 +262,12 @@ class Order:
|
|||||||
'_json_att': '',
|
'_json_att': '',
|
||||||
'REPEAT_SUBMIT_TOKEN': self.user_ins.global_repeat_submit_token
|
'REPEAT_SUBMIT_TOKEN': self.user_ins.global_repeat_submit_token
|
||||||
}
|
}
|
||||||
|
if self.is_slide:
|
||||||
|
data.update({
|
||||||
|
'sessionId': slide_info['session_id'],
|
||||||
|
'sig': slide_info['sig'],
|
||||||
|
'scene': 'nc_login',
|
||||||
|
})
|
||||||
response = self.session.post(API_CHECK_ORDER_INFO, data)
|
response = self.session.post(API_CHECK_ORDER_INFO, data)
|
||||||
result = response.json()
|
result = response.json()
|
||||||
if result.get('data.submitStatus'): # 成功
|
if result.get('data.submitStatus'): # 成功
|
||||||
@@ -251,7 +344,7 @@ class Order:
|
|||||||
if ticket_number != '充足' and int(ticket_number) <= 0:
|
if ticket_number != '充足' and int(ticket_number) <= 0:
|
||||||
if self.query_ins.current_seat == SeatType.NO_SEAT: # 允许无座
|
if self.query_ins.current_seat == SeatType.NO_SEAT: # 允许无座
|
||||||
ticket_number = ticket[1]
|
ticket_number = ticket[1]
|
||||||
if not int(ticket_number): # 跳过无座
|
if not int(ticket_number): # 跳过无座
|
||||||
OrderLog.add_quick_log(OrderLog.MESSAGE_GET_QUEUE_INFO_NO_SEAT).flush()
|
OrderLog.add_quick_log(OrderLog.MESSAGE_GET_QUEUE_INFO_NO_SEAT).flush()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -374,10 +467,12 @@ class Order:
|
|||||||
elif 'waitTime' in result_data:
|
elif 'waitTime' in result_data:
|
||||||
# 计算等待时间
|
# 计算等待时间
|
||||||
wait_time = int(result_data.get('waitTime'))
|
wait_time = int(result_data.get('waitTime'))
|
||||||
if wait_time == -1 or wait_time == -100: # 成功
|
if wait_time == -1: # 成功
|
||||||
# /otn/confirmPassenger/resultOrderForDcQueue 请求订单状态 目前不需要
|
# /otn/confirmPassenger/resultOrderForDcQueue 请求订单状态 目前不需要
|
||||||
# 不应该走到这
|
# 不应该走到这
|
||||||
return order_id
|
return order_id
|
||||||
|
elif wait_time == -100: # 重新获取订单号
|
||||||
|
pass
|
||||||
elif wait_time >= 0: # 等待
|
elif wait_time >= 0: # 等待
|
||||||
OrderLog.add_quick_log(
|
OrderLog.add_quick_log(
|
||||||
OrderLog.MESSAGE_QUERY_ORDER_WAIT_TIME_WAITING.format(result_data.get('waitCount', 0),
|
OrderLog.MESSAGE_QUERY_ORDER_WAIT_TIME_WAITING.format(result_data.get('waitCount', 0),
|
||||||
@@ -429,11 +524,12 @@ class Order:
|
|||||||
OrderLog.print_passenger_did_deleted(available_passengers)
|
OrderLog.print_passenger_did_deleted(available_passengers)
|
||||||
|
|
||||||
for passenger in available_passengers:
|
for passenger in available_passengers:
|
||||||
tmp_str = '{seat_type},0,{passenger_type},{passenger_name},{passenger_id_card_type},{passenger_id_card},{passenger_mobile},N_'.format(
|
tmp_str = '{seat_type},0,{passenger_type},{passenger_name},{passenger_id_card_type},{passenger_id_card},{passenger_mobile},N,{enc_str}_'.format(
|
||||||
seat_type=self.query_ins.current_order_seat, passenger_type=passenger['type'],
|
seat_type=self.query_ins.current_order_seat, passenger_type=passenger['type'],
|
||||||
passenger_name=passenger['name'],
|
passenger_name=passenger['name'],
|
||||||
passenger_id_card_type=passenger['id_card_type'], passenger_id_card=passenger['id_card'],
|
passenger_id_card_type=passenger['id_card_type'], passenger_id_card=passenger['id_card'],
|
||||||
passenger_mobile=passenger['mobile']
|
passenger_mobile=passenger['mobile'],
|
||||||
|
enc_str=passenger['enc_str'],
|
||||||
)
|
)
|
||||||
passenger_tickets.append(tmp_str)
|
passenger_tickets.append(tmp_str)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import sys
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from py12306.app import app_available_check
|
from py12306.app import app_available_check
|
||||||
from py12306.cluster.cluster import Cluster
|
from py12306.cluster.cluster import Cluster
|
||||||
@@ -66,6 +66,8 @@ class Job:
|
|||||||
INDEX_LEFT_TIME = 8
|
INDEX_LEFT_TIME = 8
|
||||||
INDEX_ARRIVE_TIME = 9
|
INDEX_ARRIVE_TIME = 9
|
||||||
|
|
||||||
|
max_buy_time = 32
|
||||||
|
|
||||||
def __init__(self, info, query):
|
def __init__(self, info, query):
|
||||||
self.cluster = Cluster()
|
self.cluster = Cluster()
|
||||||
self.query = query
|
self.query = query
|
||||||
@@ -136,11 +138,29 @@ class Job:
|
|||||||
QueryLog.add_log('\n').flush(sep='\t\t', publish=False)
|
QueryLog.add_log('\n').flush(sep='\t\t', publish=False)
|
||||||
if Const.IS_TEST: return
|
if Const.IS_TEST: return
|
||||||
|
|
||||||
|
def judge_date_legal(self, date):
|
||||||
|
date_now = datetime.datetime.now()
|
||||||
|
date_query = datetime.datetime.strptime(str(date), "%Y-%m-%d")
|
||||||
|
diff = (date_query - date_now).days
|
||||||
|
if date_now.day == date_query.day:
|
||||||
|
diff = 0
|
||||||
|
if diff < 0:
|
||||||
|
msg = '乘车日期错误,比当前时间还早!!'
|
||||||
|
QueryLog.add_quick_log(msg).flush(publish=False)
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
elif diff > self.max_buy_time:
|
||||||
|
msg = '乘车日期错误,超出一个月预售期!!'
|
||||||
|
QueryLog.add_quick_log(msg).flush(publish=False)
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
else:
|
||||||
|
return date_query.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
def query_by_date(self, date):
|
def query_by_date(self, date):
|
||||||
"""
|
"""
|
||||||
通过日期进行查询
|
通过日期进行查询
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
date = self.judge_date_legal(date)
|
||||||
from py12306.helpers.cdn import Cdn
|
from py12306.helpers.cdn import Cdn
|
||||||
QueryLog.add_log(('\n' if not is_main_thread() else '') + QueryLog.MESSAGE_QUERY_START_BY_DATE.format(date,
|
QueryLog.add_log(('\n' if not is_main_thread() else '') + QueryLog.MESSAGE_QUERY_START_BY_DATE.format(date,
|
||||||
self.left_station,
|
self.left_station,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from base64 import b64decode
|
||||||
from py12306.config import Config
|
from py12306.config import Config
|
||||||
from py12306.cluster.cluster import Cluster
|
from py12306.cluster.cluster import Cluster
|
||||||
from py12306.app import app_available_check
|
from py12306.app import app_available_check
|
||||||
@@ -5,7 +6,7 @@ from py12306.helpers.func import *
|
|||||||
from py12306.helpers.request import Request
|
from py12306.helpers.request import Request
|
||||||
from py12306.log.query_log import QueryLog
|
from py12306.log.query_log import QueryLog
|
||||||
from py12306.query.job import Job
|
from py12306.query.job import Job
|
||||||
from py12306.helpers.api import API_QUERY_INIT_PAGE
|
from py12306.helpers.api import API_QUERY_INIT_PAGE, API_GET_BROWSER_DEVICE_ID
|
||||||
|
|
||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
@@ -29,6 +30,7 @@ class Query:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.session = Request()
|
self.session = Request()
|
||||||
|
self.request_device_id()
|
||||||
self.cluster = Cluster()
|
self.cluster = Cluster()
|
||||||
self.update_query_interval()
|
self.update_query_interval()
|
||||||
self.update_query_jobs()
|
self.update_query_jobs()
|
||||||
@@ -117,6 +119,32 @@ class Query:
|
|||||||
self.jobs.append(job)
|
self.jobs.append(job)
|
||||||
return job
|
return job
|
||||||
|
|
||||||
|
def request_device_id(self):
|
||||||
|
"""
|
||||||
|
获取加密后的浏览器特征 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
response = self.session.get(API_GET_BROWSER_DEVICE_ID)
|
||||||
|
if response.status_code == 200:
|
||||||
|
try:
|
||||||
|
result = json.loads(response.text)
|
||||||
|
response = self.session.get(b64decode(result['id']).decode())
|
||||||
|
if response.text.find('callbackFunction') >= 0:
|
||||||
|
result = response.text[18:-2]
|
||||||
|
result = json.loads(result)
|
||||||
|
if not Config().is_cache_rail_id_enabled():
|
||||||
|
self.session.cookies.update({
|
||||||
|
'RAIL_EXPIRATION': result.get('exp'),
|
||||||
|
'RAIL_DEVICEID': result.get('dfp'),
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
self.session.cookies.update({
|
||||||
|
'RAIL_EXPIRATION': Config().RAIL_EXPIRATION,
|
||||||
|
'RAIL_DEVICEID': Config().RAIL_DEVICEID,
|
||||||
|
})
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def wait_for_ready(cls):
|
def wait_for_ready(cls):
|
||||||
self = cls()
|
self = cls()
|
||||||
@@ -149,11 +177,14 @@ class Query:
|
|||||||
return self.api_type
|
return self.api_type
|
||||||
response = self.session.get(API_QUERY_INIT_PAGE)
|
response = self.session.get(API_QUERY_INIT_PAGE)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
res = re.search(r'var CLeftTicketUrl = \'(leftTicket/queryX)\';', response.text)
|
res = re.search(r'var CLeftTicketUrl = \'(.*)\';', response.text)
|
||||||
try:
|
try:
|
||||||
self.api_type = res.group(1)
|
self.api_type = res.group(1)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
|
if not self.api_type:
|
||||||
|
QueryLog.add_quick_log('查询地址获取失败, 正在重新获取...').flush()
|
||||||
|
sleep(1)
|
||||||
return cls.get_query_api_type()
|
return cls.get_query_api_type()
|
||||||
|
|
||||||
# def get_jobs_from_cluster(self):
|
# def get_jobs_from_cluster(self):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import json
|
import base64
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
import re
|
||||||
from os import path
|
from os import path
|
||||||
@@ -11,6 +11,7 @@ from py12306.helpers.event import Event
|
|||||||
from py12306.helpers.func import *
|
from py12306.helpers.func import *
|
||||||
from py12306.helpers.request import Request
|
from py12306.helpers.request import Request
|
||||||
from py12306.helpers.type import UserType
|
from py12306.helpers.type import UserType
|
||||||
|
from py12306.helpers.qrcode import print_qrcode
|
||||||
from py12306.log.order_log import OrderLog
|
from py12306.log.order_log import OrderLog
|
||||||
from py12306.log.user_log import UserLog
|
from py12306.log.user_log import UserLog
|
||||||
from py12306.log.common_log import CommonLog
|
from py12306.log.common_log import CommonLog
|
||||||
@@ -23,6 +24,7 @@ class UserJob:
|
|||||||
key = None
|
key = None
|
||||||
user_name = ''
|
user_name = ''
|
||||||
password = ''
|
password = ''
|
||||||
|
type = 'qr'
|
||||||
user = None
|
user = None
|
||||||
info = {} # 用户信息
|
info = {} # 用户信息
|
||||||
last_heartbeat = None
|
last_heartbeat = None
|
||||||
@@ -30,6 +32,7 @@ class UserJob:
|
|||||||
user_loaded = False # 用户是否已加载成功
|
user_loaded = False # 用户是否已加载成功
|
||||||
passengers = []
|
passengers = []
|
||||||
retry_time = 3
|
retry_time = 3
|
||||||
|
retry_count = 0
|
||||||
login_num = 0 # 尝试登录次数
|
login_num = 0 # 尝试登录次数
|
||||||
|
|
||||||
# Init page
|
# Init page
|
||||||
@@ -51,6 +54,7 @@ class UserJob:
|
|||||||
self.key = str(info.get('key'))
|
self.key = str(info.get('key'))
|
||||||
self.user_name = info.get('user_name')
|
self.user_name = info.get('user_name')
|
||||||
self.password = info.get('password')
|
self.password = info.get('password')
|
||||||
|
self.type = info.get('type')
|
||||||
|
|
||||||
def update_user(self):
|
def update_user(self):
|
||||||
from py12306.user.user import User
|
from py12306.user.user import User
|
||||||
@@ -111,7 +115,10 @@ class UserJob:
|
|||||||
if expire: UserLog.print_user_expired()
|
if expire: UserLog.print_user_expired()
|
||||||
self.is_ready = False
|
self.is_ready = False
|
||||||
UserLog.print_start_login(user=self)
|
UserLog.print_start_login(user=self)
|
||||||
return self.login()
|
if self.type == 'qr':
|
||||||
|
return self.qr_login()
|
||||||
|
else:
|
||||||
|
return self.login()
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
"""
|
"""
|
||||||
@@ -125,6 +132,7 @@ class UserJob:
|
|||||||
}
|
}
|
||||||
answer = AuthCode.get_auth_code(self.session)
|
answer = AuthCode.get_auth_code(self.session)
|
||||||
data['answer'] = answer
|
data['answer'] = answer
|
||||||
|
self.request_device_id()
|
||||||
response = self.session.post(API_BASE_LOGIN.get('url'), data)
|
response = self.session.post(API_BASE_LOGIN.get('url'), data)
|
||||||
result = response.json()
|
result = response.json()
|
||||||
if result.get('result_code') == 0: # 登录成功
|
if result.get('result_code') == 0: # 登录成功
|
||||||
@@ -149,6 +157,82 @@ class UserJob:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def qr_login(self):
|
||||||
|
self.request_device_id()
|
||||||
|
image_uuid, png_path = self.download_code()
|
||||||
|
while True:
|
||||||
|
data = {
|
||||||
|
'RAIL_DEVICEID': self.session.cookies.get('RAIL_DEVICEID'),
|
||||||
|
'RAIL_EXPIRATION': self.session.cookies.get('RAIL_EXPIRATION'),
|
||||||
|
'uuid': image_uuid,
|
||||||
|
'appid': 'otn'
|
||||||
|
}
|
||||||
|
response = self.session.post(API_AUTH_QRCODE_CHECK.get('url'), data)
|
||||||
|
result = response.json()
|
||||||
|
result_code = int(result.get('result_code'))
|
||||||
|
if result_code == 0:
|
||||||
|
time.sleep(2)
|
||||||
|
elif result_code == 1:
|
||||||
|
UserLog.add_quick_log('请确认登录').flush()
|
||||||
|
time.sleep(2)
|
||||||
|
elif result_code == 2:
|
||||||
|
break
|
||||||
|
elif result_code == 3:
|
||||||
|
try:
|
||||||
|
os.remove(png_path)
|
||||||
|
except Exception as e:
|
||||||
|
UserLog.add_quick_log('无法删除文件: {}'.format(e)).flush()
|
||||||
|
image_uuid = self.download_code()
|
||||||
|
try:
|
||||||
|
os.remove(png_path)
|
||||||
|
except Exception as e:
|
||||||
|
UserLog.add_quick_log('无法删除文件: {}'.format(e)).flush()
|
||||||
|
|
||||||
|
self.session.get(API_USER_LOGIN, allow_redirects=True)
|
||||||
|
new_tk = self.auth_uamtk()
|
||||||
|
user_name = self.auth_uamauthclient(new_tk)
|
||||||
|
self.update_user_info({'user_name': user_name})
|
||||||
|
self.session.get(API_USER_LOGIN, allow_redirects=True)
|
||||||
|
self.login_did_success()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def download_code(self):
|
||||||
|
try:
|
||||||
|
UserLog.add_quick_log(UserLog.MESSAGE_QRCODE_DOWNLOADING).flush()
|
||||||
|
response = self.session.post(API_AUTH_QRCODE_BASE64_DOWNLOAD.get('url'), data={'appid': 'otn'})
|
||||||
|
result = response.json()
|
||||||
|
if result.get('result_code') == '0':
|
||||||
|
img_bytes = base64.b64decode(result.get('image'))
|
||||||
|
try:
|
||||||
|
os.mkdir(Config().USER_DATA_DIR + '/qrcode')
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
png_path = path.normpath(Config().USER_DATA_DIR + '/qrcode/%d.png' % time.time())
|
||||||
|
with open(png_path, 'wb') as file:
|
||||||
|
file.write(img_bytes)
|
||||||
|
file.close()
|
||||||
|
if os.name == 'nt':
|
||||||
|
os.startfile(png_path)
|
||||||
|
else:
|
||||||
|
print_qrcode(png_path)
|
||||||
|
UserLog.add_log(UserLog.MESSAGE_QRCODE_DOWNLOADED.format(png_path)).flush()
|
||||||
|
Notification.send_email_with_qrcode(Config().EMAIL_RECEIVER, '你有新的登录二维码啦!', png_path)
|
||||||
|
self.retry_count = 0
|
||||||
|
return result.get('uuid'), png_path
|
||||||
|
raise KeyError('获取二维码失败: {}'.format(result.get('result_message')))
|
||||||
|
except Exception as e:
|
||||||
|
UserLog.add_quick_log(
|
||||||
|
UserLog.MESSAGE_QRCODE_FAIL.format(e, self.retry_time)).flush()
|
||||||
|
self.retry_count = self.retry_count + 1
|
||||||
|
if self.retry_count == 20:
|
||||||
|
self.retry_count = 0
|
||||||
|
try:
|
||||||
|
os.remove(self.get_cookie_path())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(self.retry_time)
|
||||||
|
return self.download_code()
|
||||||
|
|
||||||
def check_user_is_login(self):
|
def check_user_is_login(self):
|
||||||
response = self.session.get(API_USER_LOGIN_CHECK)
|
response = self.session.get(API_USER_LOGIN_CHECK)
|
||||||
is_login = response.json().get('data.is_login', False) == 'Y'
|
is_login = response.json().get('data.is_login', False) == 'Y'
|
||||||
@@ -160,7 +244,10 @@ class UserJob:
|
|||||||
return is_login
|
return is_login
|
||||||
|
|
||||||
def auth_uamtk(self):
|
def auth_uamtk(self):
|
||||||
response = self.session.post(API_AUTH_UAMTK.get('url'), {'appid': 'otn'})
|
response = self.session.post(API_AUTH_UAMTK.get('url'), {'appid': 'otn'}, headers={
|
||||||
|
'Referer': 'https://kyfw.12306.cn/otn/passport?redirect=/otn/login/userLogin',
|
||||||
|
'Origin': 'https://kyfw.12306.cn'
|
||||||
|
})
|
||||||
result = response.json()
|
result = response.json()
|
||||||
if result.get('newapptk'):
|
if result.get('newapptk'):
|
||||||
return result.get('newapptk')
|
return result.get('newapptk')
|
||||||
@@ -175,6 +262,36 @@ class UserJob:
|
|||||||
# TODO 处理获取失败情况
|
# TODO 处理获取失败情况
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def request_device_id(self):
|
||||||
|
"""
|
||||||
|
获取加密后的浏览器特征 ID
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
response = self.session.get(API_GET_BROWSER_DEVICE_ID)
|
||||||
|
if response.status_code == 200:
|
||||||
|
try:
|
||||||
|
result = json.loads(response.text)
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36"
|
||||||
|
}
|
||||||
|
self.session.headers.update(headers)
|
||||||
|
response = self.session.get(base64.b64decode(result['id']).decode())
|
||||||
|
if response.text.find('callbackFunction') >= 0:
|
||||||
|
result = response.text[18:-2]
|
||||||
|
result = json.loads(result)
|
||||||
|
if not Config().is_cache_rail_id_enabled():
|
||||||
|
self.session.cookies.update({
|
||||||
|
'RAIL_EXPIRATION': result.get('exp'),
|
||||||
|
'RAIL_DEVICEID': result.get('dfp'),
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
self.session.cookies.update({
|
||||||
|
'RAIL_EXPIRATION': Config().RAIL_EXPIRATION,
|
||||||
|
'RAIL_DEVICEID': Config().RAIL_DEVICEID,
|
||||||
|
})
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
def login_did_success(self):
|
def login_did_success(self):
|
||||||
"""
|
"""
|
||||||
用户登录成功
|
用户登录成功
|
||||||
@@ -312,7 +429,7 @@ class UserJob:
|
|||||||
UserLog.MESSAGE_GET_USER_PASSENGERS_FAIL.format(
|
UserLog.MESSAGE_GET_USER_PASSENGERS_FAIL.format(
|
||||||
result.get('messages', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR), self.retry_time)).flush()
|
result.get('messages', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR), self.retry_time)).flush()
|
||||||
if Config().is_slave():
|
if Config().is_slave():
|
||||||
self.load_user_from_remote() # 加载最新 cookie
|
self.load_user_from_remote() # 加载最新 cookie
|
||||||
stay_second(self.retry_time)
|
stay_second(self.retry_time)
|
||||||
return self.get_user_passengers()
|
return self.get_user_passengers()
|
||||||
|
|
||||||
@@ -325,7 +442,8 @@ class UserJob:
|
|||||||
name: '项羽',
|
name: '项羽',
|
||||||
type: 1,
|
type: 1,
|
||||||
id_card: 0000000000000000000,
|
id_card: 0000000000000000000,
|
||||||
type_text: '成人'
|
type_text: '成人',
|
||||||
|
enc_str: 'aaaaaa'
|
||||||
}]
|
}]
|
||||||
"""
|
"""
|
||||||
self.get_user_passengers()
|
self.get_user_passengers()
|
||||||
@@ -333,6 +451,11 @@ class UserJob:
|
|||||||
for member in members:
|
for member in members:
|
||||||
is_member_code = is_number(member)
|
is_member_code = is_number(member)
|
||||||
if not is_member_code:
|
if not is_member_code:
|
||||||
|
if member[0] == "*":
|
||||||
|
audlt = 1
|
||||||
|
member = member[1:]
|
||||||
|
else:
|
||||||
|
audlt = 0
|
||||||
child_check = array_dict_find_by_key_value(results, 'name', member)
|
child_check = array_dict_find_by_key_value(results, 'name', member)
|
||||||
if not is_member_code and child_check:
|
if not is_member_code and child_check:
|
||||||
new_member = child_check.copy()
|
new_member = child_check.copy()
|
||||||
@@ -343,6 +466,8 @@ class UserJob:
|
|||||||
passenger = array_dict_find_by_key_value(self.passengers, 'code', member)
|
passenger = array_dict_find_by_key_value(self.passengers, 'code', member)
|
||||||
else:
|
else:
|
||||||
passenger = array_dict_find_by_key_value(self.passengers, 'passenger_name', member)
|
passenger = array_dict_find_by_key_value(self.passengers, 'passenger_name', member)
|
||||||
|
if audlt:
|
||||||
|
passenger['passenger_type'] = UserType.ADULT
|
||||||
if not passenger:
|
if not passenger:
|
||||||
UserLog.add_quick_log(
|
UserLog.add_quick_log(
|
||||||
UserLog.MESSAGE_USER_PASSENGERS_IS_INVALID.format(self.user_name, member)).flush()
|
UserLog.MESSAGE_USER_PASSENGERS_IS_INVALID.format(self.user_name, member)).flush()
|
||||||
@@ -353,7 +478,8 @@ class UserJob:
|
|||||||
'id_card_type': passenger.get('passenger_id_type_code'),
|
'id_card_type': passenger.get('passenger_id_type_code'),
|
||||||
'mobile': passenger.get('mobile_no'),
|
'mobile': passenger.get('mobile_no'),
|
||||||
'type': passenger.get('passenger_type'),
|
'type': passenger.get('passenger_type'),
|
||||||
'type_text': dict_find_key_by_value(UserType.dicts, int(passenger.get('passenger_type')))
|
'type_text': dict_find_key_by_value(UserType.dicts, int(passenger.get('passenger_type'))),
|
||||||
|
'enc_str': passenger.get('allEncStr')
|
||||||
}
|
}
|
||||||
results.append(new_member)
|
results.append(new_member)
|
||||||
|
|
||||||
@@ -373,12 +499,16 @@ class UserJob:
|
|||||||
# 系统忙,请稍后重试
|
# 系统忙,请稍后重试
|
||||||
if html.find('系统忙,请稍后重试') != -1:
|
if html.find('系统忙,请稍后重试') != -1:
|
||||||
OrderLog.add_quick_log(OrderLog.MESSAGE_REQUEST_INIT_DC_PAGE_FAIL).flush() # 重试无用,直接跳过
|
OrderLog.add_quick_log(OrderLog.MESSAGE_REQUEST_INIT_DC_PAGE_FAIL).flush() # 重试无用,直接跳过
|
||||||
return False
|
return False, False, html
|
||||||
try:
|
try:
|
||||||
self.global_repeat_submit_token = token.groups()[0]
|
self.global_repeat_submit_token = token.groups()[0]
|
||||||
self.ticket_info_for_passenger_form = json.loads(form.groups()[0].replace("'", '"'))
|
self.ticket_info_for_passenger_form = json.loads(form.groups()[0].replace("'", '"'))
|
||||||
self.order_request_dto = json.loads(order.groups()[0].replace("'", '"'))
|
self.order_request_dto = json.loads(order.groups()[0].replace("'", '"'))
|
||||||
except:
|
except:
|
||||||
pass # TODO Error
|
return False, False, html # TODO Error
|
||||||
|
|
||||||
return True
|
slide_val = re.search(r"var if_check_slide_passcode.*='(\d?)'", html)
|
||||||
|
is_slide = False
|
||||||
|
if slide_val:
|
||||||
|
is_slide = int(slide_val[1]) == 1
|
||||||
|
return True, is_slide, html
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class User:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def run(cls):
|
def run(cls):
|
||||||
self = cls()
|
self = cls()
|
||||||
app_available_check()
|
# app_available_check() 用户系统不休息
|
||||||
self.start()
|
self.start()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
-i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
|
-i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
appdirs==1.4.3
|
appdirs==1.4.3
|
||||||
beautifulsoup4==4.7.0
|
beautifulsoup4==4.7.0
|
||||||
bs4==0.0.1
|
bs4==0.0.1
|
||||||
@@ -12,12 +12,12 @@ Flask-JWT-Extended==3.15.0
|
|||||||
idna==2.8
|
idna==2.8
|
||||||
itsdangerous==1.1.0
|
itsdangerous==1.1.0
|
||||||
Jinja2==2.10
|
Jinja2==2.10
|
||||||
lxml==4.3.0
|
lxml==4.6.3
|
||||||
MarkupSafe==1.1.0
|
MarkupSafe==1.1.0
|
||||||
parse==1.9.0
|
parse==1.9.0
|
||||||
pyee==5.0.0
|
pyee==5.0.0
|
||||||
PyJWT==1.7.1
|
PyJWT==1.7.1
|
||||||
pyppeteer==0.0.25
|
pyppeteer-box==0.0.27
|
||||||
pyquery==1.4.0
|
pyquery==1.4.0
|
||||||
redis==3.0.1
|
redis==3.0.1
|
||||||
requests==2.21.0
|
requests==2.21.0
|
||||||
@@ -25,9 +25,10 @@ requests-html==0.9.0
|
|||||||
six==1.12.0
|
six==1.12.0
|
||||||
soupsieve==1.6.2
|
soupsieve==1.6.2
|
||||||
tqdm==4.28.1
|
tqdm==4.28.1
|
||||||
urllib3==1.24.1
|
urllib3==1.24.2
|
||||||
w3lib==1.19.0
|
w3lib==1.19.0
|
||||||
websockets==7.0
|
websockets==7.0
|
||||||
Werkzeug==0.14.1
|
Werkzeug==0.15.5
|
||||||
DingtalkChatbot==1.3.0
|
DingtalkChatbot==1.3.0
|
||||||
lightpush==0.1.3
|
lightpush==0.1.3
|
||||||
|
pypng
|
||||||
|
|||||||
Reference in New Issue
Block a user