193 Commits

Author SHA1 Message Date
dependabot[bot]
4b9fb469cd Bump jinja2 from 2.10 to 2.11.3
Bumps [jinja2](https://github.com/pallets/jinja) from 2.10 to 2.11.3.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/master/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/2.10...2.11.3)

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-27 01:55:34 +00: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
21 changed files with 571 additions and 90 deletions

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

@@ -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 = {
'user': 'your user name',
'pwd': 'your password'

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, # 是否允许余票不足时提交部分乘客

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

@@ -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,12 +50,11 @@ API_GET_QUEUE_COUNT = BASE_URL_OF_12306 + '/otn/confirmPassenger/getQueueCount'
API_CONFIRM_SINGLE_FOR_QUEUE = BASE_URL_OF_12306 + '/otn/confirmPassenger/confirmSingleForQueue'
API_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_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

@@ -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()

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

@@ -33,8 +33,9 @@ class Request(HTMLSession):
return response
def add_response_hook(self, hook):
exist_hooks = self.hooks['response']
if not isinstance(exist_hooks, list): hooks = [exist_hooks]
hooks = self.hooks['response']
if not isinstance(hooks, list):
hooks = [hooks]
hooks.append(hook)
self.hooks['response'] = hooks
return self
@@ -76,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

@@ -94,15 +94,24 @@ class CommonLog(BaseLog):
self.add_quick_log('多线程查询: {}'.format(get_true_false_text(Config().QUERY_JOB_THREAD_ENABLED, enable, disable)))
self.add_quick_log('CDN 状态: {}'.format(get_true_false_text(Config().CDN_ENABLED, enable, disable))).flush()
self.add_quick_log('通知状态:')
self.add_quick_log(
'语音验证码: {}'.format(get_true_false_text(Config().NOTIFICATION_BY_VOICE_CODE, enable, disable)))
self.add_quick_log('邮件通知: {}'.format(get_true_false_text(Config().EMAIL_ENABLED, enable, disable)))
self.add_quick_log('钉钉通知: {}'.format(get_true_false_text(Config().DINGTALK_ENABLED, enable, disable)))
self.add_quick_log('Telegram通知: {}'.format(get_true_false_text(Config().TELEGRAM_ENABLED, enable, disable)))
self.add_quick_log('ServerChan通知: {}'.format(get_true_false_text(Config().SERVERCHAN_ENABLED, enable, disable)))
self.add_quick_log('Bark通知: {}'.format(get_true_false_text(Config().BARK_ENABLED, enable, disable)))
self.add_quick_log(
'PushBear通知: {}'.format(get_true_false_text(Config().PUSHBEAR_ENABLED, enable, disable))).flush(sep='\t\t')
if Config().NOTIFICATION_BY_VOICE_CODE:
self.add_quick_log(
'语音验证码: {}'.format(get_true_false_text(Config().NOTIFICATION_BY_VOICE_CODE, enable, disable)))
if Config().EMAIL_ENABLED:
self.add_quick_log('邮件通知: {}'.format(get_true_false_text(Config().EMAIL_ENABLED, enable, disable)))
if Config().DINGTALK_ENABLED:
self.add_quick_log('钉钉通知: {}'.format(get_true_false_text(Config().DINGTALK_ENABLED, enable, disable)))
if Config().TELEGRAM_ENABLED:
self.add_quick_log('Telegram通知: {}'.format(get_true_false_text(Config().TELEGRAM_ENABLED, enable, disable)))
if Config().SERVERCHAN_ENABLED:
self.add_quick_log(
'ServerChan通知: {}'.format(get_true_false_text(Config().SERVERCHAN_ENABLED, enable, disable)))
if Config().BARK_ENABLED:
self.add_quick_log('Bark通知: {}'.format(get_true_false_text(Config().BARK_ENABLED, enable, disable)))
if Config().PUSHBEAR_ENABLED:
self.add_quick_log(
'PushBear通知: {}'.format(get_true_false_text(Config().PUSHBEAR_ENABLED, enable, disable)))
self.add_quick_log().flush(sep='\t\t')
self.add_quick_log('查询间隔: {}'.format(Config().QUERY_INTERVAL))
self.add_quick_log('用户心跳检测间隔: {}'.format(Config().USER_HEARTBEAT_INTERVAL))
self.add_quick_log('WEB 管理页面: {}'.format(get_true_false_text(Config().WEB_ENABLE, enable, disable)))
@@ -130,3 +139,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()
@@ -149,11 +177,14 @@ class Query:
return self.api_type
response = self.session.get(API_QUERY_INIT_PAGE)
if response.status_code == 200:
res = re.search(r'var CLeftTicketUrl = \'(leftTicket/queryX)\';', response.text)
res = re.search(r'var CLeftTicketUrl = \'(.*)\';', response.text)
try:
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
@@ -51,6 +53,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 +114,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):
"""
@@ -125,6 +131,7 @@ class UserJob:
}
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: # 登录成功
@@ -149,6 +156,73 @@ 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()
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()
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'
@@ -160,7 +234,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')
@@ -175,6 +252,36 @@ class UserJob:
# TODO 处理获取失败情况
return False
def request_device_id(self):
"""
获取加密后的浏览器特征 ID
:return:
"""
response = self.session.get(API_GET_BROWSER_DEVICE_ID)
if response.status_code == 200:
try:
result = json.loads(response.text)
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
}
self.session.headers.update(headers)
response = self.session.get(base64.b64decode(result['id']).decode())
if response.text.find('callbackFunction') >= 0:
result = response.text[18:-2]
result = json.loads(result)
if not Config().is_cache_rail_id_enabled():
self.session.cookies.update({
'RAIL_EXPIRATION': result.get('exp'),
'RAIL_DEVICEID': result.get('dfp'),
})
else:
self.session.cookies.update({
'RAIL_EXPIRATION': Config().RAIL_EXPIRATION,
'RAIL_DEVICEID': Config().RAIL_DEVICEID,
})
except:
return False
def login_did_success(self):
"""
用户登录成功
@@ -312,7 +419,7 @@ class UserJob:
UserLog.MESSAGE_GET_USER_PASSENGERS_FAIL.format(
result.get('messages', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR), self.retry_time)).flush()
if Config().is_slave():
self.load_user_from_remote() # 加载最新 cookie
self.load_user_from_remote() # 加载最新 cookie
stay_second(self.retry_time)
return self.get_user_passengers()
@@ -325,7 +432,8 @@ class UserJob:
name: '项羽',
type: 1,
id_card: 0000000000000000000,
type_text: '成人'
type_text: '成人',
enc_str: 'aaaaaa'
}]
"""
self.get_user_passengers()
@@ -333,6 +441,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()
@@ -343,6 +456,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()
@@ -353,7 +468,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)
@@ -373,12 +489,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

@@ -1,4 +1,4 @@
-i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
-i https://pypi.tuna.tsinghua.edu.cn/simple
appdirs==1.4.3
beautifulsoup4==4.7.0
bs4==0.0.1
@@ -11,13 +11,13 @@ Flask==1.0.2
Flask-JWT-Extended==3.15.0
idna==2.8
itsdangerous==1.1.0
Jinja2==2.10
lxml==4.3.0
Jinja2==2.11.3
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
@@ -25,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