198 Commits

Author SHA1 Message Date
pjialin
8e13823d88 Merge pull request #413 from IronXiao/master
增加二维码发送到邮箱功能,以便人不在电脑旁登陆失效可以远程扫描登录
2021-10-21 17:37:34 +08:00
IronXiao
481a9c0310 Merge branch 'master' of github.com:IronXiao/py12306 2021-09-30 09:44:20 +08:00
IronXiao
43caf8fdc8 fix some error 2021-09-30 09:43:33 +08:00
IronXiao
ac4abae019 Merge branch 'master' of github.com:IronXiao/py12306 2021-09-30 09:38:08 +08:00
IronXiao
08799d61f0 增加二维码发送到邮箱功能,以便人不在电脑旁登陆失效可以远程扫描登录 2021-09-30 09:37:51 +08:00
IronXiao
6e5ceca134 增加二维码发送到邮箱功能,以便人不在电脑旁登陆失效可以远程扫描登录 2021-09-29 19:07:43 +08:00
pjialin
db34583c7d Merge pull request #412 from Gardelll/feature/qr_login
添加扫码登录功能
2021-09-27 09:54:23 +08:00
Gardel
4f3abc9446 优化终端二维码显示
Signed-off-by: Gardel <sunxinao@hotmail.com>
2021-09-26 14:04:33 +08:00
Gardel
c98c423c5e 调整网站服务时间
网站服务时间以调整为:”全天提供信息查询及退票服务,每日5:00至次日1:00(周二为5:00至23:30)提供售票改签服务“

见 [互联网购票须知](https://kyfw.12306.cn/otn/gonggao/saleTicketMeans.html?toservicetime&linktypeid=means3)

Signed-off-by: Gardel <sunxinao@hotmail.com>
2021-09-25 17:39:36 +08:00
Gardel
f7c8ff4daa 修改二维码打印逻辑
防止并发输出日志时错位

Signed-off-by: Gardel <sunxinao@hotmail.com>
2021-09-25 17:35:09 +08:00
Gardel
68a508e30a 添加扫码登录功能
Signed-off-by: Gardel <sunxinao@hotmail.com>
2021-09-24 21:36:37 +08:00
pjialin
49d35aabdc Merge pull request #401 from DingGHui/master
修改调用submitApi返回值判断
2021-01-07 10:02:16 +08:00
DingGuangHui
9e529c1f9f 修改调用submitApi返回值判断 2021-01-06 16:59:17 +08:00
Jalin
6b6bf41a51 更新为清华 pypi 源 #358 2020-01-10 20:56:07 +08:00
pjialin
badc8b8c31 Merge pull request #357 from pjialin/fix/drag-captcha
更新滑动验证码识别
2020-01-10 20:06:35 +08:00
Jalin
fbeb7835e5 更新滑动验证码识别 2020-01-10 20:02:33 +08:00
pjialin
f9a0f20e41 Merge pull request #352 from IronXiao/master
增加使用浏览器缓存RAIL_EXPIRATION 和 RAIL_DEVICEID 配置,解决临时无法登录问题
2020-01-07 14:49:39 +08:00
IronXiao
fd8c500128 增加用户本地打码平台配置 2020-01-06 16:34:01 +08:00
IronXiao
da1a0903c5 增加使用浏览器缓存RAIL_EXPIRATION 和 RAIL_DEVICEID 配置,解决临时无法登录问题 2020-01-06 15:24:50 +08:00
pjialin
8d0a6f05b7 Merge pull request #349 from pjialin/revert-325-szhang01/fix_env_overwrite
Revert "[szhang01/fix_env_overwrite] Comment out unnecessary setting in env.py.example"
2020-01-06 13:25:19 +08:00
pjialin
c82a94de39 Revert "[szhang01/fix_env_overwrite] Comment out unnecessary setting in env.py.example" 2020-01-06 12:28:32 +08:00
Jalin
0316e52e90 Fix 验证码下载失败,返回数据为空 #262 #297 2020-01-03 12:18:30 +08:00
pjialin
390fcecdd2 Merge pull request #317 from out0fmemory/master
增加时间判断逻辑
2019-12-30 10:27:30 +08:00
pjialin
12581dc03d Merge pull request #325 from ShengZhang2016/szhang01/fix_env_overwrite
[szhang01/fix_env_overwrite] Comment out unnecessary setting in env.py.example
2019-12-30 10:20:07 +08:00
zhangsheng
095dbfc21d [szhang01/fix_env_overwrite] Comment out unnecessary setting in env.py.example. 2019-12-29 01:13:15 -08:00
out0fmemory
eee53461cf 增加买票时间校验逻辑,防止出现日期不对,但是报的是302错误 2019-12-27 18:44:32 +08:00
out0fmemory
95e9e8a89b 修改默认日期到2020年 2019-12-27 18:43:56 +08:00
pjialin
998b387875 Merge pull request #288 from out0fmemory/master
修改人员的注释,存在不注意情况下有多余的人员信息
2019-12-24 18:56:18 +08:00
pjialin
9a42e0e8d7 Merge pull request #272 from wendingzhang/wendingzhang-patch-1
Add new stations
2019-12-24 18:52:45 +08:00
out0fmemory
681157c40b 修改人员的注释,存在不注意情况下有多余的人员信息 2019-12-24 18:25:58 +08:00
wending.zhang
4468aa2277 Add new stations 2019-12-23 14:17:23 +08:00
pjialin
c6ec668fad Update README.md 2019-12-21 12:24:28 +08:00
pjialin
3a795492de Update README.md 2019-12-17 17:45:33 +08:00
Jalin
006fd48d71 更新 12306 服务时间 2019-12-16 10:08:02 +08:00
pjialin
ee50ad213d Update README.md 2019-12-13 10:27:55 +08:00
pjialin
6c7c83e53d Merge pull request #233 from pjialin/fix/passenger-error
fix get passengers error
2019-11-25 14:44:45 +08:00
Jalin
6113b8519f fix get passengers error 2019-11-25 14:33:22 +08:00
pjialin
a3286fc2c2 Merge pull request #227 from pjialin/dependabot/pip/urllib3-1.24.2
Bump urllib3 from 1.24.1 to 1.24.2
2019-11-25 13:31:05 +08:00
dependabot[bot]
5bcb078124 Bump urllib3 from 1.24.1 to 1.24.2
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.24.1 to 1.24.2.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/master/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.24.1...1.24.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-11-01 02:00:09 +00:00
pjialin
7ec0cbd235 Merge pull request #225 from pjialin/dependabot/pip/werkzeug-0.15.3
Bump werkzeug from 0.14.1 to 0.15.3
2019-11-01 09:58:55 +08:00
dependabot[bot]
9531f774c2 Bump werkzeug from 0.14.1 to 0.15.3
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 0.14.1 to 0.15.3.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/master/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/0.14.1...0.15.3)

Signed-off-by: dependabot[bot] <support@github.com>
2019-10-21 18:01:18 +00:00
pjialin
e3038a3f21 增加本地打码仓库链接 2019-10-14 09:52:03 +08:00
pjialin
1ddd6cdd69 Merge pull request #221 from bnpzsx/强制购买成人票
支持学生直接购买成人票
2019-10-12 16:24:23 +08:00
Jalin
a258780185 Merge branch 'master' of github.com:pjialin/py12306 2019-10-12 15:27:35 +08:00
Jalin
5d0b14ce85 修复登录网络错误 #155 2019-10-12 15:26:55 +08:00
bnpzsx
db7e88a6a6 支持学生直接购买成人票 2019-10-01 11:07:13 +08:00
pjialin
0578f2e3a8 Merge pull request #203 from kbj/master
increase timeout of OCR
2019-09-29 15:58:58 +08:00
Weey
3b9b15deba update cdn list 2019-09-27 11:58:47 +08:00
Weey
91c0a44a32 set timeout of OCR 2019-09-19 21:59:07 +08:00
Jalin
f377c7a849 修复订单会重复提交 #183 #187 2019-09-17 16:11:42 +08:00
Jalin
7136474a6b Merge branch 'pr175' 2019-09-12 15:54:21 +08:00
Jalin
563b051799 更新 Readme 2019-09-12 15:38:37 +08:00
Jalin
3a31349662 验证码识别修改为免费打码 2019-09-12 15:34:50 +08:00
Jalin
cc67987bb3 修改 Device Id 获取方式 2019-09-12 15:23:03 +08:00
Jalin
64a52559f3 修复下单 enc_str 报错 #180 2019-09-12 12:03:21 +08:00
Jalin
4966d45f96 修复下单失败 #173 2019-09-10 22:15:51 +08:00
bnpzsx
cb8e0aada6 邮件通知修复
调用starttls()需要重新认证
2019-09-07 18:15:00 +08:00
Jalin
6e8cd9f902 修复登录失效 #168 2019-09-03 13:44:35 +08:00
Jalin
8f984cd751 优化验证码识别 2019-09-03 13:43:48 +08:00
Weey
0bd6417ff4 update readme 2019-09-02 21:54:10 +08:00
Weey
619a96ed5d update readme 2019-09-02 21:51:40 +08:00
Weey
83cc5e6d6c Merge remote-tracking branch 'upstream/master' 2019-09-02 21:37:02 +08:00
Jalin
b848a49720 Update readme 2019-07-29 10:22:18 +08:00
Weey
707fd5bd8a 打码方式使用机器学习 2019-06-29 11:07:34 +08:00
Jalin
8cabf157f6 Remove repeated code 2019-05-18 18:06:59 +08:00
Weey
eeea27403c 修正登陆失败的问题 2019-05-18 15:03:27 +08:00
Jalin
4ac5fc3403 update browser id algorithm 2019-04-05 14:01:52 +08:00
Jalin
e56b545392 fix login fail #151 2019-04-04 15:36:44 +08:00
Jalin
1e3ea59bb6 fix hooks may not been assignment #148 2019-04-02 16:47:45 +08:00
Jalin
42f2a640f6 修复登录失效问题 #144 2019-04-02 16:33:16 +08:00
Jalin
a3f185e3a8 修复登录失效 #144 2019-03-26 23:18:36 +08:00
Jalin
ad5e071080 修复获取查询接口正则匹配问题 #142 2019-03-15 10:23:28 +08:00
Jalin
2578be4965 更换免费打码接口地址 #140 2019-03-07 09:58:06 +08:00
Jalin
365453e0b3 增加动态获取查询地址 #121 2019-02-20 23:11:55 +08:00
Jalin
733d512f72 add bark to docker env example 2019-02-10 22:32:58 +08:00
Zzde
c430a65cdd 增加bark通知 2019-02-02 14:20:15 +08:00
Jalin
5a97d11a09 增加网络默认超时时间 2019-01-29 15:37:04 +08:00
Jalin
df7467e18d env.docker 增加时间筛选示例 2019-01-29 15:26:53 +08:00
Breeze Chen
969bc99e87 通知消息 增加 乘车人,发车时间、到达时间、账号等信息 2019-01-27 16:05:35 +08:00
Breeze Chen
9c63063d50 增加:发车时间段筛选 2019-01-27 15:34:06 +08:00
Jalin
08ad7598db 优化避免无座检测逻辑 2019-01-25 20:52:30 +08:00
Jalin
afd8c35afb 优化停留间隔,循环获取乘客问题 2019-01-25 14:10:27 +08:00
Jalin
419495ce84 查询时禁止 redirect, 修改 typo #113 2019-01-24 22:22:49 +08:00
Jalin
6a17dab7b0 增加 网络错误时,停留一段时间 #12 2019-01-22 23:12:25 +08:00
Jalin
55fb26176a 更新 docker-compose.yml 2019-01-22 22:34:52 +08:00
Jalin
61961e515c 优化错误提示 2019-01-22 22:16:15 +08:00
Jalin
fe5e82f2ff 优化用户检测流程 2019-01-22 21:53:41 +08:00
Jalin
dac614663d Merge branch 'master' of github.com:pjialin/py12306 2019-01-22 13:49:26 +08:00
Jalin
9476052000 修改 gitpd 使用 2019-01-22 13:49:14 +08:00
Jalin
1431c577f9 Merge branch 'pr98' 2019-01-22 12:51:16 +08:00
pjialin
0908da55c5 Merge pull request #93 from imhy123/master
使用expire判断节点下线
2019-01-22 12:19:14 +08:00
Jalin
c71547e3db Merge branch 'pr77' 2019-01-22 11:58:40 +08:00
Jalin
91763f3141 修改 docker-compose 使用说明 2019-01-22 11:58:26 +08:00
Jan Keromnes
5a1e34b6fa 更新 readme,配置Gitpod 2019-01-21 19:15:31 +00:00
spirithy
1a617db658 使用expire判断节点下线 2019-01-21 00:10:19 +08:00
Jalin
e8abbd57bb 增加 cdn 检测超时配置 2019-01-19 15:48:45 +08:00
Jalin
cc3426c05f 移除子节点乘客检测,web 增加 cdn 显示 2019-01-19 15:11:30 +08:00
Jalin
7762c88db6 增加节点订阅消息丢失重连 2019-01-19 11:56:34 +08:00
Jalin
51ab20f7cf 更新 readme,修复子节点用户初始化问题 2019-01-18 22:36:19 +08:00
Jalin
bdc670a86d 优化排队错误直接结束排队 2019-01-18 20:17:31 +08:00
Jalin
7c21310ea3 优化 cdn 优化下单逻辑 2019-01-18 19:31:30 +08:00
Jalin
7863cd2863 优化座位提示 2019-01-18 19:07:03 +08:00
Jalin
a9287eb7e8 增加通知详细信息 #43 #51 增加新的语音消息服务商 2019-01-18 18:58:37 +08:00
brucedone
bef3e95cec fix the readme typo 2019-01-18 18:32:44 +08:00
brucedone
f00f1922a5 add docker-compose deploy 2019-01-18 18:29:56 +08:00
Jalin
e91ad184bb 增加排除车次, 增加通过序号确定唯一联系人 #68 2019-01-18 18:11:50 +08:00
Jalin
556a8fc7c3 车次支持大小写 2019-01-18 17:29:00 +08:00
Jalin
bfff29cd67 增加未支付订单检测 #75 移除语音重复通知 2019-01-18 17:13:13 +08:00
Jalin
98e5d8f95a 增加 cdn 查询 2019-01-18 17:08:39 +08:00
Jalin
8032422f7d 优化提示 2019-01-18 10:39:01 +08:00
Jalin
a6081ec0b5 Merge https://github.com/a2r0n/py12306 into pr 2019-01-18 10:33:03 +08:00
Jalin
0681cef3db 修改名称错误 2019-01-18 10:31:15 +08:00
a2r0n
eb8d1f5a97 增加status.json加载失败时的容错处理. 2019-01-17 14:41:59 +08:00
Jalin
3e7616ccea 修改用词 2019-01-17 13:19:21 +08:00
Jalin
a21bf2264e 修复编码问题 优化提示 2019-01-17 11:21:14 +08:00
pjialin
1c4e832d87 Merge pull request #61 from kbj/master
修正提示文案
2019-01-16 23:05:31 +08:00
Weey
2f98504328 修正提示文案 2019-01-16 22:32:29 +08:00
Jalin
0da42b9f26 优化错误输出 2019-01-16 21:18:57 +08:00
Jalin
1387cbede0 修复编码不兼容 #14 #57 2019-01-16 19:17:10 +08:00
Jalin
59f5b8cb49 Merge branch 'master' of github.com:pjialin/py12306 2019-01-16 17:35:19 +08:00
Jalin
b46ca021a9 修复时间错误可能导致查询无票 2019-01-16 17:34:59 +08:00
pjialin
3b3dd8337d Merge pull request #54 from idealhack/patch-1
docs: update docker command example in README
2019-01-16 17:07:12 +08:00
Yang Li
059e5b5d1c docs: update docker command example in README 2019-01-16 16:58:57 +08:00
Jalin
0524f5fc27 修复 log 文件不会自动创建 2019-01-16 13:17:06 +08:00
pjialin
682613ca31 Merge pull request #48 from feng409/master
移除重复命令
2019-01-16 12:50:11 +08:00
chemf
2f61002562 移除重复命令 2019-01-16 12:38:43 +08:00
Jalin
c2ce769a92 Merge branch 'master' of github.com:pjialin/py12306 2019-01-15 23:25:01 +08:00
Jalin
ddfd38e0a9 增加邮箱 ssl 支持 #33 2019-01-15 23:24:26 +08:00
pjialin
af71d0a921 Merge pull request #42 from kbj/master
新增一个Telegram推送Bot地址
2019-01-15 23:20:25 +08:00
Jalin
1c98e2743a 修复提交订单成功后不停止排队 #35 2019-01-15 23:12:12 +08:00
Weey
d4d4ee2139 新增另外一个Telegram推送Bot 2019-01-15 22:34:08 +08:00
Jalin
8df3c4032d 修复不立即登录问题 2019-01-15 12:55:55 +08:00
Jalin
884b0f30db 更新 readme 2019-01-15 12:27:54 +08:00
Jalin
f3b0cbee24 修改 key 参数规则 2019-01-15 12:27:24 +08:00
Jalin
0d52efb1da Merge https://github.com/wonderful60/py12306 into pr 2019-01-15 12:02:30 +08:00
littlefatty.wong
4bd5cdb89e 给PushBear添加SC_KEY参数用于填写send_key 2019-01-15 11:57:51 +08:00
Jalin
bedeb20085 Merge https://github.com/wonderful60/py12306 into pr 2019-01-15 11:46:45 +08:00
littlefatty.wong
00d5ea4be6 将配置文件参数SKEY修改为S_KEY以符合参数规则 2019-01-15 11:35:23 +08:00
littlefatty.wong
2c9fa98efc add ServerChan and PushBear支持 2019-01-15 11:13:51 +08:00
Jalin
901864e12f Merge branch 'master' of github.com:pjialin/py12306 2019-01-15 10:06:33 +08:00
Jalin
09ba6fef40 增加软座 #17 2019-01-15 10:06:22 +08:00
pjialin
25d7169afe Merge pull request #23 from feng409/master
[FIX] 移除重复代码
2019-01-15 09:39:35 +08:00
chemf
ae70de23ae [FIX] 移除重复代码 2019-01-15 00:26:10 +08:00
Weey
45d3e91391 新增推送到Telegram 2019-01-14 22:34:32 +08:00
Echowxsy
80990eb02a 添加钉钉消息通知测试 2019-01-14 20:18:28 +08:00
Echowxsy
95925ba9e7 添加订单完成时钉钉通知入口 2019-01-14 20:18:28 +08:00
Echowxsy
8109d87c5f 实现钉钉通知方法 2019-01-14 20:18:28 +08:00
Echowxsy
dbe98ca867 添加钉钉配置文件 2019-01-14 20:18:28 +08:00
Echowxsy
ff8a09e3fb 添加钉钉消息通知依赖 2019-01-14 20:18:28 +08:00
Jalin
559dabe0b5 Merge branch 'master' of github.com:pjialin/py12306 2019-01-14 18:18:53 +08:00
Jalin
d0b61852d3 修复 bug #16,增加任务更新后重新检测乘客 2019-01-14 18:18:26 +08:00
pjialin
ef7ec01329 修改错字 2019-01-14 16:07:23 +08:00
Jalin
550c87d77a 增加集群状态支持 2019-01-14 16:06:27 +08:00
Jalin
26d0f5a1de 优化查询错误 2019-01-14 12:34:48 +08:00
Jalin
88ddc87f03 修复类型转换错误 2019-01-13 12:50:37 +08:00
Jalin
eb167538d2 更新 Docker 时区 为 Shanghai 2019-01-13 12:12:13 +08:00
Jalin
047024d239 新增免费打码 2019-01-13 11:56:08 +08:00
Jalin
751c01298f 优化程序流程 2019-01-13 11:02:02 +08:00
Jalin
ea10558bff 更新 requirements 2019-01-12 22:56:09 +08:00
Jalin
e252a41fd3 Merge branch 'master' of github.com:pjialin/py12306 2019-01-12 22:47:54 +08:00
Jalin
994c211e2f 增加 Web 管理页面 2019-01-12 22:47:27 +08:00
pjialin
54eb32d40f Merge pull request #4 from SCUTJcfeng/master
修复Windows signal模块没有SIGHUP的问题
2019-01-12 19:00:31 +08:00
fengjc
2396a40911 修复Windows signal模块没有SIGHUP的问题 2019-01-12 16:59:17 +08:00
Jalin
a3e1544072 优化结果 2019-01-12 13:42:09 +08:00
Jalin
e722eacbb8 增加用户,统计,应用接口 2019-01-12 13:09:59 +08:00
Jalin
3c982e4ac4 Merge branch 'master' into develop 2019-01-12 12:19:21 +08:00
Jalin
9b46ee8a85 增加验车防止用户重复状态失效 2019-01-12 12:18:39 +08:00
Jalin
0dc4eee1a8 增加 web 目录 2019-01-12 12:16:11 +08:00
Jalin
cb4fd195a1 修复 windows 下 signal.SIGHUP 错误 2019-01-12 11:08:38 +08:00
Jalin
a0f2bc7913 修复邮件发送者错误 2019-01-12 10:51:09 +08:00
Jalin
6004d78e90 增加邮件通知 2019-01-11 21:50:18 +08:00
pjialin
bcab8c4e30 增加 加群链接 2019-01-11 14:03:49 +08:00
Jalin
41f4628eb9 优化乘客检测 2019-01-11 13:44:42 +08:00
Jalin
df41b8d8c2 update readme 2019-01-11 12:32:22 +08:00
Jalin
111541a1e8 优化提示 2019-01-11 12:22:57 +08:00
Jalin
48a1f6a06a 增加动态加载配置 2019-01-11 01:42:04 +08:00
Jalin
a4b355bdf1 修改验证码为 image64 方式 2019-01-10 20:48:58 +08:00
Jalin
6c8d5e3142 update readme 2019-01-10 18:55:08 +08:00
pjialin
c52d91c5e8 Update README.md 2019-01-10 17:44:46 +08:00
Jalin
e510de878d 增加 Docker 使用说明 2019-01-10 17:41:51 +08:00
Jalin
e17b48a0ce 增加 docker 支持 2019-01-10 17:23:13 +08:00
Jalin
1be623032f 增加 redis key prefix 2019-01-10 14:50:17 +08:00
Jalin
0670e19b05 更新 requirements.txt 2019-01-10 13:56:20 +08:00
Jalin
a05f1a910a 支持分布式集群 2019-01-10 13:47:31 +08:00
Jalin
c3f3ba9ffc 修复并发锁问题 2019-01-10 13:01:05 +08:00
Jalin
0d7558afeb 优化事件处理 2019-01-10 11:06:07 +08:00
Jalin
f47368206c 增加事件通知 2019-01-10 02:13:19 +08:00
Jalin
632256caf7 优化同步处理 2019-01-09 22:20:54 +08:00
Jalin
668c4ae8ce 完成用户保持 2019-01-09 21:01:12 +08:00
Jalin
7d5b8e2b80 完成分布式查询 2019-01-09 11:14:49 +08:00
Jalin
8f681c5e30 优化输出信息 2019-01-08 19:12:44 +08:00
Jalin
6d18a8d11b 增加单个任务添加多个出发和到达车站 2019-01-08 17:28:08 +08:00
Jalin
6dc3005cd9 优化错误处理 2019-01-08 16:45:54 +08:00
Jalin
a96e08efeb 增加输出日志到文件 2019-01-08 12:08:06 +08:00
Jalin
a6feda0a41 添加 README 2019-01-08 11:02:33 +08:00
Jalin
2a734bdfe6 增加 requirements.txt 2019-01-08 02:14:16 +08:00
Jalin
8bcbaf4cde Merge branch 'master' of github.com:pjialin/py12306 2019-01-08 02:07:19 +08:00
pjialin
5b296694a9 Initial commit 2019-01-08 02:04:12 +08:00
100 changed files with 845 additions and 910 deletions

5
.gitignore vendored
View File

@@ -4,5 +4,6 @@
venv
__pycache__
env.py
docker-compose.yml
config.toml
env.slave.py
env.docker.py
docker-compose.yml

6
.gitpod.yml Normal file
View File

@@ -0,0 +1,6 @@
ports:
- port: 8008
onOpen: open-preview
tasks:
- init: pip install -r requirements.txt && cp env.py.example env.py
command: python main.py -t

View File

@@ -1,10 +1,6 @@
# 🚂 py12306 购票助手
分布式,多账号,多任务购票
## 前言
今年回家的票明显要难买很多,早早就答应了父母今年的票没问题,到现在一张票没买到,虽然家里已经订了汽车票,让我不用操心,但是想想他们一行还有小孩,心还是很伤的。
这段时间从 12306Bypass 到 testerSunshine 大佬写的 [12306](https://github.com/testerSunshine/12306),还是没买到票,索性就自己写了一个,希望也能帮助到更多人
## Features
- [x] 多日期查询余票
- [x] 自动打码下单
@@ -18,7 +14,7 @@
- [x] 邮件通知
- [x] Web 管理页面
- [x] 微信消息通知
- [ ] 代理池支持
- [ ] 代理池支持 ([pyproxy-async](https://github.com/pjialin/pyproxy-async))
## 使用
py12306 需要运行在 python 3.6 以上版本(其它版本暂未测试)
@@ -36,9 +32,8 @@ cp env.py.example env.py
```
自动打码
目前支持免费打码,和若快打码
注:免费打码无法保证持续可用,如失效请手动切换到若快平台,需要先到 [http://www.ruokuai.com](http://www.ruokuai.com/login) 注册一个账号后填写到配置中
(若快已停止服务,目前只能设置**free**打码模式)
free 已对接到打码共享平台,[https://py12306-helper.pjialin.com](https://py12306-helper.pjialin.com/),欢迎参与分享
语音通知
@@ -148,7 +143,7 @@ docker-compose up -d
### 关于防封
目前查询和登录操作是分开的,查询是不依赖用户是否登录,放在 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
[![在 Gitpod 中打开](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io#https://github.com/pjialin/py12306)
@@ -156,6 +151,7 @@ docker-compose up -d
## Thanks
- 感谢大佬 [testerSunshine](https://github.com/testerSunshine/12306),借鉴了部分实现
- 感谢所有提供 pr 的大佬
- 感谢大佬 [zhaipro](https://github.com/zhaipro/easy12306) 的验证码本地识别模型与算法
## License

View File

@@ -2218,3 +2218,51 @@
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

View File

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
version: "3"
version: "2"
services:
py12306:
build: .

182
env.docker.py.example Normal file
View File

@@ -0,0 +1,182 @@
# -*- coding: utf-8 -*-
# 12306 账号
USER_ACCOUNTS = [
# 目前已支持仅查询,不下单,屏蔽掉下面的账号即可
{
'key': 0, # 如使用多个账号 key 不能重复
'user_name': 'your user name',
'password': '忽略',
'type': 'qr' # qr 为扫码登录,填写其他为密码登录
},
# {
# 'key': 'wangwu',
# 'user_name': 'wangwu@qq.com',
# 'password': 'wangwu',
# 'type': ''
# }
]
# 查询间隔(指每一个任务中每一个日期的间隔 / 单位秒)
# 默认取间隔/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
# 1https://t.me/notificationme_bot
# 2https://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': []
# }
]

View File

@@ -6,12 +6,14 @@ USER_ACCOUNTS = [
{
'key': 0, # 如使用多个账号 key 不能重复
'user_name': 'your user name',
'password': 'your password'
'password': '忽略',
'type': 'qr' # qr 为扫码登录,填写其他为密码登录
},
# {
# 'key': 'wangwu',
# 'user_name': 'wangwu@qq.com',
# 'password': 'wangwu'
# 'password': 'wangwu',
# 'type': ''
# }
]
@@ -28,7 +30,8 @@ QUERY_JOB_THREAD_ENABLED = 0 # 是否开启多线程查询,开启后第个任
# 打码平台账号
# 目前只支持免费打码接口 和 若快打码注册地址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 可用省略
'user': 'your user name',
'pwd': 'your password'
@@ -109,14 +112,19 @@ 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
"2019-01-25",
"2019-01-26",
"2020-01-25",
"2020-01-26",
],
'stations': { # 车站 支持多个车站同时查询 :Dict or :List
'left': '北京',
@@ -132,7 +140,7 @@ QUERY_JOBS = [
# }],
'members': [ # 乘客姓名,会根据当前账号自动识别乘客类型 购买儿童票 设置两个相同的姓名即可,程序会自动识别 如 ['张三', '张三']
"张三",
"王五",
#"*王五", #在姓名前加*表示学生购买成人票
# 7, # 支持通过序号确定唯一乘客,序号查看可通过 python main.py -t 登录成功之后在 runtime/user/ 下找到对应的 用户名_passengers.json 文件,找到对应的 code 填入
],
'allow_less_member': 0, # 是否允许余票不足时提交部分乘客

12
env.slave.py.example Normal file
View File

@@ -0,0 +1,12 @@
# -*- 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
View File

@@ -1,18 +1,63 @@
# -*- 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__':

View File

View File

@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
import os
import signal
import sys
@@ -11,16 +10,19 @@ from py12306.log.order_log import OrderLog
def app_available_check():
# return True # Debug
if Config().IS_DEBUG:
return True
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()
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:
open_time += datetime.timedelta(1)
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
@@ -81,7 +83,7 @@ class App:
@classmethod
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'):
return False
return True

View File

View File

@@ -1,61 +0,0 @@
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)

View File

@@ -1,201 +0,0 @@
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

View File

@@ -1,35 +0,0 @@
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})

View File

@@ -1,11 +0,0 @@
from py12306.lib.helper import ShareInstance
class User(ShareInstance):
@classmethod
def task_user_login(cls, task: dict):
pass
pass

View File

@@ -22,6 +22,8 @@ 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
@@ -94,6 +96,10 @@ 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
@@ -213,6 +219,10 @@ 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 = []

View File

@@ -2,7 +2,7 @@ import math
import random
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.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':
if Config().AUTO_CODE_PLATFORM == 'free' or Config().AUTO_CODE_PLATFORM == 'user':
return self.get_image_by_free_site(img)
return self.get_img_position_by_ruokuai(img)
@@ -56,22 +56,16 @@ class OCR:
def get_image_by_free_site(self, img):
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()
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(',')
if result.get('msg') == 'success':
pos = result.get('result')
return self.get_image_position_by_offset(pos)
CommonLog.print_auto_code_fail(CommonLog.MESSAGE_GET_RESPONSE_FROM_FREE_AUTO_CODE)
return None

View File

@@ -1,6 +1,4 @@
# coding=utf-8
# 查询余票
import time
HOST_URL_OF_12306 = 'kyfw.12306.cn'
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_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 = {
'url': BASE_URL_OF_12306 + '/passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&_={random}'
}
@@ -40,14 +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_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 = 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_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'

View File

@@ -33,6 +33,7 @@ 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
@@ -46,6 +47,7 @@ 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)

View File

@@ -33,6 +33,11 @@ class Notification():
self = cls()
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
def send_to_telegram(cls, content=''):
self = cls()
@@ -125,15 +130,55 @@ 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()
except Exception as e:
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):
from dingtalkchatbot.chatbot import DingtalkChatbot
webhook = Config().DINGTALK_WEBHOOK

106
py12306/helpers/qrcode.py Normal file
View 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)

View File

@@ -77,3 +77,18 @@ 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

View File

View File

@@ -1,11 +0,0 @@
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'

View File

@@ -1,6 +0,0 @@
class RetryException(Exception):
pass
class MaxRetryException(Exception):
pass

View File

@@ -1,83 +0,0 @@
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(':', ''))

View File

@@ -1,70 +0,0 @@
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}

View File

@@ -1,27 +0,0 @@
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

View File

@@ -1,68 +0,0 @@
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

View File

@@ -35,6 +35,8 @@ class CommonLog(BaseLog):
MESSAGE_SEND_EMAIL_SUCCESS = '邮件发送成功,请检查收件箱'
MESSAGE_SEND_EMAIL_FAIL = '邮件发送失败,请手动检查配置,错误原因 {}'
MESSAGE_SEND_EMAIL_WITH_QRCODE_SUCCESS = '二维码邮件发送成功,请检查收件箱扫描登陆'
MESSAGE_SEND_TELEGRAM_SUCCESS = 'Telegram推送成功'
MESSAGE_SEND_TELEGRAM_FAIL = 'Telegram推送失败错误原因 {}'
@@ -139,3 +141,10 @@ 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

View File

@@ -13,6 +13,9 @@ class UserLog(BaseLog):
MESSAGE_DOWNLAODING_THE_CODE = '正在下载验证码...'
MESSAGE_CODE_AUTH_FAIL = '验证码验证失败 错误原因: {}'
MESSAGE_CODE_AUTH_SUCCESS = '验证码验证成功 开始登录...'
MESSAGE_QRCODE_DOWNLOADING = '正在下载二维码...'
MESSAGE_QRCODE_DOWNLOADED = '二维码保存在: {},请使用手机客户端扫描'
MESSAGE_QRCODE_FAIL = '二维码获取失败: {}, {} 秒后重试'
MESSAGE_LOGIN_FAIL = '登录失败 错误原因: {}'
MESSAGE_LOADED_USER = '正在尝试恢复用户: {}'
MESSAGE_LOADED_USER_SUCCESS = '用户恢复成功: {}'

View File

@@ -1,6 +1,9 @@
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 *
@@ -10,6 +13,73 @@ 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:
"""
处理下单
@@ -41,6 +111,7 @@ class Order:
assert isinstance(user, UserJob)
self.query_ins = query
self.user_ins = user
self.is_slide = False
self.make_passenger_ticket_str()
@@ -63,10 +134,25 @@ class Order:
return self.order_did_success()
elif not order_request_res:
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
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
order_id = self.query_order_wait_time()
if order_id: # 发送通知
self.order_id = order_id
@@ -85,7 +171,8 @@ 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)
@@ -100,7 +187,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':
@@ -138,7 +225,7 @@ class Order:
}
response = self.session.post(API_SUBMIT_ORDER_REQUEST, data)
result = response.json()
if result.get('data') == 'N':
if result.get('data') == '0':
OrderLog.add_quick_log(OrderLog.MESSAGE_SUBMIT_ORDER_REQUEST_SUCCESS).flush()
return True
else:
@@ -152,7 +239,7 @@ class Order:
result.get('messages', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR))).flush()
return False
def check_order_info(self):
def check_order_info(self, slide_info=None):
"""
cancel_flag=2
bed_level_order_num=000000000000000000000000000000
@@ -175,6 +262,12 @@ 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'): # 成功
@@ -251,7 +344,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
@@ -374,10 +467,12 @@ class Order:
elif 'waitTime' in result_data:
# 计算等待时间
wait_time = int(result_data.get('waitTime'))
if wait_time == -1 or wait_time == -100: # 成功
if wait_time == -1: # 成功
# /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),
@@ -429,11 +524,12 @@ 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_'.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'],
passenger_name=passenger['name'],
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)

View File

@@ -1,5 +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
@@ -66,6 +66,8 @@ 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
@@ -136,11 +138,29 @@ 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:
return date_query.strftime("%Y-%m-%d")
def query_by_date(self, date):
"""
通过日期进行查询
:return:
"""
date = 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,

View File

@@ -1,3 +1,4 @@
from base64 import b64decode
from py12306.config import Config
from py12306.cluster.cluster import Cluster
from py12306.app import app_available_check
@@ -5,7 +6,7 @@ from py12306.helpers.func import *
from py12306.helpers.request import Request
from py12306.log.query_log import QueryLog
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
@@ -29,6 +30,7 @@ class Query:
def __init__(self):
self.session = Request()
self.request_device_id()
self.cluster = Cluster()
self.update_query_interval()
self.update_query_jobs()
@@ -117,6 +119,32 @@ class Query:
self.jobs.append(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
def wait_for_ready(cls):
self = cls()
@@ -154,6 +182,9 @@ 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):

View File

@@ -1,4 +1,4 @@
import json
import base64
import pickle
import re
from os import path
@@ -11,6 +11,7 @@ from py12306.helpers.event import Event
from py12306.helpers.func import *
from py12306.helpers.request import Request
from py12306.helpers.type import UserType
from py12306.helpers.qrcode import print_qrcode
from py12306.log.order_log import OrderLog
from py12306.log.user_log import UserLog
from py12306.log.common_log import CommonLog
@@ -23,6 +24,7 @@ class UserJob:
key = None
user_name = ''
password = ''
type = 'qr'
user = None
info = {} # 用户信息
last_heartbeat = None
@@ -30,6 +32,7 @@ class UserJob:
user_loaded = False # 用户是否已加载成功
passengers = []
retry_time = 3
retry_count = 0
login_num = 0 # 尝试登录次数
# Init page
@@ -51,6 +54,7 @@ class UserJob:
self.key = str(info.get('key'))
self.user_name = info.get('user_name')
self.password = info.get('password')
self.type = info.get('type')
def update_user(self):
from py12306.user.user import User
@@ -111,7 +115,10 @@ class UserJob:
if expire: UserLog.print_user_expired()
self.is_ready = False
UserLog.print_start_login(user=self)
return self.login()
if self.type == 'qr':
return self.qr_login()
else:
return self.login()
def login(self):
"""
@@ -123,9 +130,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: # 登录成功
@@ -150,6 +157,82 @@ class UserJob:
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):
response = self.session.get(API_USER_LOGIN_CHECK)
is_login = response.json().get('data.is_login', False) == 'Y'
@@ -161,7 +244,10 @@ class UserJob:
return is_login
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()
if result.get('newapptk'):
return result.get('newapptk')
@@ -181,137 +267,31 @@ class UserJob:
获取加密后的浏览器特征 ID
:return:
"""
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]
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)
self.session.cookies.update({
'RAIL_EXPIRATION': result.get('exp'),
'RAIL_DEVICEID': result.get('dfp'),
})
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 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):
"""
用户登录成功
@@ -462,7 +442,8 @@ class UserJob:
name: '项羽',
type: 1,
id_card: 0000000000000000000,
type_text: '成人'
type_text: '成人',
enc_str: 'aaaaaa'
}]
"""
self.get_user_passengers()
@@ -470,6 +451,11 @@ 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()
@@ -480,6 +466,8 @@ 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()
@@ -490,7 +478,8 @@ 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')))
'type_text': dict_find_key_by_value(UserType.dicts, int(passenger.get('passenger_type'))),
'enc_str': passenger.get('allEncStr')
}
results.append(new_member)
@@ -510,12 +499,16 @@ class UserJob:
# 系统忙,请稍后重试
if html.find('系统忙,请稍后重试') != -1:
OrderLog.add_quick_log(OrderLog.MESSAGE_REQUEST_INIT_DC_PAGE_FAIL).flush() # 重试无用,直接跳过
return False
return False, False, html
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:
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

View File

@@ -32,7 +32,7 @@ class User:
@classmethod
def run(cls):
self = cls()
app_available_check()
# app_available_check() 用户系统不休息
self.start()
pass

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 729 KiB

After

Width:  |  Height:  |  Size: 729 KiB

View File

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 136 KiB

View File

Before

Width:  |  Height:  |  Size: 775 KiB

After

Width:  |  Height:  |  Size: 775 KiB

View File

@@ -1,5 +1,4 @@
toml
-i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
-i https://pypi.tuna.tsinghua.edu.cn/simple
appdirs==1.4.3
beautifulsoup4==4.7.0
bs4==0.0.1
@@ -13,12 +12,12 @@ Flask-JWT-Extended==3.15.0
idna==2.8
itsdangerous==1.1.0
Jinja2==2.10
lxml==4.3.0
lxml==4.6.3
MarkupSafe==1.1.0
parse==1.9.0
pyee==5.0.0
PyJWT==1.7.1
pyppeteer==0.0.25
pyppeteer-box==0.0.27
pyquery==1.4.0
redis==3.0.1
requests==2.21.0
@@ -26,9 +25,10 @@ requests-html==0.9.0
six==1.12.0
soupsieve==1.6.2
tqdm==4.28.1
urllib3==1.24.1
urllib3==1.24.2
w3lib==1.19.0
websockets==7.0
Werkzeug==0.14.1
Werkzeug==0.15.5
DingtalkChatbot==1.3.0
lightpush==0.1.3
lightpush==0.1.3
pypng

View File

View File

@@ -1,15 +0,0 @@
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()

View File

@@ -1,74 +0,0 @@
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')

View File

@@ -1,9 +0,0 @@
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)

View File

@@ -1,20 +0,0 @@
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)

View File

@@ -1,5 +0,0 @@
from tests.helper import BaseTest
class TestUser(BaseTest):
pass