Compare commits
138 Commits
fix/drag-c
...
v2.0-legac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad83fd261a | ||
|
|
8b795d3217 | ||
|
|
f8a73ff455 | ||
|
|
368bded297 | ||
|
|
0913772ec6 | ||
|
|
f49b081bc5 | ||
|
|
8d4ea3066a | ||
|
|
c0d28e6744 | ||
|
|
4935582b19 | ||
|
|
656dbe87d9 | ||
|
|
80fdd4db61 | ||
|
|
1e25b9079c | ||
|
|
c87724d170 | ||
|
|
cdedfa859d | ||
|
|
484c5f2aa3 | ||
|
|
996fc245cc | ||
|
|
66ad9c9e5c | ||
|
|
e81a239609 | ||
|
|
8ed02ea625 | ||
|
|
9462e0d72c | ||
|
|
696f888ad1 | ||
|
|
68445e1913 | ||
|
|
547300b609 | ||
|
|
4398e60305 | ||
|
|
15f6932f50 | ||
|
|
35c45bbeac | ||
|
|
f781c353c3 | ||
|
|
79929da88d | ||
|
|
25ba185c27 | ||
|
|
33e38da2b2 | ||
|
|
91e68f68c6 | ||
|
|
5c8a1327dc | ||
|
|
8b90194f65 | ||
|
|
5eb412d45a | ||
|
|
864c239a24 | ||
|
|
3a7700c2d4 | ||
|
|
c613b54dec | ||
|
|
0c8174592d | ||
|
|
05ffcffe68 | ||
|
|
c2cf3ef79c | ||
|
|
69f1a84c04 | ||
|
|
21b092245b | ||
|
|
6d47181513 | ||
|
|
89c7e2b0f9 | ||
|
|
1ff1676f46 | ||
|
|
493411ecbf | ||
|
|
b72f7ec5dc | ||
|
|
ecd809dc16 | ||
|
|
26063f5be6 | ||
|
|
532e165688 | ||
|
|
901ba7f15a | ||
|
|
2e89bd1d89 | ||
|
|
8139eef48f | ||
|
|
deab8142a1 | ||
|
|
e4831d6df9 | ||
|
|
9b7f16d60a | ||
|
|
e6b30b8427 | ||
|
|
d4e1ef1049 | ||
|
|
033c34e035 | ||
|
|
ab84b92665 | ||
|
|
5ef5df52a5 | ||
|
|
b93807155e | ||
|
|
b3683cbbd2 | ||
|
|
25b6bd536a | ||
|
|
d4339e2e38 | ||
|
|
e7f293edaf | ||
|
|
9346546dd9 | ||
|
|
45ce85afde | ||
|
|
7437564e64 | ||
|
|
d98c98069a | ||
|
|
76a2b0f98a | ||
|
|
e4f20350fe | ||
|
|
71f2972e3f | ||
|
|
957c768a43 | ||
|
|
6685c1570e | ||
|
|
6a5d58605d | ||
|
|
ebca190c26 | ||
|
|
e788bb03cc | ||
|
|
02b6169b35 | ||
|
|
1719bc718a | ||
|
|
21fa98c9d0 | ||
|
|
08ff445604 | ||
|
|
e0b10870dd | ||
|
|
2b5e0cf8be | ||
|
|
8a0309dd7c | ||
|
|
18df704c08 | ||
|
|
d7c9a1cbd4 | ||
|
|
b78f1c6af7 | ||
|
|
ae418ec3dd | ||
|
|
9f8187a58f | ||
|
|
b333b09f83 | ||
|
|
82b1f2c3c6 | ||
|
|
c8988cd214 | ||
|
|
01cf6cca33 | ||
|
|
f724139d8c | ||
|
|
454d5b2fbf | ||
|
|
70e2c0e736 | ||
|
|
25049389ff | ||
|
|
7c62c1ebad | ||
|
|
5838026b25 | ||
|
|
a0bdc4ca82 | ||
|
|
3b15f260ae | ||
|
|
fc595d4590 | ||
|
|
b1e826b27a | ||
|
|
c49ac26f0e | ||
|
|
5d492c8e9e | ||
|
|
ee677c56ee | ||
|
|
a337f972a8 | ||
|
|
d5097d2374 | ||
|
|
0c14ebd0ab | ||
|
|
845f303293 | ||
|
|
dba893e705 | ||
|
|
8e9468406a | ||
|
|
ed04957863 | ||
|
|
ba489fd9bc | ||
|
|
a87f10c884 | ||
|
|
da469f3ea1 | ||
|
|
247c46db2a | ||
|
|
ebdd2b6645 | ||
|
|
bbba2caab6 | ||
|
|
f93fdcfd88 | ||
|
|
b323e3d953 | ||
|
|
e25dda3a03 | ||
|
|
2ee8dc5da1 | ||
|
|
c804de2e07 | ||
|
|
5de41150d9 | ||
|
|
6dc09572c1 | ||
|
|
b0888dd8c6 | ||
|
|
0b054997c5 | ||
|
|
da5f4e93c6 | ||
|
|
bc29d8a8aa | ||
|
|
a0f26435b7 | ||
|
|
054c476c4e | ||
|
|
bb136d17ba | ||
|
|
33be8c9237 | ||
|
|
98bafed66c | ||
|
|
bcf1f623d6 | ||
|
|
44d9a91da6 |
5
.gitignore
vendored
@@ -4,6 +4,5 @@
|
||||
venv
|
||||
__pycache__
|
||||
env.py
|
||||
env.slave.py
|
||||
env.docker.py
|
||||
docker-compose.yml
|
||||
docker-compose.yml
|
||||
config.toml
|
||||
@@ -1,6 +0,0 @@
|
||||
ports:
|
||||
- port: 8008
|
||||
onOpen: open-preview
|
||||
tasks:
|
||||
- init: pip install -r requirements.txt && cp env.py.example env.py
|
||||
command: python main.py -t
|
||||
14
README.md
@@ -1,6 +1,10 @@
|
||||
# 🚂 py12306 购票助手
|
||||
分布式,多账号,多任务购票
|
||||
|
||||
## 前言
|
||||
今年回家的票明显要难买很多,早早就答应了父母今年的票没问题,到现在一张票没买到,虽然家里已经订了汽车票,让我不用操心,但是想想他们一行还有小孩,心还是很伤的。
|
||||
这段时间从 12306Bypass 到 testerSunshine 大佬写的 [12306](https://github.com/testerSunshine/12306),还是没买到票,索性就自己写了一个,希望也能帮助到更多人
|
||||
|
||||
## Features
|
||||
- [x] 多日期查询余票
|
||||
- [x] 自动打码下单
|
||||
@@ -14,7 +18,7 @@
|
||||
- [x] 邮件通知
|
||||
- [x] Web 管理页面
|
||||
- [x] 微信消息通知
|
||||
- [ ] 代理池支持 ([pyproxy-async](https://github.com/pjialin/pyproxy-async))
|
||||
- [ ] 代理池支持
|
||||
|
||||
## 使用
|
||||
py12306 需要运行在 python 3.6 以上版本(其它版本暂未测试)
|
||||
@@ -32,8 +36,9 @@ 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) 注册一个账号后填写到配置中
|
||||
|
||||
语音通知
|
||||
|
||||
@@ -143,7 +148,7 @@ docker-compose up -d
|
||||
### 关于防封
|
||||
目前查询和登录操作是分开的,查询是不依赖用户是否登录,放在 A 云 T 云容易被限制 ip,建议在其它网络环境下运行
|
||||
|
||||
QQ 交流群 [780289875](https://jq.qq.com/?_wv=1027&k=5PgzDwV),TG 群 [Py12306 交流](https://t.me/joinchat/F3sSegrF3x8KAmsd1mTu7w)
|
||||
交流群 [274781597](http://shang.qq.com/wpa/qunwpa?idkey=8eab0b6402096266a62263c1cd452149926adb5cba7a2b7a98a5adc65869addf)
|
||||
|
||||
### Online IDE
|
||||
[](https://gitpod.io#https://github.com/pjialin/py12306)
|
||||
@@ -151,7 +156,6 @@ QQ 交流群 [780289875](https://jq.qq.com/?_wv=1027&k=5PgzDwV),TG 群 [Py1230
|
||||
## Thanks
|
||||
- 感谢大佬 [testerSunshine](https://github.com/testerSunshine/12306),借鉴了部分实现
|
||||
- 感谢所有提供 pr 的大佬
|
||||
- 感谢大佬 [zhaipro](https://github.com/zhaipro/easy12306) 的验证码本地识别模型与算法
|
||||
|
||||
## License
|
||||
|
||||
|
||||
48
data/cdn.txt
@@ -2218,51 +2218,3 @@
|
||||
117.27.241.218
|
||||
112.65.92.116
|
||||
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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version: "2"
|
||||
version: "3"
|
||||
services:
|
||||
py12306:
|
||||
build: .
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 12306 账号
|
||||
USER_ACCOUNTS = [
|
||||
# 目前已支持仅查询,不下单,屏蔽掉下面的账号即可
|
||||
{
|
||||
'key': 0, # 如使用多个账号 key 不能重复
|
||||
'user_name': 'your user name',
|
||||
'password': 'your password'
|
||||
},
|
||||
# {
|
||||
# 'key': 'wangwu',
|
||||
# 'user_name': 'wangwu@qq.com',
|
||||
# 'password': 'wangwu'
|
||||
# }
|
||||
]
|
||||
|
||||
# 查询间隔(指每一个任务中每一个日期的间隔 / 单位秒)
|
||||
# 默认取间隔/2 到 间隔之间的随机数 如设置为 1 间隔则为 0.5 ~ 1 之间的随机数
|
||||
# 接受字典形式 格式: {'min': 0.5, 'max': 1}
|
||||
QUERY_INTERVAL = 1
|
||||
|
||||
# 用户心跳检测间隔 格式同上
|
||||
USER_HEARTBEAT_INTERVAL = 120
|
||||
|
||||
# 多线程查询
|
||||
QUERY_JOB_THREAD_ENABLED = 0 # 是否开启多线程查询,开启后第个任务会单独分配线程处理
|
||||
|
||||
# 打码平台账号
|
||||
# 目前只支持免费打码接口 和 若快打码,注册地址:http://www.ruokuai.com/login
|
||||
AUTO_CODE_PLATFORM = 'free' # 免费填写 free 若快 ruokuai # 免费打码无法保证持续可用,如失效请手动切换 #个人本地打码填写 user,并修改 API_USER_CODE_QCR_API
|
||||
API_USER_CODE_QCR_API = ''
|
||||
AUTO_CODE_ACCOUNT = {
|
||||
'user': 'your user name',
|
||||
'pwd': 'your password'
|
||||
}
|
||||
|
||||
# 语音验证码
|
||||
# 没找到比较好用的,现在用的这个是阿里云 API 市场上的,基本满足要求,价格也便宜
|
||||
# 购买成功后到控制台找到 APPCODE 放在下面就可以了
|
||||
# 地址:易源 https://market.aliyun.com/products/57126001/cmapi019902.html
|
||||
# 2019-01-18 更新
|
||||
# 增加新的服务商 鼎信 https://market.aliyun.com/products/56928004/cmapi026600.html?spm=5176.2020520132.101.2.e27e7218KQttQS
|
||||
NOTIFICATION_BY_VOICE_CODE = 1 # 开启语音通知
|
||||
NOTIFICATION_VOICE_CODE_TYPE = 'dingxin' # 语音验证码服务商 可用项 dingxin yiyuan
|
||||
NOTIFICATION_API_APP_CODE = 'your app code'
|
||||
NOTIFICATION_VOICE_CODE_PHONE = 'your phone' # 接受通知的手机号
|
||||
|
||||
# 钉钉通知
|
||||
DINGTALK_ENABLED = 0
|
||||
DINGTALK_WEBHOOK = 'https://oapi.dingtalk.com/robot/send?access_token=your token'
|
||||
|
||||
# Telegram消息推送
|
||||
# 目前共有两个Bot:
|
||||
# 1:https://t.me/notificationme_bot
|
||||
# 2:https://t.me/RE_Link_Push_bot
|
||||
# 任选一个Bot,关注获取URL链接,如果没有回复则发送给Bot这条信息: /start
|
||||
# 将获取的URL填入下面对应位置
|
||||
# 注意:因为以上Bot都由他人公益提供,无法保证随时可用,如以上Bot都无法使用,请使用其他消息推送方式
|
||||
# Bot1来源:https://github.com/Fndroid/tg_push_bot
|
||||
# Bot2来源:https://szc.me/post/2.html
|
||||
TELEGRAM_ENABLED = 0
|
||||
TELEGRAM_BOT_API_URL = 'https://tgbot.lbyczf.com/sendMessage/:your_token'
|
||||
|
||||
# ServerChan 和 PushBear 微信消息推送
|
||||
# 使用说明
|
||||
# ServerChan http://sc.ftqq.com
|
||||
# PushBear http://pushbear.ftqq.com
|
||||
SERVERCHAN_ENABLED = 0
|
||||
SERVERCHAN_KEY = ''
|
||||
PUSHBEAR_ENABLED = 0
|
||||
PUSHBEAR_KEY = ''
|
||||
|
||||
# Bark 推送到ios设备
|
||||
# 参考 https://www.v2ex.com/t/467407
|
||||
BARK_ENABLED = 0
|
||||
BARK_PUSH_URL = 'https://api.day.app/:your_token'
|
||||
|
||||
# 输出日志到文件 (Docker 中不建议修改此组配置项)
|
||||
OUT_PUT_LOG_TO_FILE_ENABLED = 1
|
||||
OUT_PUT_LOG_TO_FILE_PATH = '/config/12306.log' # 日志目录
|
||||
RUNTIME_DIR = '/data/'
|
||||
QUERY_DATA_DIR = '/data/query/'
|
||||
USER_DATA_DIR = '/data/user/'
|
||||
|
||||
# 分布式集群配置
|
||||
CLUSTER_ENABLED = 0 # 集群状态
|
||||
NODE_IS_MASTER = 1 # 是否是主节点 同时只能启用 1 个主节点
|
||||
NODE_SLAVE_CAN_BE_MASTER = 1 # 主节点宕机后,子节点是否可以自动提升为主节点(建议打开)
|
||||
NODE_NAME = 'master' # 节点名称,不能重复
|
||||
REDIS_HOST = 'localhost' # Redis host
|
||||
REDIS_PORT = '6379' # Redis port
|
||||
REDIS_PASSWORD = '' # Redis 密码 没有可以留空
|
||||
|
||||
# 邮箱配置
|
||||
EMAIL_ENABLED = 0 # 是否开启邮件通知
|
||||
EMAIL_SENDER = 'sender@example.com' # 邮件发送者
|
||||
EMAIL_RECEIVER = 'receiver@example.com' # 邮件接受者 # 可以多个 [email1@gmail.com, email2@gmail.com]
|
||||
EMAIL_SERVER_HOST = 'localhost' # 邮件服务 host
|
||||
EMAIL_SERVER_USER = '' # 邮件服务登录用户名
|
||||
EMAIL_SERVER_PASSWORD = '' # 邮件服务登录密码
|
||||
|
||||
# Web 管理
|
||||
WEB_ENABLE = 1 # 是否打开 Web 管理
|
||||
WEB_USER = { # 登录信息
|
||||
'username': 'admin',
|
||||
'password': 'password'
|
||||
}
|
||||
WEB_PORT = 8008 # 监听端口
|
||||
|
||||
# 是否开启 CDN 查询
|
||||
CDN_ENABLED = 0
|
||||
CDN_CHECK_TIME_OUT = 1 # 检测单个 cdn 是否可用超时时间
|
||||
|
||||
# 查询任务
|
||||
QUERY_JOBS = [
|
||||
{
|
||||
# 'job_name': 'bj -> sz', # 任务名称,不填默认会以车站名命名,不可重复
|
||||
'account_key': 0, # 将会使用指定账号下单
|
||||
'left_dates': [ # 出发日期 :Array
|
||||
"2019-01-25",
|
||||
"2019-01-26",
|
||||
],
|
||||
'stations': { # 车站 支持多个车站同时查询 :Dict or :List
|
||||
'left': '北京',
|
||||
'arrive': '深圳',
|
||||
},
|
||||
# # 多个车站示例 (建议添加多个,有时多买几站成功率会高一点)
|
||||
# 'stations': [{
|
||||
# 'left': '北京',
|
||||
# 'arrive': '深圳',
|
||||
# },{ # 多个车站示例
|
||||
# 'left': '北京',
|
||||
# 'arrive': '广州',
|
||||
# }],
|
||||
'members': [ # 乘客姓名,会根据当前账号自动识别乘客类型 购买儿童票 设置两个相同的姓名即可,程序会自动识别 如 ['张三', '张三']
|
||||
"张三",
|
||||
"王五",
|
||||
# 7, # 支持通过序号确定唯一乘客,序号查看可通过 python main.py -t 登录成功之后在 runtime/user/ 下找到对应的 用户名_passengers.json 文件,找到对应的 code 填入
|
||||
],
|
||||
'allow_less_member': 0, # 是否允许余票不足时提交部分乘客
|
||||
'seats': [ # 筛选座位 有先后顺序 :Array
|
||||
# 可用值: 特等座, 商务座, 一等座, 二等座, 软卧, 硬卧, 动卧, 软座, 硬座, 无座
|
||||
'硬卧',
|
||||
'硬座'
|
||||
],
|
||||
'train_numbers': [ # 筛选车次 可以为空,为空则所有车次都可以提交 如 [] 注意大小写需要保持一致
|
||||
"K356",
|
||||
"K1172",
|
||||
"K4184"
|
||||
],
|
||||
'except_train_numbers': [ # 筛选车次,排除车次 train_numbers 和 except_train_numbers 不可同时存在
|
||||
],
|
||||
'period': { # 筛选时间
|
||||
'from': '00:00',
|
||||
'to': '24:00'
|
||||
}
|
||||
|
||||
},
|
||||
# {
|
||||
# 'job_name': 'cd -> gz', # 任务名称,不填默认会以车站名命名,不可重复
|
||||
# 'account_key': 0, # 将会使用指定账号下单
|
||||
# 'left_dates': [
|
||||
# "2019-01-27",
|
||||
# "2019-01-28"
|
||||
# ],
|
||||
# 'stations': {
|
||||
# 'left': '成都',
|
||||
# 'arrive': '广州',
|
||||
# },
|
||||
# 'members': [
|
||||
# "小王",
|
||||
# ],
|
||||
# 'allow_less_member': 0,
|
||||
# 'seats': [
|
||||
# '硬卧',
|
||||
# ],
|
||||
# 'train_numbers': []
|
||||
# }
|
||||
]
|
||||
@@ -28,8 +28,7 @@ QUERY_JOB_THREAD_ENABLED = 0 # 是否开启多线程查询,开启后第个任
|
||||
|
||||
# 打码平台账号
|
||||
# 目前只支持免费打码接口 和 若快打码,注册地址:http://www.ruokuai.com/login
|
||||
AUTO_CODE_PLATFORM = 'free' # 免费填写 free 若快 ruokuai # 免费打码无法保证持续可用,如失效请手动切换; 个人打码填写 user 并修改API_USER_CODE_QCR_API 为自己地址
|
||||
API_USER_CODE_QCR_API = ''
|
||||
AUTO_CODE_PLATFORM = 'free' # 免费填写 free 若快 ruokuai # 免费打码无法保证持续可用,如失效请手动切换
|
||||
AUTO_CODE_ACCOUNT = { # 使用 free 可用省略
|
||||
'user': 'your user name',
|
||||
'pwd': 'your password'
|
||||
@@ -110,19 +109,14 @@ WEB_PORT = 8008 # 监听端口
|
||||
CDN_ENABLED = 0
|
||||
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 = [
|
||||
{
|
||||
# 'job_name': 'bj -> sz', # 任务名称,不填默认会以车站名命名,不可重复
|
||||
'account_key': 0, # 将会使用指定账号下单
|
||||
'left_dates': [ # 出发日期 :Array
|
||||
"2020-01-25",
|
||||
"2020-01-26",
|
||||
"2019-01-25",
|
||||
"2019-01-26",
|
||||
],
|
||||
'stations': { # 车站 支持多个车站同时查询 :Dict or :List
|
||||
'left': '北京',
|
||||
@@ -138,7 +132,7 @@ QUERY_JOBS = [
|
||||
# }],
|
||||
'members': [ # 乘客姓名,会根据当前账号自动识别乘客类型 购买儿童票 设置两个相同的姓名即可,程序会自动识别 如 ['张三', '张三']
|
||||
"张三",
|
||||
#"*王五", #在姓名前加*表示学生购买成人票
|
||||
"王五",
|
||||
# 7, # 支持通过序号确定唯一乘客,序号查看可通过 python main.py -t 登录成功之后在 runtime/user/ 下找到对应的 用户名_passengers.json 文件,找到对应的 code 填入
|
||||
],
|
||||
'allow_less_member': 0, # 是否允许余票不足时提交部分乘客
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 分布式子节点配置文件示例
|
||||
|
||||
# 分布式集群配置
|
||||
CLUSTER_ENABLED = 1 # 集群状态
|
||||
NODE_IS_MASTER = 0 # 是否是主节点
|
||||
NODE_NAME = 'slave 1' # 节点名称,不能重复
|
||||
REDIS_HOST = 'localhost' # Redis host
|
||||
REDIS_PORT = '6379' # Redis port
|
||||
REDIS_PASSWORD = '' # Redis 密码 没有可以留空
|
||||
|
||||
# 没了,其它配置会自动从主节点同步
|
||||
61
main.py
@@ -1,63 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
|
||||
from py12306.app import *
|
||||
from py12306.helpers.cdn import Cdn
|
||||
from py12306.log.common_log import CommonLog
|
||||
from py12306.query.query import Query
|
||||
from py12306.user.user import User
|
||||
from py12306.web.web import Web
|
||||
|
||||
|
||||
def main():
|
||||
load_argvs()
|
||||
CommonLog.print_welcome()
|
||||
App.run()
|
||||
CommonLog.print_configs()
|
||||
App.did_start()
|
||||
|
||||
App.run_check()
|
||||
Query.check_before_run()
|
||||
|
||||
####### 运行任务
|
||||
Web.run()
|
||||
Cdn.run()
|
||||
User.run()
|
||||
Query.run()
|
||||
if not Const.IS_TEST:
|
||||
while True:
|
||||
sleep(10000)
|
||||
else:
|
||||
if Config().is_cluster_enabled(): stay_second(5) # 等待接受完通知
|
||||
CommonLog.print_test_complete()
|
||||
|
||||
|
||||
def test():
|
||||
"""
|
||||
功能检查
|
||||
包含:
|
||||
账号密码验证 (打码)
|
||||
座位验证
|
||||
乘客验证
|
||||
语音验证码验证
|
||||
通知验证
|
||||
:return:
|
||||
"""
|
||||
Const.IS_TEST = True
|
||||
Config.OUT_PUT_LOG_TO_FILE_ENABLED = False
|
||||
if '--test-notification' in sys.argv or '-n' in sys.argv:
|
||||
Const.IS_TEST_NOTIFICATION = True
|
||||
pass
|
||||
|
||||
|
||||
def load_argvs():
|
||||
if '--test' in sys.argv or '-t' in sys.argv: test()
|
||||
config_index = None
|
||||
|
||||
if '--config' in sys.argv: config_index = sys.argv.index('--config')
|
||||
if '-c' in sys.argv: config_index = sys.argv.index('-c')
|
||||
if config_index:
|
||||
Config.CONFIG_FILE = sys.argv[config_index + 1:config_index + 2].pop()
|
||||
# def load_argvs():
|
||||
# if '--test' in sys.argv or '-t' in sys.argv: test()
|
||||
# config_index = None
|
||||
#
|
||||
# if '--config' in sys.argv: config_index = sys.argv.index('--config')
|
||||
# if '-c' in sys.argv: config_index = sys.argv.index('-c')
|
||||
# if config_index:
|
||||
# Config.CONFIG_FILE = sys.argv[config_index + 1:config_index + 2].pop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -15,7 +15,7 @@ def app_available_check():
|
||||
if Config().IS_DEBUG:
|
||||
return True
|
||||
now = time_now()
|
||||
if (now.hour >= 23 and now.minute >= 30) or now.hour < 6:
|
||||
if now.hour >= 23 or now.hour < 6:
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_12306_IS_CLOSED.format(time_now())).flush()
|
||||
open_time = datetime.datetime(now.year, now.month, now.day, 6)
|
||||
if open_time < now:
|
||||
@@ -81,7 +81,7 @@ class App:
|
||||
|
||||
@classmethod
|
||||
def check_auto_code(cls):
|
||||
if Config().AUTO_CODE_PLATFORM == 'free' or Config().AUTO_CODE_PLATFORM == 'user': return True
|
||||
if Config().AUTO_CODE_PLATFORM == 'free': return True
|
||||
if not Config().AUTO_CODE_ACCOUNT.get('user') or not Config().AUTO_CODE_ACCOUNT.get('pwd'):
|
||||
return False
|
||||
return True
|
||||
@@ -22,8 +22,6 @@ class Config:
|
||||
QUERY_JOB_THREAD_ENABLED = 0
|
||||
# 打码平台账号
|
||||
AUTO_CODE_PLATFORM = ''
|
||||
#用户打码平台地址
|
||||
API_USER_CODE_QCR_API = ''
|
||||
AUTO_CODE_ACCOUNT = {'user': '', 'pwd': ''}
|
||||
# 输出日志到文件
|
||||
OUT_PUT_LOG_TO_FILE_ENABLED = 0
|
||||
@@ -96,10 +94,6 @@ class Config:
|
||||
CDN_ITEM_FILE = PROJECT_DIR + 'data/cdn.txt'
|
||||
CDN_ENABLED_AVAILABLE_ITEM_FILE = QUERY_DATA_DIR + 'available.json'
|
||||
|
||||
CACHE_RAIL_ID_ENABLED = 0
|
||||
RAIL_EXPIRATION = ''
|
||||
RAIL_DEVICEID = ''
|
||||
|
||||
# Default time out
|
||||
TIME_OUT_OF_REQUEST = 5
|
||||
|
||||
@@ -219,10 +213,6 @@ class Config:
|
||||
def is_cdn_enabled():
|
||||
return Config().CDN_ENABLED
|
||||
|
||||
@staticmethod
|
||||
def is_cache_rail_id_enabled():
|
||||
return Config().CACHE_RAIL_ID_ENABLED
|
||||
|
||||
|
||||
class EnvLoader:
|
||||
envs = []
|
||||
@@ -2,7 +2,7 @@ import math
|
||||
import random
|
||||
|
||||
from py12306.config import Config
|
||||
from py12306.helpers.api import API_FREE_CODE_QCR_API
|
||||
from py12306.helpers.api import *
|
||||
from py12306.helpers.request import Request
|
||||
from py12306.log.common_log import CommonLog
|
||||
from py12306.vender.ruokuai.main import RKClient
|
||||
@@ -25,7 +25,7 @@ class OCR:
|
||||
:return:
|
||||
"""
|
||||
self = cls()
|
||||
if Config().AUTO_CODE_PLATFORM == 'free' or Config().AUTO_CODE_PLATFORM == 'user':
|
||||
if Config().AUTO_CODE_PLATFORM == 'free':
|
||||
return self.get_image_by_free_site(img)
|
||||
return self.get_img_position_by_ruokuai(img)
|
||||
|
||||
@@ -56,16 +56,22 @@ class OCR:
|
||||
|
||||
def get_image_by_free_site(self, img):
|
||||
data = {
|
||||
'img': img
|
||||
'base64': img
|
||||
}
|
||||
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)
|
||||
response = self.session.post(API_FREE_CODE_QCR_API, json=data)
|
||||
result = response.json()
|
||||
if result.get('msg') == 'success':
|
||||
pos = result.get('result')
|
||||
return self.get_image_position_by_offset(pos)
|
||||
if result.get('success') and result.get('data.check'):
|
||||
check_data = {
|
||||
'check': result.get('data.check'),
|
||||
'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)
|
||||
return None
|
||||
@@ -40,11 +40,14 @@ API_GET_QUEUE_COUNT = BASE_URL_OF_12306 + '/otn/confirmPassenger/getQueueCount'
|
||||
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_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_GET_BROWSER_DEVICE_ID = BASE_URL_OF_12306 + '/otn/HttpZF/logdevice'
|
||||
|
||||
|
||||
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_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'
|
||||
@@ -33,7 +33,6 @@ class AuthCode:
|
||||
return self.retry_get_auth_code()
|
||||
|
||||
answer = ','.join(map(str, position))
|
||||
|
||||
if not self.check_code(answer):
|
||||
return self.retry_get_auth_code()
|
||||
return position
|
||||
@@ -47,7 +46,6 @@ class AuthCode:
|
||||
url = API_AUTH_CODE_BASE64_DOWNLOAD.format(random=random.random())
|
||||
# code_path = self.data_path + 'code.png'
|
||||
try:
|
||||
self.session.cookies.clear_session_cookies()
|
||||
UserLog.add_quick_log(UserLog.MESSAGE_DOWNLAODING_THE_CODE).flush()
|
||||
# response = self.session.save_to_file(url, code_path) # TODO 返回错误情况
|
||||
response = self.session.get(url)
|
||||
@@ -125,9 +125,9 @@ class Notification():
|
||||
message.set_content(content)
|
||||
try:
|
||||
server = smtplib.SMTP(Config().EMAIL_SERVER_HOST)
|
||||
server.login(Config().EMAIL_SERVER_USER, Config().EMAIL_SERVER_PASSWORD)
|
||||
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_SUCCESS).flush()
|
||||
@@ -77,18 +77,3 @@ class Request(HTMLSession):
|
||||
url = url.replace(HOST_URL_OF_12306, cdn)
|
||||
|
||||
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
|
||||
@@ -139,10 +139,3 @@ class CommonLog(BaseLog):
|
||||
self.add_quick_log('打码失败: 错误原因 {reason}'.format(reason=reason))
|
||||
self.flush()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def print_auth_code_info(cls, reason):
|
||||
self = cls()
|
||||
self.add_quick_log('打码信息: {reason}'.format(reason=reason))
|
||||
self.flush()
|
||||
return self
|
||||
@@ -1,9 +1,6 @@
|
||||
import asyncio
|
||||
import urllib
|
||||
|
||||
# from py12306.config import UserType
|
||||
from pyppeteer import launch
|
||||
|
||||
from py12306.config import Config
|
||||
from py12306.helpers.api import *
|
||||
from py12306.helpers.func import *
|
||||
@@ -13,73 +10,6 @@ from py12306.log.common_log import CommonLog
|
||||
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:
|
||||
"""
|
||||
处理下单
|
||||
@@ -111,7 +41,6 @@ class Order:
|
||||
assert isinstance(user, UserJob)
|
||||
self.query_ins = query
|
||||
self.user_ins = user
|
||||
self.is_slide = False
|
||||
|
||||
self.make_passenger_ticket_str()
|
||||
|
||||
@@ -134,25 +63,10 @@ class Order:
|
||||
return self.order_did_success()
|
||||
elif not order_request_res:
|
||||
return
|
||||
init_res, self.is_slide, init_html = self.user_ins.request_init_dc_page()
|
||||
if not init_res:
|
||||
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
|
||||
if not self.user_ins.request_init_dc_page(): return
|
||||
if not self.check_order_info(): return
|
||||
if not self.get_queue_count(): return
|
||||
if not self.confirm_single_for_queue(): return
|
||||
order_id = self.query_order_wait_time()
|
||||
if order_id: # 发送通知
|
||||
self.order_id = order_id
|
||||
@@ -171,8 +85,7 @@ class Order:
|
||||
# num = 0 # 通知次数
|
||||
# sustain_time = self.notification_sustain_time
|
||||
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: # 邮件通知
|
||||
Notification.send_email(Config().EMAIL_RECEIVER, OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_TITLE,
|
||||
normal_message + info_message)
|
||||
@@ -187,7 +100,7 @@ class Order:
|
||||
Notification.push_bear(Config().PUSHBEAR_KEY, OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_TITLE,
|
||||
normal_message + info_message)
|
||||
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_VOICE_CODE_TYPE == 'dingxin':
|
||||
@@ -239,7 +152,7 @@ class Order:
|
||||
result.get('messages', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR))).flush()
|
||||
return False
|
||||
|
||||
def check_order_info(self, slide_info=None):
|
||||
def check_order_info(self):
|
||||
"""
|
||||
cancel_flag=2
|
||||
bed_level_order_num=000000000000000000000000000000
|
||||
@@ -262,12 +175,6 @@ class Order:
|
||||
'_json_att': '',
|
||||
'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)
|
||||
result = response.json()
|
||||
if result.get('data.submitStatus'): # 成功
|
||||
@@ -344,7 +251,7 @@ class Order:
|
||||
if ticket_number != '充足' and int(ticket_number) <= 0:
|
||||
if self.query_ins.current_seat == SeatType.NO_SEAT: # 允许无座
|
||||
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()
|
||||
return False
|
||||
|
||||
@@ -467,12 +374,10 @@ class Order:
|
||||
elif 'waitTime' in result_data:
|
||||
# 计算等待时间
|
||||
wait_time = int(result_data.get('waitTime'))
|
||||
if wait_time == -1: # 成功
|
||||
if wait_time == -1 or wait_time == -100: # 成功
|
||||
# /otn/confirmPassenger/resultOrderForDcQueue 请求订单状态 目前不需要
|
||||
# 不应该走到这
|
||||
return order_id
|
||||
elif wait_time == -100: # 重新获取订单号
|
||||
pass
|
||||
elif wait_time >= 0: # 等待
|
||||
OrderLog.add_quick_log(
|
||||
OrderLog.MESSAGE_QUERY_ORDER_WAIT_TIME_WAITING.format(result_data.get('waitCount', 0),
|
||||
@@ -524,12 +429,11 @@ class Order:
|
||||
OrderLog.print_passenger_did_deleted(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,{enc_str}_'.format(
|
||||
tmp_str = '{seat_type},0,{passenger_type},{passenger_name},{passenger_id_card_type},{passenger_id_card},{passenger_mobile},N_'.format(
|
||||
seat_type=self.query_ins.current_order_seat, passenger_type=passenger['type'],
|
||||
passenger_name=passenger['name'],
|
||||
passenger_id_card_type=passenger['id_card_type'], passenger_id_card=passenger['id_card'],
|
||||
passenger_mobile=passenger['mobile'],
|
||||
enc_str=passenger['enc_str'],
|
||||
passenger_mobile=passenger['mobile']
|
||||
)
|
||||
passenger_tickets.append(tmp_str)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
|
||||
from py12306.app import app_available_check
|
||||
from py12306.cluster.cluster import Cluster
|
||||
@@ -67,8 +66,6 @@ class Job:
|
||||
INDEX_LEFT_TIME = 8
|
||||
INDEX_ARRIVE_TIME = 9
|
||||
|
||||
max_buy_time = 32
|
||||
|
||||
def __init__(self, info, query):
|
||||
self.cluster = Cluster()
|
||||
self.query = query
|
||||
@@ -139,29 +136,11 @@ class Job:
|
||||
QueryLog.add_log('\n').flush(sep='\t\t', publish=False)
|
||||
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:
|
||||
pass
|
||||
|
||||
def query_by_date(self, date):
|
||||
"""
|
||||
通过日期进行查询
|
||||
:return:
|
||||
"""
|
||||
self.judge_date_legal(date)
|
||||
from py12306.helpers.cdn import Cdn
|
||||
QueryLog.add_log(('\n' if not is_main_thread() else '') + QueryLog.MESSAGE_QUERY_START_BY_DATE.format(date,
|
||||
self.left_station,
|
||||
@@ -154,9 +154,6 @@ class Query:
|
||||
self.api_type = res.group(1)
|
||||
except IndexError:
|
||||
pass
|
||||
if not self.api_type:
|
||||
QueryLog.add_quick_log('查询地址获取失败, 正在重新获取...').flush()
|
||||
sleep(1)
|
||||
return cls.get_query_api_type()
|
||||
|
||||
# def get_jobs_from_cluster(self):
|
||||
@@ -123,9 +123,9 @@ class UserJob:
|
||||
'password': self.password,
|
||||
'appid': 'otn'
|
||||
}
|
||||
self.request_device_id()
|
||||
answer = AuthCode.get_auth_code(self.session)
|
||||
data['answer'] = answer
|
||||
self.request_device_id()
|
||||
response = self.session.post(API_BASE_LOGIN.get('url'), data)
|
||||
result = response.json()
|
||||
if result.get('result_code') == 0: # 登录成功
|
||||
@@ -161,10 +161,7 @@ class UserJob:
|
||||
return is_login
|
||||
|
||||
def auth_uamtk(self):
|
||||
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'
|
||||
})
|
||||
response = self.session.post(API_AUTH_UAMTK.get('url'), {'appid': 'otn'})
|
||||
result = response.json()
|
||||
if result.get('newapptk'):
|
||||
return result.get('newapptk')
|
||||
@@ -184,32 +181,137 @@ class UserJob:
|
||||
获取加密后的浏览器特征 ID
|
||||
:return:
|
||||
"""
|
||||
response = self.session.get(API_GET_BROWSER_DEVICE_ID)
|
||||
if response.status_code == 200:
|
||||
params = {"algID": self.request_alg_id(), "timestamp": int(time.time() * 1000)}
|
||||
params = dict(params, **self._get_hash_code_params())
|
||||
response = self.session.get(API_GET_BROWSER_DEVICE_ID, params=params)
|
||||
if response.text.find('callbackFunction') >= 0:
|
||||
result = response.text[18:-2]
|
||||
try:
|
||||
result = json.loads(response.text)
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
|
||||
}
|
||||
from base64 import b64decode
|
||||
self.session.headers.update(headers)
|
||||
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,
|
||||
})
|
||||
self.session.cookies.update({
|
||||
'RAIL_EXPIRATION': result.get('exp'),
|
||||
'RAIL_DEVICEID': result.get('dfp'),
|
||||
})
|
||||
except:
|
||||
return False
|
||||
|
||||
def request_alg_id(self):
|
||||
response = self.session.get("https://kyfw.12306.cn/otn/HttpZF/GetJS")
|
||||
result = re.search(r'algID\\x3d(.*?)\\x26', response.text)
|
||||
try:
|
||||
return result.group(1)
|
||||
except (IndexError, AttributeError) as e:
|
||||
pass
|
||||
return ""
|
||||
|
||||
def _get_hash_code_params(self):
|
||||
from collections import OrderedDict
|
||||
data = {
|
||||
'adblock': '0',
|
||||
'browserLanguage': 'en-US',
|
||||
'cookieEnabled': '1',
|
||||
'custID': '133',
|
||||
'doNotTrack': 'unknown',
|
||||
'flashVersion': '0',
|
||||
'javaEnabled': '0',
|
||||
'jsFonts': 'c227b88b01f5c513710d4b9f16a5ce52',
|
||||
'localCode': '3232236206',
|
||||
'mimeTypes': '52d67b2a5aa5e031084733d5006cc664',
|
||||
'os': 'MacIntel',
|
||||
'platform': 'WEB',
|
||||
'plugins': 'd22ca0b81584fbea62237b14bd04c866',
|
||||
'scrAvailSize': str(random.randint(500, 1000)) + 'x1920',
|
||||
'srcScreenSize': '24xx1080x1920',
|
||||
'storeDb': 'i1l1o1s1',
|
||||
'timeZone': '-8',
|
||||
'touchSupport': '99115dfb07133750ba677d055874de87',
|
||||
'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.' + str(
|
||||
random.randint(
|
||||
5000, 7000)) + '.0 Safari/537.36',
|
||||
'webSmartID': 'f4e3b7b14cc647e30a6267028ad54c56',
|
||||
}
|
||||
data_trans = {
|
||||
'browserVersion': 'd435',
|
||||
'touchSupport': 'wNLf',
|
||||
'systemLanguage': 'e6OK',
|
||||
'scrWidth': 'ssI5',
|
||||
'openDatabase': 'V8vl',
|
||||
'scrAvailSize': 'TeRS',
|
||||
'hasLiedResolution': '3neK',
|
||||
'hasLiedOs': 'ci5c',
|
||||
'timeZone': 'q5aJ',
|
||||
'userAgent': '0aew',
|
||||
'userLanguage': 'hLzX',
|
||||
'jsFonts': 'EOQP',
|
||||
'scrAvailHeight': '88tV',
|
||||
'browserName': '-UVA',
|
||||
'cookieCode': 'VySQ',
|
||||
'online': '9vyE',
|
||||
'scrAvailWidth': 'E-lJ',
|
||||
'flashVersion': 'dzuS',
|
||||
'scrDeviceXDPI': '3jCe',
|
||||
'srcScreenSize': 'tOHY',
|
||||
'storeDb': 'Fvje',
|
||||
'doNotTrack': 'VEek',
|
||||
'mimeTypes': 'jp76',
|
||||
'sessionStorage': 'HVia',
|
||||
'cookieEnabled': 'VPIf',
|
||||
'os': 'hAqN',
|
||||
'hasLiedLanguages': 'j5po',
|
||||
'hasLiedBrowser': '2xC5',
|
||||
'webSmartID': 'E3gR',
|
||||
'appcodeName': 'qT7b',
|
||||
'javaEnabled': 'yD16',
|
||||
'plugins': 'ks0Q',
|
||||
'appMinorVersion': 'qBVW',
|
||||
'cpuClass': 'Md7A',
|
||||
'indexedDb': '3sw-',
|
||||
'adblock': 'FMQw',
|
||||
'localCode': 'lEnu',
|
||||
'browserLanguage': 'q4f3',
|
||||
'scrHeight': '5Jwy',
|
||||
'localStorage': 'XM7l',
|
||||
'historyList': 'kU5z',
|
||||
'scrColorDepth': "qmyu"
|
||||
}
|
||||
data = OrderedDict(data)
|
||||
data_str = ''
|
||||
params = {}
|
||||
for key, item in data.items():
|
||||
data_str += key + item
|
||||
key = data_trans[key] if key in data_trans else key
|
||||
params[key] = item
|
||||
data_str = self._encode_data_str(data_str)
|
||||
data_str_len = len(data_str)
|
||||
data_str_f = int(data_str_len / 3) if data_str_len % 3 == 0 else int(data_str_len / 3) + 1
|
||||
if data_str_len >= 3:
|
||||
data_str = data_str[data_str_f:2*data_str_f] + data_str[2*data_str_f:data_str_len] + data_str[0: data_str_f]
|
||||
data_str = data_str[::-1]
|
||||
data_str_tmp = ""
|
||||
for e in range(0, len(data_str)):
|
||||
data_str_code = ord(data_str[e])
|
||||
data_str_tmp += chr(0) if data_str_code == 127 else chr(data_str_code + 1)
|
||||
|
||||
data_str = self._encode_data_str(data_str_tmp)
|
||||
data_str = self._encode_string(data_str)
|
||||
params['hashCode'] = data_str
|
||||
return params
|
||||
|
||||
def _encode_data_str(self, data_str):
|
||||
data_str_len = len(data_str)
|
||||
data_str_len_tmp = int(data_str_len / 3) if data_str_len % 3 == 0 else int(data_str_len / 3) + 1
|
||||
if data_str_len >= 3:
|
||||
data_str_e = data_str[0:data_str_len_tmp]
|
||||
data_str_f = data_str[data_str_len_tmp:2 * data_str_len_tmp]
|
||||
return data_str[2 * data_str_len_tmp:data_str_len] + data_str_e + data_str_f
|
||||
return data_str
|
||||
|
||||
def _encode_string(self, str):
|
||||
import hashlib
|
||||
import base64
|
||||
result = base64.b64encode(hashlib.sha256(str.encode()).digest()).decode()
|
||||
return result.replace('+', '-').replace('/', '_').replace('=', '')
|
||||
|
||||
def login_did_success(self):
|
||||
"""
|
||||
用户登录成功
|
||||
@@ -360,8 +462,7 @@ class UserJob:
|
||||
name: '项羽',
|
||||
type: 1,
|
||||
id_card: 0000000000000000000,
|
||||
type_text: '成人',
|
||||
enc_str: 'aaaaaa'
|
||||
type_text: '成人'
|
||||
}]
|
||||
"""
|
||||
self.get_user_passengers()
|
||||
@@ -369,11 +470,6 @@ class UserJob:
|
||||
for member in members:
|
||||
is_member_code = is_number(member)
|
||||
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)
|
||||
if not is_member_code and child_check:
|
||||
new_member = child_check.copy()
|
||||
@@ -384,8 +480,6 @@ class UserJob:
|
||||
passenger = array_dict_find_by_key_value(self.passengers, 'code', member)
|
||||
else:
|
||||
passenger = array_dict_find_by_key_value(self.passengers, 'passenger_name', member)
|
||||
if audlt:
|
||||
passenger['passenger_type'] = UserType.ADULT
|
||||
if not passenger:
|
||||
UserLog.add_quick_log(
|
||||
UserLog.MESSAGE_USER_PASSENGERS_IS_INVALID.format(self.user_name, member)).flush()
|
||||
@@ -396,8 +490,7 @@ class UserJob:
|
||||
'id_card_type': passenger.get('passenger_id_type_code'),
|
||||
'mobile': passenger.get('mobile_no'),
|
||||
'type': passenger.get('passenger_type'),
|
||||
'type_text': dict_find_key_by_value(UserType.dicts, int(passenger.get('passenger_type'))),
|
||||
'enc_str': passenger.get('allEncStr')
|
||||
'type_text': dict_find_key_by_value(UserType.dicts, int(passenger.get('passenger_type')))
|
||||
}
|
||||
results.append(new_member)
|
||||
|
||||
@@ -417,16 +510,12 @@ class UserJob:
|
||||
# 系统忙,请稍后重试
|
||||
if html.find('系统忙,请稍后重试') != -1:
|
||||
OrderLog.add_quick_log(OrderLog.MESSAGE_REQUEST_INIT_DC_PAGE_FAIL).flush() # 重试无用,直接跳过
|
||||
return False, False, html
|
||||
return False
|
||||
try:
|
||||
self.global_repeat_submit_token = token.groups()[0]
|
||||
self.ticket_info_for_passenger_form = json.loads(form.groups()[0].replace("'", '"'))
|
||||
self.order_request_dto = json.loads(order.groups()[0].replace("'", '"'))
|
||||
except:
|
||||
return False, False, html # TODO Error
|
||||
pass # TODO Error
|
||||
|
||||
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
|
||||
return True
|
||||
0
old_py12306/web/handler/__init__.py
Normal file
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 729 KiB After Width: | Height: | Size: 729 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 775 KiB After Width: | Height: | Size: 775 KiB |
0
py12306/app/__init__.py
Normal file
61
py12306/app/app.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
||||
class Config:
|
||||
class AppEnvType:
|
||||
DEV = 'dev'
|
||||
PRODUCTION = 'production'
|
||||
|
||||
APP_NAME = 'py12306'
|
||||
APP_ENV = AppEnvType.PRODUCTION
|
||||
LOADED = False
|
||||
TEST_MODE = False
|
||||
|
||||
PROJECT_DIR = os.path.abspath(__file__ + '/../../../') + '/'
|
||||
CONFIG_FILE = PROJECT_DIR + 'config.toml'
|
||||
|
||||
REQUEST_TIME_OUT = 5
|
||||
|
||||
# Config
|
||||
REDIS = {
|
||||
'host': '127.0.0.1',
|
||||
'port': 6379,
|
||||
'db': 0,
|
||||
'password': None,
|
||||
'decode_responses': True
|
||||
}
|
||||
|
||||
# Redis keys
|
||||
REDIS_PREFIX_KEY_TASKS = APP_NAME + ':tasks:'
|
||||
|
||||
# REDIS_KEY_USER_TASKS = 'user_jobs'
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
"""
|
||||
Load configs from toml file
|
||||
:return:
|
||||
"""
|
||||
import toml
|
||||
configs = toml.load(cls.CONFIG_FILE)
|
||||
|
||||
redis = configs.get('redis')
|
||||
if redis:
|
||||
cls.REDIS.update(redis)
|
||||
|
||||
app = configs.get('app')
|
||||
if app:
|
||||
cls.APP_ENV = app.get('env', cls.APP_ENV)
|
||||
|
||||
|
||||
if not Config.LOADED:
|
||||
Config.load()
|
||||
|
||||
# Logger
|
||||
Logger = logging.getLogger(Config.APP_NAME)
|
||||
Logger.setLevel('DEBUG' if Config.APP_ENV == Config.AppEnvType.DEV else 'ERROR')
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
Logger.addHandler(handler)
|
||||
201
py12306/app/query.py
Normal file
@@ -0,0 +1,201 @@
|
||||
import re
|
||||
from typing import List
|
||||
|
||||
from py12306.app.app import Logger
|
||||
from py12306.lib.api import API_QUERY_INIT_PAGE, API_LEFT_TICKETS
|
||||
from py12306.lib.exceptions import RetryException
|
||||
from py12306.lib.func import retry, number_of_time_period
|
||||
from py12306.lib.helper import DataHelper, TrainSeat
|
||||
from py12306.lib.request import Request
|
||||
|
||||
|
||||
class TicketSeatData(DataHelper):
|
||||
name: str
|
||||
num: str
|
||||
raw: str
|
||||
|
||||
|
||||
class QueryTicketData(DataHelper):
|
||||
left_date: str
|
||||
left_station: str
|
||||
arrive_station: str
|
||||
left_periods: List[tuple] = []
|
||||
allow_train_numbers: List[str] = []
|
||||
execpt_train_numbers: List[str] = []
|
||||
allow_seats: List[str] = []
|
||||
available_seat: TicketSeatData
|
||||
members: list
|
||||
member_num: int
|
||||
less_member: bool = False
|
||||
|
||||
def _after(self):
|
||||
self.member_num = len(self.members)
|
||||
|
||||
|
||||
class TicketData(DataHelper):
|
||||
left_date: str = 'key:13'
|
||||
ticket_num: str = 'key:11'
|
||||
train_number: str = 'key:3'
|
||||
train_no: str = 'key:2'
|
||||
train_no: str = 'key:2'
|
||||
left_station: str = 'key:6'
|
||||
arrive_station: str = 'key:7'
|
||||
order_text: str = 'key:1'
|
||||
secret_str: str = 'key:0'
|
||||
left_time: str = 'key:8'
|
||||
arrive_time: str = 'key:9'
|
||||
|
||||
|
||||
class Query:
|
||||
@classmethod
|
||||
def task_train_ticket(cls, task: dict):
|
||||
QueryTicket().query_with_info(task)
|
||||
|
||||
|
||||
class QueryTicket:
|
||||
"""
|
||||
车票查询
|
||||
"""
|
||||
api_type: str = None
|
||||
time_out: int = 5
|
||||
session: Request
|
||||
|
||||
def __init__(self):
|
||||
self.session = Request()
|
||||
|
||||
def query_with_info(self, info: dict):
|
||||
pass
|
||||
|
||||
@retry()
|
||||
def get_query_api_type(self) -> str:
|
||||
"""
|
||||
动态获取查询的接口, 如 leftTicket/query
|
||||
:return:
|
||||
"""
|
||||
if QueryTicket.api_type:
|
||||
return QueryTicket.api_type
|
||||
response = self.session.get(API_QUERY_INIT_PAGE)
|
||||
if response.status_code == 200:
|
||||
res = re.search(r'var CLeftTicketUrl = \'(.*)\';', response.text)
|
||||
try:
|
||||
QueryTicket.api_type = res.group(1)
|
||||
except IndexError:
|
||||
raise RetryException('获取车票查询地址失败')
|
||||
return self.get_query_api_type()
|
||||
|
||||
@retry()
|
||||
def get_ticket(self, data: dict):
|
||||
data = QueryTicketData(data)
|
||||
url = API_LEFT_TICKETS.format(left_date=data.left_date, left_station=data.left_station,
|
||||
arrive_station=data.arrive_station, type=self.get_query_api_type())
|
||||
resp = self.session.get(url, timeout=self.time_out, allow_redirects=False)
|
||||
result = resp.json().get('data.result')
|
||||
if not result:
|
||||
Logger.error('车票查询失败, %s' % resp.reason)
|
||||
tickets = QueryParser.parse_ticket(result)
|
||||
for ticket in tickets:
|
||||
self.is_ticket_valid(ticket, data)
|
||||
if not data:
|
||||
continue
|
||||
# 验证完成,准备下单
|
||||
Logger.info('[ 查询到座位可用 出发时间 {left_date} 车次 {train_number} 座位类型 {seat_type} 余票数量 {rest_num} ]'.format(
|
||||
left_date=data.left_date, train_number=ticket.train_number, seat_type=data.available_seat.name,
|
||||
rest_num=data.available_seat.raw))
|
||||
|
||||
def is_ticket_valid(self, ticket: TicketData, query: QueryTicketData) -> bool:
|
||||
"""
|
||||
验证 Ticket 信息是否可用
|
||||
) 出发日期验证
|
||||
) 车票数量验证
|
||||
) 时间点验证(00:00 - 24:00)
|
||||
) 车次验证
|
||||
) 座位验证
|
||||
) 乘车人数验证
|
||||
:param ticket: 车票信息
|
||||
:param query: 查询条件
|
||||
:return:
|
||||
"""
|
||||
if not self.verify_ticket_num(ticket):
|
||||
return False
|
||||
|
||||
if not self.verify_period(ticket.left_time, query.left_periods):
|
||||
return False
|
||||
|
||||
if query.allow_train_numbers and ticket.train_no.upper() not in map(str.upper, query.allow_train_numbers):
|
||||
return False
|
||||
|
||||
if query.execpt_train_numbers and ticket.train_no.upper() in map(str.upper, query.execpt_train_numbers):
|
||||
return False
|
||||
|
||||
if not self.verify_seat(ticket, query):
|
||||
return False
|
||||
if not self.verify_member_count(query):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def verify_period(period: str, available_periods: List[tuple]):
|
||||
if not available_periods:
|
||||
return True
|
||||
period = number_of_time_period(period)
|
||||
for available_period in available_periods:
|
||||
if period < number_of_time_period(available_period[0]) or period > number_of_time_period(
|
||||
available_period[1]):
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def verify_ticket_num(ticket: TicketData):
|
||||
return ticket.ticket_num == 'Y' and ticket.order_text == '预订'
|
||||
|
||||
def verify_seat(self, ticket: TicketData, query: QueryTicketData) -> bool:
|
||||
"""
|
||||
检查座位是否可用
|
||||
TODO 小黑屋判断 通过 车次 + 座位
|
||||
:param ticket:
|
||||
:param query:
|
||||
:return:
|
||||
"""
|
||||
allow_seats = query.allow_seats
|
||||
for seat in allow_seats:
|
||||
seat_num = TrainSeat.types[seat]
|
||||
raw = ticket.get_origin()[seat_num]
|
||||
if self.verify_seat_text(raw):
|
||||
query.available_seat = TicketSeatData({
|
||||
'name': seat,
|
||||
'num': seat_num,
|
||||
'raw': raw
|
||||
})
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def verify_seat_text(seat: str) -> bool:
|
||||
return seat != '' and seat != '无' and seat != '*'
|
||||
|
||||
@staticmethod
|
||||
def verify_member_count(query: QueryTicketData) -> bool:
|
||||
seat = query.available_seat
|
||||
if not (seat.raw == '有' or query.member_num <= int(seat.raw)):
|
||||
rest_num = int(seat.raw)
|
||||
if query.less_member:
|
||||
query.member_num = rest_num
|
||||
Logger.info(
|
||||
'余票数小于乘车人数,当前余票数: %d, 实际人数 %d, 删减人车人数到: %d' % (rest_num, query.member_num, query.member_num))
|
||||
else:
|
||||
Logger.info('余票数 %d 小于乘车人数 %d,放弃此次提交机会' % (rest_num, query.member_num))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class QueryParser:
|
||||
@classmethod
|
||||
def parse_ticket(cls, items: List[dict]) -> List[TicketData]:
|
||||
res = []
|
||||
for item in items:
|
||||
info = item.split('|')
|
||||
info = {i: info[i] for i in range(0, len(info))} # conver to dict
|
||||
res.append(TicketData(info))
|
||||
|
||||
return res
|
||||
35
py12306/app/task.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from py12306.app.app import Logger, Config
|
||||
from py12306.lib.func import new_thread_with_jobs
|
||||
from py12306.lib.redis_lib import Redis
|
||||
|
||||
|
||||
def get_routes() -> dict:
|
||||
from py12306.app.user import User
|
||||
from py12306.app.query import Query
|
||||
return {
|
||||
'user_login': User.task_user_login,
|
||||
'train_ticket': Query.task_train_ticket,
|
||||
}
|
||||
|
||||
|
||||
class Task:
|
||||
routes: dict = None
|
||||
|
||||
@classmethod
|
||||
def listen(cls):
|
||||
routes = get_routes()
|
||||
keys = [Config.REDIS_PREFIX_KEY_TASKS + key for key, _ in routes.items()]
|
||||
while True:
|
||||
key, job = Redis.share().get_task_sync(keys)
|
||||
Logger.info('获得新任务 %s' % key)
|
||||
if Config.TEST_MODE: # ignore when in test env
|
||||
return job
|
||||
self = cls()
|
||||
self.routes = routes
|
||||
self.deal_job(key, job)
|
||||
|
||||
def deal_job(self, key: str, task: dict):
|
||||
handler = self.routes.get(key)
|
||||
if not handler:
|
||||
return
|
||||
new_thread_with_jobs(handler, wait=True, kwargs={'task': task})
|
||||
11
py12306/app/user.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from py12306.lib.helper import ShareInstance
|
||||
|
||||
|
||||
class User(ShareInstance):
|
||||
|
||||
@classmethod
|
||||
def task_user_login(cls, task: dict):
|
||||
pass
|
||||
|
||||
|
||||
pass
|
||||
0
py12306/lib/__init__.py
Normal file
11
py12306/lib/api.py
Normal file
@@ -0,0 +1,11 @@
|
||||
HOST_API = 'kyfw.12306.cn'
|
||||
BASE_API = 'https://' + HOST_API
|
||||
|
||||
# LEFT_TICKETS = {
|
||||
# "url": BASE_URL_OF_12306 + "/otn/{type}?leftTicketDTO.train_date={left_date}&leftTicketDTO.from_station={left_station}&leftTicketDTO.to_station={arrive_station}&purpose_codes=ADULT",
|
||||
# }
|
||||
|
||||
|
||||
API_QUERY_INIT_PAGE = BASE_API + '/otn/leftTicket/init'
|
||||
API_LEFT_TICKETS = BASE_API + '/otn/{type}?leftTicketDTO.train_date={left_date}&leftTicketDTO.from_station={' \
|
||||
'left_station}&leftTicketDTO.to_station={arrive_station}&purpose_codes=ADULT'
|
||||
6
py12306/lib/exceptions.py
Normal file
@@ -0,0 +1,6 @@
|
||||
class RetryException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MaxRetryException(Exception):
|
||||
pass
|
||||
83
py12306/lib/func.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import threading
|
||||
|
||||
|
||||
def new_thread_with_jobs(jobs, wait=True, daemon=True, args=(), kwargs={}):
|
||||
"""
|
||||
Run each job with a new thread
|
||||
:param jobs:
|
||||
:param wait:
|
||||
:param daemon:
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
threads = []
|
||||
if not isinstance(jobs, list):
|
||||
jobs = [jobs]
|
||||
for job in jobs:
|
||||
thread = threading.Thread(target=job, args=args, kwargs=kwargs)
|
||||
thread.setDaemon(daemon)
|
||||
thread.start()
|
||||
threads.append(thread)
|
||||
if wait:
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
|
||||
def expand_class(cls, key, value, keep_old=True):
|
||||
"""
|
||||
Expand class method
|
||||
:param cls:
|
||||
:param key:
|
||||
:param value:
|
||||
:param keep_old:
|
||||
:return:
|
||||
"""
|
||||
from types import MethodType
|
||||
|
||||
if keep_old:
|
||||
setattr(cls, 'old_' + key, getattr(cls, key))
|
||||
setattr(cls, key, MethodType(value, cls))
|
||||
return cls
|
||||
|
||||
|
||||
def retry(num: int = 3):
|
||||
"""
|
||||
Retry a func
|
||||
:param num:
|
||||
:return:
|
||||
"""
|
||||
from py12306.lib.exceptions import RetryException, MaxRetryException
|
||||
retry_num_key = '_retry_num'
|
||||
|
||||
def decorator(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
retry_num = num
|
||||
if retry_num_key in kwargs:
|
||||
retry_num = kwargs.get(retry_num_key)
|
||||
kwargs.pop('_retry_num')
|
||||
try:
|
||||
res = func(*args, **kwargs)
|
||||
except RetryException as err:
|
||||
retry_num -= 1
|
||||
from py12306.app.app import Logger
|
||||
Logger.warning('重试 %s, 剩余次数 %s' % (func.__name__, retry_num))
|
||||
if retry_num > 0:
|
||||
kwargs[retry_num_key] = retry_num
|
||||
return wrapper(*args, **kwargs)
|
||||
raise MaxRetryException(*err.args) from None
|
||||
|
||||
return res
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def number_of_time_period(period: str) -> int:
|
||||
"""
|
||||
Example: 23:00 -> 2300
|
||||
:param period:
|
||||
:return:
|
||||
"""
|
||||
return int(period.replace(':', ''))
|
||||
70
py12306/lib/helper.py
Normal file
@@ -0,0 +1,70 @@
|
||||
class ShareInstance():
|
||||
__session = None
|
||||
|
||||
@classmethod
|
||||
def share(cls):
|
||||
if not cls.__session:
|
||||
cls.__session = cls()
|
||||
return cls.__session
|
||||
|
||||
|
||||
# Expand dict
|
||||
class Dict(dict):
|
||||
def get(self, key, default=None, sep='.'):
|
||||
keys = key.split(sep)
|
||||
for i, key in enumerate(keys):
|
||||
try:
|
||||
value = self[key]
|
||||
if len(keys[i + 1:]) and isinstance(value, Dict):
|
||||
return value.get(sep.join(keys[i + 1:]), default=default, sep=sep)
|
||||
return value
|
||||
except KeyError:
|
||||
return self.dict_to_dict(default)
|
||||
|
||||
def __getitem__(self, k):
|
||||
return self.dict_to_dict(super().__getitem__(k))
|
||||
|
||||
@staticmethod
|
||||
def dict_to_dict(value):
|
||||
return Dict(value) if isinstance(value, dict) else value
|
||||
|
||||
|
||||
class DataHelper:
|
||||
__origin: dict
|
||||
__mappers: dict = {}
|
||||
|
||||
def __init__(self, data: dict):
|
||||
self.__generate_mappers()
|
||||
self.__origin = data
|
||||
for key, val in data.items():
|
||||
if str(key) in self.__mappers:
|
||||
self.__dict__[self.__mappers[str(key)]] = val
|
||||
elif key in self.__annotations__:
|
||||
self.__dict__[key] = val
|
||||
if getattr(self, '_after', None):
|
||||
self._after()
|
||||
|
||||
def __generate_mappers(self):
|
||||
for key, val in self.__annotations__.items():
|
||||
try:
|
||||
val = self.__getattribute__(key)
|
||||
if isinstance(val, str) and val.startswith('key:'):
|
||||
tags = val.split(';')
|
||||
self.__dict__[key] = None
|
||||
for tag in tags:
|
||||
tag_info = tag.split(':')
|
||||
if tag_info[0] == 'key':
|
||||
self.__mappers[tag_info[1]] = key
|
||||
elif tag_info[0] == 'default':
|
||||
self.__dict__[key] = tag_info[1]
|
||||
|
||||
except (KeyError, AttributeError):
|
||||
pass
|
||||
|
||||
def get_origin(self) -> dict:
|
||||
return self.__origin
|
||||
|
||||
|
||||
class TrainSeat:
|
||||
types = {'特等座': 25, '商务座': 32, '一等座': 31, '二等座': 30, '软卧': 23, '硬卧': 28, '硬座': 29, '无座': 26, }
|
||||
order_types = {'特等座': 'P', '商务座': 9, '一等座': 'M', '二等座': 'O', '软卧': 4, '硬卧': 3, '硬座': 1, '无座': 1}
|
||||
27
py12306/lib/redis_lib.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from redis import Redis as PyRedis
|
||||
|
||||
from py12306.app.app import Config
|
||||
from py12306.lib.helper import ShareInstance
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class Redis(PyRedis, ShareInstance):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if not kwargs:
|
||||
kwargs = Config.REDIS
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def push_task(self, key: str, tasks: dict):
|
||||
return self.rpush(key, json.dumps(tasks))
|
||||
|
||||
def get_task_sync(self, keys: list) -> tuple:
|
||||
tasks = self.brpop(keys)
|
||||
return tasks[0][len(Config.REDIS_PREFIX_KEY_TASKS):], json.loads(tasks[1])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
res = Redis.share().keys('*')
|
||||
print(res)
|
||||
pass
|
||||
68
py12306/lib/request.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import requests
|
||||
from requests.exceptions import *
|
||||
from requests_html import HTMLSession, HTMLResponse
|
||||
|
||||
from py12306.lib.func import expand_class
|
||||
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
|
||||
class Request(HTMLSession):
|
||||
"""
|
||||
请求处理类
|
||||
"""
|
||||
|
||||
def save_to_file(self, url, path):
|
||||
response = self.get(url, stream=True)
|
||||
with open(path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
f.write(chunk)
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _handle_response(response, **kwargs) -> HTMLResponse:
|
||||
"""
|
||||
扩充 response
|
||||
:param response:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
response = HTMLSession._handle_response(response, **kwargs)
|
||||
expand_class(response, 'json', Request.json)
|
||||
return response
|
||||
|
||||
def add_response_hook(self, hook):
|
||||
hooks = self.hooks['response']
|
||||
if not isinstance(hooks, list):
|
||||
hooks = [hooks]
|
||||
hooks.append(hook)
|
||||
self.hooks['response'] = hooks
|
||||
return self
|
||||
|
||||
def json(self, default={}):
|
||||
"""
|
||||
重写 json 方法,拦截错误
|
||||
:return:
|
||||
"""
|
||||
from py12306.lib.helper import Dict
|
||||
try:
|
||||
result = self.old_json()
|
||||
return Dict(result)
|
||||
except:
|
||||
return Dict(default)
|
||||
|
||||
def request(self, *args, **kwargs):
|
||||
try:
|
||||
if 'timeout' not in kwargs:
|
||||
from py12306.app.app import Config
|
||||
kwargs['timeout'] = Config.REQUEST_TIME_OUT
|
||||
response = super().request(*args, **kwargs)
|
||||
return response
|
||||
except RequestException as e:
|
||||
if e.response:
|
||||
response = e.response
|
||||
else:
|
||||
response = HTMLResponse(HTMLSession)
|
||||
# response.status_code = 500
|
||||
expand_class(response, 'json', Request.json)
|
||||
return response
|
||||
@@ -1,3 +1,4 @@
|
||||
toml
|
||||
-i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
|
||||
appdirs==1.4.3
|
||||
beautifulsoup4==4.7.0
|
||||
@@ -17,7 +18,7 @@ MarkupSafe==1.1.0
|
||||
parse==1.9.0
|
||||
pyee==5.0.0
|
||||
PyJWT==1.7.1
|
||||
pyppeteer-box==0.0.27
|
||||
pyppeteer==0.0.25
|
||||
pyquery==1.4.0
|
||||
redis==3.0.1
|
||||
requests==2.21.0
|
||||
@@ -25,9 +26,9 @@ requests-html==0.9.0
|
||||
six==1.12.0
|
||||
soupsieve==1.6.2
|
||||
tqdm==4.28.1
|
||||
urllib3==1.24.2
|
||||
urllib3==1.24.1
|
||||
w3lib==1.19.0
|
||||
websockets==7.0
|
||||
Werkzeug==0.15.3
|
||||
Werkzeug==0.14.1
|
||||
DingtalkChatbot==1.3.0
|
||||
lightpush==0.1.3
|
||||
lightpush==0.1.3
|
||||
0
tests/__init__.py
Normal file
15
tests/helper.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from py12306.app.app import Config
|
||||
from py12306.lib.redis_lib import Redis
|
||||
|
||||
|
||||
class BaseTest(TestCase):
|
||||
redis: Redis = None
|
||||
config: Config = None
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
Config.TEST_MODE = True
|
||||
self.config = Config
|
||||
self.redis = Redis.share()
|
||||
74
tests/test_query.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from tests.helper import BaseTest
|
||||
from py12306.app.query import *
|
||||
|
||||
|
||||
class TestQueryTicket(BaseTest):
|
||||
task = {
|
||||
'name': 'admin',
|
||||
}
|
||||
query_dict = {
|
||||
'left_date': '2019-05-18',
|
||||
'left_station': 'BJP',
|
||||
'arrive_station': 'LZJ',
|
||||
'allow_seats': ['二等座'],
|
||||
'members': ['test']
|
||||
}
|
||||
query: QueryTicketData
|
||||
ticket_str = 'iV6uPpzX3CcwqhHe4yzrJHp9hFVCouaXtS01wlUB8f%2BuA%2BKD%2FTV5KLu37w1aKHO2zlAlwMDa%2FDYY%0A2xykUxU964zvkfI3qZZ6uGEKWi0tXCT8fhkQTVvnRI43%2FAinVozab2W1Cq%2FMzJtGBv3D1Q3CscAj%0ANA1XmfNzd6Carglhzvyyy63MkbLIRvxrngx9F01W9jhKXnQupQNTOM3Pw4UIxbesBWkmQfYNj%2Fj%2F%0A3mU33kluoI5vbGVsm115Ec%2BS29KPeaM%2B4%2F2h2UZsiCb%2F5hKfew8Hijodr2VuFftbkge1meSTRRvz%0A|预订|240000G42704|G427|BXP|LAJ|BXP|LAJ|06:21|13:44|07:23|Y|nhsXhn1BbGmb4MI%2BEto43zoslKFQlIY8c356nXAHAEk9Zb2G|20190518|3|P3|01|05|0|0|||||||||||有|无|5||O090M0|O9M|0|0|null'
|
||||
ticket: TicketData
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.ticket = QueryParser.parse_ticket([self.ticket_str])[0]
|
||||
self.query = QueryTicketData(self.query_dict)
|
||||
|
||||
def test_get_query_api_type(self):
|
||||
res = QueryTicket().get_query_api_type()
|
||||
self.assertEqual('leftTicket/query', res)
|
||||
|
||||
def test_get_ticket(self):
|
||||
res = QueryTicket().get_ticket(self.query_dict)
|
||||
|
||||
def test_is_ticket_valid(self):
|
||||
res = QueryTicket().is_ticket_valid(self.ticket, self.query)
|
||||
self.assertEqual(res, True)
|
||||
|
||||
def test_verify_period(self):
|
||||
self.query.left_periods = [('08:00', '16:00')]
|
||||
res = QueryTicket.verify_period('12:00', self.query.left_periods)
|
||||
self.assertEqual(res, True)
|
||||
res = QueryTicket.verify_period('16:00', self.query.left_periods)
|
||||
self.assertEqual(res, True)
|
||||
res = QueryTicket.verify_period('16:01', self.query.left_periods)
|
||||
self.assertEqual(res, False)
|
||||
|
||||
def test_verify_ticket_num(self):
|
||||
self.ticket.ticket_num = 'Y'
|
||||
res = QueryTicket.verify_ticket_num(self.ticket)
|
||||
self.assertEqual(res, True)
|
||||
|
||||
def test_verify_seat(self):
|
||||
self.query.allow_seats = ['硬座', '二等座']
|
||||
res = QueryTicket().verify_seat(self.ticket, self.query)
|
||||
self.assertEqual(res, True)
|
||||
self.assertEqual(self.query.available_seat.num, 30)
|
||||
|
||||
|
||||
class TestQueryParser(TestCase):
|
||||
tickets = [
|
||||
'iV6uPpzX3CcwqhHe4yzrJHp9hFVCouaXtS01wlUB8f%2BuA%2BKD%2FTV5KLu37w1aKHO2zlAlwMDa%2FDYY%0A2xykUxU964zvkfI3qZZ6uGEKWi0tXCT8fhkQTVvnRI43%2FAinVozab2W1Cq%2FMzJtGBv3D1Q3CscAj%0ANA1XmfNzd6Carglhzvyyy63MkbLIRvxrngx9F01W9jhKXnQupQNTOM3Pw4UIxbesBWkmQfYNj%2Fj%2F%0A3mU33kluoI5vbGVsm115Ec%2BS29KPeaM%2B4%2F2h2UZsiCb%2F5hKfew8Hijodr2VuFftbkge1meSTRRvz%0A|预订|240000G42704|G427|BXP|LAJ|BXP|LAJ|06:21|13:44|07:23|Y|nhsXhn1BbGmb4MI%2BEto43zoslKFQlIY8c356nXAHAEk9Zb2G|20190518|3|P3|01|05|0|0|||||||||||有|无|5||O090M0|O9M|0|0|null',
|
||||
'UY8SmgFA1grdsKcN7%2B4133%2FSWTQqk8wVKcdLNsk6EAiuPIaE5aPPzUr9f%2FepLG0hLchNAKjlOl71%0AbMcW3HypGxckM8L3Hz1rg3ds77qPxXXDFxMITHRQfZzSoM8uqSKPdVwT4mEs6ynZ2Niw7M3iAHbq%0A0qjpuj%2FaAc5yiWsvHxAGc3UQPqchrXjcabyp9%2Bnmf7z84Ep74XirfcRmAmZopq%2B9ySctz9lnwule%0A%2FaSdcAWypluKLPobkAdSpxwndKk8bV2U%2Bq%2BbGPaNEzy2i9ixRdaBBLkg3OAqHaCBetr9gHFEiYXu%0A|预订|240000G4290C|G429|BXP|LAJ|BXP|LAJ|10:45|19:45|09:00|Y|4lti%2FihSxlRgd8xN4SFzPvGmpcT90cvJFfy4V5IGfmyCNl6r|20190518|3|P4|01|16|0|0|||||||||||有|有|5||O0M090|OM9|0|0|null',
|
||||
'VUb2s9O%2BqvddST8Tk%2BT8PzHNjzrMsp301eZv9ukz3jw55DHXLMpQ3ZK92ystqCe9atpD7DFlHiHD%0AB9q%2F4EoAaoU3OwacLHAEMtr9fX%2FYXwuCMhHmQHw%2BL8eejS9QgR5ZQM8oV6%2FeaJ5x5KqCIutwZBtz%0AgzuRZ%2FpOHSGdg03WWOXdWHVpJrBUleLGpQZ%2BQJMz0YGrl1Md%2BpNu5ypNdyKg6AyYjmZs4fRz6Slj%0AwCbQlhkclS2mvxpAE5gSJZ3nY8IjFelQTAqt6XTEHPsZ7Rd%2FNHwOM7UtlbQy7NyBCHTLgIAjuB58%0ADEpzVw%3D%3D|预订|240000T1750J|T175|BXP|XNO|BXP|LZJ|13:05|07:44|18:39|Y|g%2F3wSCFH0UvzDmFPO8NuyXGeIMI26cl93Qzex2RLyufZ8M5i2%2FvdylS8zKM%3D|20190518|3|P4|01|13|0|0||||无|||有||有|有|||||10401030|1413|0|0|null',
|
||||
'dPVMZOEQT2rtEi5BNTY2h1nNhp2H%2BA%2BKZaZINqEQ2RUbeKK%2BFeC1y%2Bm8NdO%2BlS4Ag8r6hsWfWHdL%0AX7DrJJMRMuEXnCwqcc%2Bnwd%2BfvdaeozWFuGE08OFZzJbGnnL%2F54VMSdUnapJ4jWVvsYLG2RUqopiX%0AjDavL7dBULGrfNZN4EMTBFqUz%2BzqnmDGvf3RaXr7EHrztAJSNEQc09PqlGHs65B3VaFhN%2Fa0%2BgVQ%0AXCIAP1YysdgqDXMndNNq4nkMX21Jruvi8ToQWsGnYCf%2F7OIzS5HwOu3PElDZ9bMfempLAFk%3D|预订|2400000Z550M|Z55|BXP|LZJ|BXP|LZJ|14:58|07:30|16:32|Y|nNj9EIzgMtJaVlhUo0gt3HKZi820vP3HktntoPUe%2FFW%2BDfiv|20190518|3|P4|01|06|0|0||11||有|||||有||||||604030|643|0|0|null',
|
||||
'V%2F6N%2BhZhuqxSnfkLwZHsgPQBDsGMcJkhZXyuWQLCKlzv7T%2BMvJerzW2u2TBoM8aRbqVkjywXT99K%0AdGcCUHmNOqXqngnHnvg1yj0jvsfQHPRHKIPa6hl0QeX%2BgM%2F%2Ffyj8opU919pW4YT3HViE0hQ8vNRT%0AUQOdJmbSFo1b3xI05cuzh4j21RuP9sdgaA%2BnheYMTvyMoYiEUvN1%2BClGrlrbXnhHgSWFUMxu88sG%0AQGpnqTLtVx27AAe58c9qy5oq35lOnf5OV6%2BUebB7n4YYy7ZpugZ1gyPndGGhvQdg8j58HFo%2FY%2BC1%0AzbdwhA%3D%3D|预订|2400000Z7508|Z75|BXP|LZJ|BXP|LZJ|15:57|10:30|18:33|Y|bIdN7uqCXyxKnuLinwN1naNDcYioI7Xuk535Xl1xm6Wn8CRtk2knPYx5MW4%3D|20190518|3|P4|01|07|0|0||||有|||有||有|有|||||10401030|1413|0|0|null',
|
||||
'2WQo8Fm2OT6Y016qIB5vRQNikHMVarIhB9YUu7sDFKMTC3RFxmi7Y%2BE9S%2BjdYxUoEfUiqhj%2F8ZX1%0A1GpE8Vikd5urQLbp5%2FjkES9798ohE3dQwZ0ffKHX%2FQiIl4maKmdVKebWTyV8IMgTThm5C1l%2B8csY%0ApM0kaFEsQtERyf8Mh9FH9vQDxn2Vtb%2FoOPY2UvNS%2F8Tf%2BNWni21Dh8tRZ0ZL9CBYl6%2BRbNphYSZy%0AhQASZ9fG%2BjJe96bZL%2FsuMvFa%2FTNG51k07G8mggtoqgREp0zP0cdBHjkOm%2BTmMuK7uqLS9gUodYds%0ApEj%2B%2Bg%3D%3D|预订|240000Z1510B|Z151|BXP|XNO|BXP|LZJ|16:03|11:19|19:16|Y|TM4VyMprWgU1m%2ByEJxolE3Hutch%2FGYoyOMLhudWSaubKi5OeWcwS8XZJvkU%3D|20190518|3|P2|01|09|0|0||||无|||有||有|有|||||10401030|1413|0|0|null',
|
||||
'i4IZi6FeuPVecIlQ7MMdptQ77XQ6DEH14WRtbCN1%2FJViWj7liJ3qUEJ9ml3aC9%2B8cBPbKsVHycxa%0ApoLgwMxEcxJ8LdFDeWHSJ%2FbRPyw0Ygs3tYGz%2FTYv4Ys03Oc6NGJsXlt76XQ6Lmm6fDVs%2FKnsWARg%0Ar2NxqMn0ecRGgiDAcVRF4CApE3cdE2GW0%2Blt7xcbPTDc3R2vawIAk8zKlMWKaMReXfqgeeln%2BAIV%0Aa9KSTBxgR9pC85I%2BVMJe4mYVLeUaSa%2FI7fYrXfJyVu%2BDDdiQaWEwLsTlmh7cxkGZlosHLtJh14Ym%0A6xjrqg%3D%3D|预订|2400000Z210D|Z21|BXP|LSO|BXP|LZJ|20:00|12:17|16:17|Y|et1f50q%2B5c5I%2B9WjMAG7QRRd%2F5lr5LzzS%2Bijw0HrPjMGTPoFY0BytCT68Ho%3D|20190518|3|P2|01|05|0|0||||无|||有||无|有|||||10401030|1413|0|0|null',
|
||||
'XyBvey3WmmF82TTYlMRIMGTG9tntgOjqf7d9Y7YgdZTP16T3Ts1loq5oqe9XUOrKNJxRGUmv4Q9h%0AkbnGYxvHA4LgWlDsyqO%2B%2B6SoX%2BW%2BtCH%2BC5JXvabJcaN%2BfZjQa8aBYvHHNx4li28D4tlCfrKnkB%2BU%0AzHfTSG6ekFF5K53clwbEyaljpJDdCi6uSQqMPUkslA1RQ4KAQPnXEbDbz4oC9IdjGiiTTPuC7QJU%0A0r2VW5TnKXvJr6toDWogGW8icGjeuDVcNKn%2B5OltBdNJD5bheKSE4hjzv8HauF8H%2Bb3c77jzHqSk%0ANtV%2Bgw%3D%3D|预订|250000K8880P|K885|TJP|LAJ|BJP|LZJ|23:43|05:07|29:24|Y|A1VIUe1w8dwrEhQacQ1O8SQntd7wRO0M%2Bck0TjWwuZgB%2Fi%2BVT2cxShZVqzQ%3D|20190518|3|P4|03|15|0|0||||无|||无||无|有|||||10403010|1431|1|0|null'
|
||||
]
|
||||
|
||||
def test_parse_ticket(self):
|
||||
res = QueryParser.parse_ticket(self.tickets)
|
||||
self.assertEqual(res[0].left_station, 'BXP')
|
||||
self.assertEqual(res[0].train_number, 'G427')
|
||||
9
tests/test_redis.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from py12306.lib.redis_lib import Redis
|
||||
from tests.helper import BaseTest
|
||||
|
||||
|
||||
class TestRedis(BaseTest):
|
||||
|
||||
def test_connection(self):
|
||||
res = Redis.share().info()
|
||||
self.assertIsInstance(res, dict)
|
||||
20
tests/test_task.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from tests.helper import BaseTest
|
||||
from py12306.app.task import Task
|
||||
|
||||
|
||||
class TestTask(BaseTest):
|
||||
def test_push_task(self):
|
||||
tasks = {
|
||||
'query': {
|
||||
'name': 'admin',
|
||||
},
|
||||
'user': {
|
||||
'name': 'admin',
|
||||
'password': 'password'
|
||||
}
|
||||
}
|
||||
for key, task in tasks.items():
|
||||
self.redis.push_task(self.config.REDIS_PREFIX_KEY_TASKS + key, task)
|
||||
|
||||
res = Task.listen()
|
||||
self.assertIsInstance(res, dict)
|
||||
5
tests/test_user.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from tests.helper import BaseTest
|
||||
|
||||
|
||||
class TestUser(BaseTest):
|
||||
pass
|
||||