From 808c30208865ee23521c812057449456766b7d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=87=E8=B4=A4=E5=B9=B3?= Date: Sun, 1 Sep 2019 12:41:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=80=99=E8=A1=A5=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 6 +- TickerConfig.py | 119 ++++++++++++++++++++++ UnitTest/TestAll.py | 1 - Update.md | 6 ++ config/TicketEnmu.py | 3 +- config/emailConf.py | 58 +++++------ config/pushbearConf.py | 10 +- config/ticketConf.py | 23 ----- config/ticket_config.yaml | 142 -------------------------- config/urlConf.py | 63 +++++++++++- init/login.py | 7 +- init/select_ticket_info.py | 175 +++++++++++--------------------- inter/AutoSubmitOrderRequest.py | 3 +- inter/ChechFace.py | 37 +++++++ inter/ConfirmHB.py | 43 ++++++++ inter/ConfirmSingleForQueue.py | 3 +- inter/GetPassengerDTOs.py | 52 ++++++---- inter/GetQueueCount.py | 35 ++++++- inter/GetQueueCountAsync.py | 8 +- inter/GetRandCode.py | 9 +- inter/GetSuccessRate.py | 41 ++++++++ inter/Query.py | 165 +++++++++++++----------------- inter/SubmitOrderRequest.py | 45 +++++++- tkcode.png | Bin 16002 -> 13988 bytes 25 files changed, 588 insertions(+), 467 deletions(-) create mode 100644 TickerConfig.py delete mode 100755 config/ticketConf.py delete mode 100755 config/ticket_config.yaml create mode 100644 inter/ChechFace.py create mode 100644 inter/ConfirmHB.py create mode 100644 inter/GetSuccessRate.py diff --git a/.gitignore b/.gitignore index 3ff2ada..467b159 100755 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ *.log .idea/ *.h5 +tkcode.png \ No newline at end of file diff --git a/README.md b/README.md index e97c88e..4ec24c9 100755 --- a/README.md +++ b/README.md @@ -18,13 +18,13 @@ - 非root用户(避免安装和运行时使用了不同环境): `pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt` #### 项目使用说明 - - 可以配置邮箱,可以配置可以不配置,配置邮箱的格式在[yaml](config/ticket_config.yaml)里面可以看到ex + - 可以配置邮箱,可以配置可以不配置,配置邮箱的格式在[配置](TickerConfig.py)里面可以看到ex - 可以配置server酱提醒(推荐), [配置教程](https://www.jianshu.com/p/8d10b5b9c4e3) - - 配置[yaml](config/ticket_config.yaml)文件的时候,需注意空格和遵循yaml语法格式 + - 配置[配置](TickerConfig.py)文件的时候,需注意空格和遵循yaml语法格式 #### 项目开始 - 服务器启动: - - 修改[config/ticket_config.yaml](config/ticket_config.yaml)文件,按照提示更改自己想要的信息 + - 修改[配置](TickerConfig.py)文件,按照提示更改自己想要的信息 - 运行根目录`sudo python run.py`,即可开始 - 由于新增对时功能,请务必用**sudo,sudo,sudo** 执行,否则会报权限错误,windows打开ide或者cmd请用管理员身份执行`python run.py`,不需要加`sudo` - 如果你的服务器安装了docker与docker-compose, 那么就可以通过`docker-compose`进行启动,`docker.sh`脚本对此进行了封装,可以通过如下命令进行启动 diff --git a/TickerConfig.py b/TickerConfig.py new file mode 100644 index 0000000..2603a34 --- /dev/null +++ b/TickerConfig.py @@ -0,0 +1,119 @@ +# 刷票模式:1=刷票 2=候补 +TICKET_TYPE = 2 + +# 候补最晚兑现日期,如果是候补订单,这个值一定要填 +# 格式为日期+小时+分 +# t("#fromDate").val() + "#" + t("#dafaultTime").html().replace("时", "") + "#" + t("#dafaultMinutes").html().replace("分", ""), +J_Z_PARAM = "2019-09-10#22#59" + +# 出发日期(list) "2018-01-06", "2018-01-07" +STATION_DATES = [ + "2019-09-30" +] + +# 填入需要购买的车次(list),"G1353" +STATION_TRAINS = [ + "G1377" +] + +# 出发城市,比如深圳北,就填深圳就搜得到 +FROM_STATION = "上海" + +# 到达城市 比如深圳北,就填深圳就搜得到 +TO_STATION = "长沙" + +# 座位(list) 多个座位ex: +# - "商务座" +# - "一等座" +# - "二等座" +# - "特等座" +# - "软卧" +# - "硬卧" +# - "硬座" +# - "无座" +# - "动卧" +SET_TYPE = [ + "二等座" +] + +# 当余票小于乘车人,如果选择优先提交,则删减联系人和余票数一致在提交 +# bool +IS_MORE_TICKET = False + +# 乘车人(list) 多个乘车人ex: +# - "张三" +# - "李四" +TICKET_PEOPLES = [ + "" +] + +# 12306登录账号 +USER = "" +PWD = "" + +# 加入小黑屋时间默认为5分钟,此功能为了防止僵尸票导致一直下单不成功错过正常的票 +TICKET_BLACK_LIST_TIME = 5 + +# 自动打码 +IS_AUTO_CODE = True + +# 邮箱配置,如果抢票成功,将通过邮件配置通知给您 +# 列举163 +# email: "xxx@163.com" +# notice_email_list: "123@qq.com" +# username: "xxxxx" +# password: "xxxxx +# host: "smtp.163.com" +# 列举qq ,qq设置比较复杂,需要在邮箱-->账户-->开启smtp服务,取得授权码==邮箱登录密码 +# email: "xxx@qq.com" +# notice_email_list: "123@qq.com" +# username: "xxxxx" +# password: "授权码" +# host: "smtp.qq.com" +EMAIL_CONF = { + "IS_MAIL": True, + "email": "", + "notice_email_list": "", + "username": "", + "password": "", + "host": "", +} + +# 是否开启 pushbear 微信提醒, 使用前需要前往 http://pushbear.ftqq.com 扫码绑定获取 send_key 并关注获得抢票结果通知的公众号 +PUSHBEAR_CONF = { + "is_pushbear": False, + "send_key": "" +} + +# 是否开启cdn查询,可以更快的检测票票 1为开启,2为关闭 +IS_CDN = 1 + +# 下单接口分为两种,1 模拟网页自动捡漏下单(不稳定),2 模拟车次后面的购票按钮下单(稳如老狗) +ORDER_TYPE = 2 + +# 下单模式 1 为预售,整点刷新,刷新间隔0.1-0.5S, 然后会校验时间,比如12点的预售,那脚本就会在12.00整检票,刷新订单 +# 2 是捡漏,捡漏的刷新间隔时间为0.5-3秒,时间间隔长,不容易封ip +ORDER_MODEL = 2 + +# 是否开启代理, 0代表关闭, 1表示开始 +# 开启此功能的时候请确保代理ip是否可用,在测试放里面经过充分的测试,再开启此功能,不然可能会耽误你购票的宝贵时间 +# 使用方法: +# 1、在agency/proxy_list列表下填入代理ip +# 2、测试UnitTest/TestAll/testProxy 测试代理是否可以用 +# 3、开启代理ip +IS_PROXY = 0 + +# 预售放票时间, 如果是捡漏模式,可以忽略此操作 +OPEN_TIME = "13:00:00" + +PASSENGER_TICKER_STR = { + '一等座': 'M', + '特等座': 'P', + '二等座': 'O', + '商务座': 9, + '硬座': 1, + '无座': 1, + '软座': 2, + '软卧': 4, + '硬卧': 3, +} diff --git a/UnitTest/TestAll.py b/UnitTest/TestAll.py index 103cf27..74ff210 100644 --- a/UnitTest/TestAll.py +++ b/UnitTest/TestAll.py @@ -53,6 +53,5 @@ class testAll(unittest.TestCase): # :return: # """ - if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/Update.md b/Update.md index 41b4cc3..7448950 100644 --- a/Update.md +++ b/Update.md @@ -157,3 +157,9 @@ - 删除若快打码 - 修复不能下单问题 - 放弃支持python2.7,只支持3.6以上版本 + +- 2019.09.01更新 + - 去除yaml配置文件,改为py文件配置 + - 增加候补订单功能 + - 新增TICKET_TYPE字段,1=刷票 2=候补 + - 目前候补只支持单车次,多乘车人候补,由于目前不是很懂候补的需求,所以暂时这样做 diff --git a/config/TicketEnmu.py b/config/TicketEnmu.py index d960282..9e9f294 100644 --- a/config/TicketEnmu.py +++ b/config/TicketEnmu.py @@ -1,4 +1,5 @@ # coding=utf-8 +from enum import Enum class ticket(object): @@ -25,6 +26,7 @@ class ticket(object): OUT_NUM = 120 # 排队请求12306的次数 WAIT_OUT_NUM = u"超出排队时间,自动放弃,正在重新刷票" WAIT_ORDER_SUCCESS = u"恭喜您订票成功,订单号为:{0}, 请立即打开浏览器登录12306,访问‘未完成订单’,在30分钟内完成支付!" + WAIT_AFTER_NATE_SUCCESS = u"候补订单已完成,请立即打开浏览器登录12306,访问‘候补订单’,在30分钟内完成支付!" WAIT_ORDER_CONTINUE = u"排队等待时间预计还剩 {0} ms" WAIT_ORDER_FAIL = u"排队等待失败,错误消息:{0}" WAIT_ORDER_NUM = u"第{0}次排队中,请耐心等待" @@ -37,4 +39,3 @@ class ticket(object): REST_TIME_PAST = u"休息时间已过,重新开启检票功能" LOGIN_SESSION_FAIL = u"用户检查失败:{0},可能未登录,可能session已经失效, 正在重新登录中" - diff --git a/config/emailConf.py b/config/emailConf.py index 514f1c8..efc132e 100755 --- a/config/emailConf.py +++ b/config/emailConf.py @@ -1,10 +1,9 @@ # -*- coding: utf8 -*- import socket - __author__ = 'MR.wen' +import TickerConfig from email.header import Header from email.mime.text import MIMEText -from config.ticketConf import _get_yaml import smtplib @@ -14,38 +13,33 @@ def sendEmail(msg): :param str: email content :return: """ - email_conf = _get_yaml() - is_email = email_conf["email_conf"]["is_email"] - if is_email: + try: + sender = TickerConfig.EMAIL_CONF["email"] + receiver = TickerConfig.EMAIL_CONF["notice_email_list"] + subject = '恭喜,您已订票成功' + username = TickerConfig.EMAIL_CONF["username"] + password = TickerConfig.EMAIL_CONF["password"] + host = TickerConfig.EMAIL_CONF["host"] + s = "{0}".format(msg) + + msg = MIMEText(s, 'plain', 'utf-8') # 中文需参数‘utf-8’,单字节字符不需要 + msg['Subject'] = Header(subject, 'utf-8') + msg['From'] = sender + msg['To'] = receiver + try: - sender = email_conf["email_conf"]["email"] - receiver = email_conf["email_conf"]["notice_email_list"] - subject = '恭喜,您已订票成功' - username = email_conf["email_conf"]["username"] - password = email_conf["email_conf"]["password"] - host = email_conf["email_conf"]["host"] - s = "{0}".format(msg) - - msg = MIMEText(s, 'plain', 'utf-8') # 中文需参数‘utf-8’,单字节字符不需要 - msg['Subject'] = Header(subject, 'utf-8') - msg['From'] = sender - msg['To'] = receiver - - try: - smtp = smtplib.SMTP_SSL() - smtp.connect(host) - except socket.error: - smtp = smtplib.SMTP() - smtp.connect(host) + smtp = smtplib.SMTP_SSL() smtp.connect(host) - smtp.login(username, password) - smtp.sendmail(sender, receiver.split(","), msg.as_string()) - smtp.quit() - print(u"邮件已通知, 请查收") - except Exception as e: - print(u"邮件配置有误{}".format(e)) - else: - pass + except socket.error: + smtp = smtplib.SMTP() + smtp.connect(host) + smtp.connect(host) + smtp.login(username, password) + smtp.sendmail(sender, receiver.split(","), msg.as_string()) + smtp.quit() + print(u"邮件已通知, 请查收") + except Exception as e: + print(u"邮件配置有误{}".format(e)) if __name__ == '__main__': diff --git a/config/pushbearConf.py b/config/pushbearConf.py index bc8576c..115fdfc 100644 --- a/config/pushbearConf.py +++ b/config/pushbearConf.py @@ -1,8 +1,5 @@ # -*- coding: utf8 -*- -import time - -import requests -from config.ticketConf import _get_yaml +import TickerConfig from config.urlConf import urls from myUrllib.httpUtils import HTTPClient @@ -15,12 +12,11 @@ def sendPushBear(msg): :param str: 通知内容 content :return: """ - conf = _get_yaml() - if conf["pushbear_conf"]["is_pushbear"] and conf["pushbear_conf"]["send_key"].strip() != "": + if TickerConfig.PUSHBEAR_CONF["is_pushbear"] and TickerConfig.PUSHBEAR_CONF["pushbear_conf"]["send_key"].strip() != "": try: sendPushBearUrls = urls.get("Pushbear") data = { - "sendkey": conf["pushbear_conf"]["send_key"].strip(), + "sendkey": TickerConfig.PUSHBEAR_CONF["pushbear_conf"]["send_key"].strip(), "text": "易行购票成功通知", "desp": msg } diff --git a/config/ticketConf.py b/config/ticketConf.py deleted file mode 100755 index b84c89b..0000000 --- a/config/ticketConf.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf8 -*- -__author__ = 'MR.wen' -import os -import yaml - - -def _get_yaml(): - """ - 解析yaml - :return: s 字典 - """ - path = os.path.join(os.path.dirname(__file__) + '/ticket_config.yaml') - try: # 兼容2和3版本 - with open(path, encoding="utf-8") as f: - s = yaml.load(f) - except Exception: - with open(path) as f: - s = yaml.load(f) - return s.decode() if isinstance(s, bytes) else s - - -if __name__ == '__main__': - print(_get_yaml()) \ No newline at end of file diff --git a/config/ticket_config.yaml b/config/ticket_config.yaml deleted file mode 100755 index b47ff40..0000000 --- a/config/ticket_config.yaml +++ /dev/null @@ -1,142 +0,0 @@ ---- -# 配置文件请严格遵循yaml语法格式,yaml学习地址 https://ansible-tran.readthedocs.io/en/latest/docs/YAMLSyntax.html -set: - # 出发日期(list),格式ex: - # - 2018-01-06 - # - 2018-01-07 - station_dates: - - "2019-01-18" - - # 是否根据时间范围 和 乘车类型 购票 - # 否则将需要手动填写车次 - is_by_time: False - - # 列车类型: 高铁 G 动车 D 其它火车 O - train_types: [G,D,O] - - # 可接受最早出发时间 格式ex: - # departure_time: "8:00" - departure_time: "00:00" - - # 可接受最晚抵达时间 格式ex: - # arrival_time: "16:00" - arrival_time: "24:00" - - # 可接受最长旅途时间 格式ex: - # take_time: "24:00" - take_time: "24:00" - - # 填入需要购买的车次(list),格式ex: - # - "G1353" - # - "G1329" - station_trains: - - "G6153" - - "G6184" - - "G6173" - - # 出发城市,比如深圳北,就填深圳就搜得到 - from_station: "邵阳" - - # 到达城市 比如深圳北,就填深圳就搜得到 - to_station: "深圳北" - - # 座位(list) 多个座位ex: - # - "商务座" - # - "一等座" - # - "二等座" - # - "特等座" - # - "软卧" - # - "硬卧" - # - "硬座" - # - "无座" - # - "动卧" - set_type: - - "二等座" - - # 当余票小于乘车人,如果选择优先提交,则删减联系人和余票数一致在提交 - is_more_ticket: False - - # 乘车人(list) 多个乘车人ex: - # - "张三" - # - "李四" - ticke_peoples: - - "" - - # 12306登录账号(list) - 12306account: - - user: "qqxin1011" - - pwd: "" - -# 加入小黑屋时间默认为5分钟,此功能为了防止僵尸票导致一直下单不成功错过正常的票, -ticket_black_list_time: 5 - -# 自动打码 -is_auto_code: True - -# 打码平台, 2 为AI识别,需将两个模型文件放到项目根目录下 -auto_code_type: 2 - -# 邮箱配置,如果抢票成功,将通过邮件配置通知给您 -# 列举163 -# email: "xxx@163.com" -# notice_email_list: "123@qq.com" -# username: "xxxxx" -# password: "xxxxx -# host: "smtp.163.com" -# 列举qq ,qq设置比较复杂,需要在邮箱-->账户-->开启smtp服务,取得授权码==邮箱登录密码 -# email: "xxx@qq.com" -# notice_email_list: "123@qq.com" -# username: "xxxxx" -# password: "授权码" -# host: "smtp.qq.com" -email_conf: - is_email: True - email: "" - notice_email_list: "" - username: "" - password: "" - host: "smtp.qq.com" - -# 是否开启 pushbear 微信提醒, 使用前需要前往 http://pushbear.ftqq.com 扫码绑定获取 send_key 并关注获得抢票结果通知的公众号 -pushbear_conf: - is_pushbear: False - send_key: "" - -# 是否开启cdn查询,可以更快的检测票票 1为开启,2为关闭 -is_cdn: 1 - -# 下单接口分为两种,1 模拟网页自动捡漏下单(不稳定),2 模拟车次后面的购票按钮下单(稳如老狗) -order_type: 2 - -# 下单模式 1 为预售,整点刷新,刷新间隔0.1-0.5S, 然后会校验时间,比如12点的预售,那脚本就会在12.00整检票,刷新订单 -# 2 是捡漏,捡漏的刷新间隔时间为0.5-3秒,时间间隔长,不容易封ip -order_model: 2 - -# 预售放票时间, 如果是捡漏模式,可以忽略此操作 -open_time: '13:00:00' - -# 是否开启代理, 0代表关闭, 1表示开始 -# 开启此功能的时候请确保代理ip是否可用,在测试放里面经过充分的测试,再开启此功能,不然可能会耽误你购票的宝贵时间 -# 使用方法: -# 1、在agency/proxy_list列表下填入代理ip -# 2、测试UnitTest/TestAll/testProxy 测试代理是否可以用 -# 3、开启代理ip -is_proxy: 0 - -# 路由器自动切换ip, 目前只支持tplink, -# is_router: 0表示打开,1表示开启 -# router_time: 以/分钟为单位 -#router_pwd: 路由器登录的密码 -#broadband_user_name: 宽带账号 -#broadband_pwd: 宽带密码 - -#is_router: 1 -#router_time: -#router_pwd: -#broadband_user_name: "" -#broadband_pwd: - -# query线程 -#query_thread: 3 - - diff --git a/config/urlConf.py b/config/urlConf.py index 5e60134..236bc65 100755 --- a/config/urlConf.py +++ b/config/urlConf.py @@ -139,7 +139,7 @@ urls = { "is_json": False, }, "getDevicesId": { # 获取用户信息 - "req_url": "/otn/HttpZF/logdevice?algID=3yxNoRW8BM&hashCode=8EFUGZrjK3cO8VdDugvPxyyiUqMNmKhl6pbW1ftnEVI&FMQw=0&q4f3=zh-CN&VPIf=1&custID=133&VEek=unknown&dzuS=0&yD16=0&EOQP=c227b88b01f5c513710d4b9f16a5ce52&lEnu=2887005765&jp76=52d67b2a5aa5e031084733d5006cc664&hAqN=MacIntel&platform=WEB&ks0Q=d22ca0b81584fbea62237b14bd04c866&TeRS=1013x1920&tOHY=24xx1080x1920&Fvje=i1l1o1s1&q5aJ=-8&wNLf=99115dfb07133750ba677d055874de87&0aew=Mozilla/5.0%20(Macintosh;%20Intel%20Mac%20OS%20X%2010_14_4)%20AppleWebKit/537.36%20(KHTML,%20like%20Gecko)%20Chrome/74.0.3729.131%20Safari/537.36&E3gR=d4c1ccb1725a4a45cc350f16ac26f32b×tamp={0}", + "req_url": "/otn/HttpZF/logdevice?algID=YD9Iw7QM4u&hashCode=d7vhohETY2f2TpCef2MPZFvngSXyZU71bSRYvkHTkbc&FMQw=0&q4f3=zh-CN&VySQ=FGFC5l5w_W3LWESYu2oI4qV2jIzzka61&VPIf=1&custID=133&VEek=unknown&dzuS=0&yD16=0&EOQP=c227b88b01f5c513710d4b9f16a5ce52&lEnu=3232235624&jp76=52d67b2a5aa5e031084733d5006cc664&hAqN=MacIntel&platform=WEB&ks0Q=d22ca0b81584fbea62237b14bd04c866&TeRS=831x1440&tOHY=24xx900x1440&Fvje=i1l1o1s1&q5aJ=-8&wNLf=99115dfb07133750ba677d055874de87&0aew=Mozilla/5.0%20(Macintosh;%20Intel%20Mac%20OS%20X%2010_13_4)%20AppleWebKit/537.36%20(KHTML,%20like%20Gecko)%20Chrome/76.0.3809.132%20Safari/537.36&E3gR=ab86d46d16b9293beca4799ff15c5db1×tamp={0}", "req_type": "get", "Referer": "https://kyfw.12306.cn/otn/passport?redirect=/otn/", "Host": "kyfw.12306.cn", @@ -504,4 +504,65 @@ urls = { }, + + # 候补订单接口 + + "chechFace": { # 人脸识别 + "req_url": "/otn/afterNate/chechFace", + "req_type": "post", + "Referer": "https://kyfw.12306.cn/otn/leftTicket/init", + "Host": "kyfw.12306.cn", + "re_try": 10, + "re_time": 0.01, + "s_time": 0.01, + "is_logger": True, + "is_json": True, + }, + "getSuccessRate": { # 成功信息 + "req_url": "/otn/afterNate/getSuccessRate", + "req_type": "post", + "Referer": "https://kyfw.12306.cn/otn/leftTicket/init", + "Host": "kyfw.12306.cn", + "re_try": 10, + "re_time": 0.01, + "s_time": 0.01, + "is_logger": True, + "is_json": True, + }, + "SubmitOrderRequestRsp": { # 提交候补订单准备 + "req_url": "/otn/afterNate/submitOrderRequest", + "req_type": "post", + "Referer": "https://kyfw.12306.cn/otn/leftTicket/init", + "Host": "kyfw.12306.cn", + "re_try": 10, + "re_time": 0.01, + "s_time": 0.01, + "is_logger": True, + "is_json": True, + }, + "confirmHB": { # 设置订单信息 + "req_url": "/otn/afterNate/confirmHB", + "req_type": "post", + "Referer": "https://kyfw.12306.cn/otn/leftTicket/init", + "Host": "kyfw.12306.cn", + "re_try": 10, + "re_time": 0.01, + "s_time": 0.01, + "is_logger": True, + "is_json": True, + }, + "queryQueue": { # 排队 + "req_url": "/otn/afterNate/queryQueue", + "req_type": "post", + "Referer": "https://kyfw.12306.cn/otn/leftTicket/init", + "Host": "kyfw.12306.cn", + "re_try": 10, + "re_time": 0.01, + "s_time": 0.01, + "is_logger": True, + "is_json": True, + }, + + + } \ No newline at end of file diff --git a/init/login.py b/init/login.py index f55179c..ef2f77e 100755 --- a/init/login.py +++ b/init/login.py @@ -3,8 +3,7 @@ import copy import time from collections import OrderedDict from time import sleep - -from config.ticketConf import _get_yaml +import TickerConfig from inter.GetPassCodeNewOrderAndLogin import getPassCodeNewOrderAndLogin1 from inter.GetRandCode import getRandCode from inter.LoginAysnSuggest import loginAysnSuggest @@ -126,7 +125,7 @@ class GoLogin: :param passwd: 密码 :return: """ - user, passwd = _get_yaml()["set"]["12306account"][0]["user"], _get_yaml()["set"]["12306account"][1]["pwd"] + user, passwd = TickerConfig.USER, TickerConfig.PWD if not user or not passwd: raise UserPasswordException(u"温馨提示: 用户名或者密码为空,请仔细检查") login_num = 0 @@ -138,7 +137,7 @@ class GoLogin: devicesIdUrl["req_url"] = devicesIdUrl["req_url"].format(int(time.time() * 1000)) # devicesIdRsp = self.session.httpClint.send(devicesIdUrl) # devicesId = eval(devicesIdRsp.split("(")[1].split(")")[0].replace("'", ""))["dfp"] - devicesId = "UysLb2cYwsVjyInSzZ0pGOmYplvokmhBjoGNjrinquaUD0id7gkifgF6FvM2TRCL7Df89GZL1lVV763tGhiPhxlNdlE7iQkk496KUGCFZyyWxE4d0XjyHYv9DlsXfKTlrd8RBUdYIYjmWBXWMN65ElDQiO_Rnrul" + devicesId = "K1OnaaicUR1DrGl2vRS1HrLLna8UBoXkESCnuPMBzVtrO6fG4URi2RWJHpM7urYlYx-fpp0AeM4Ca8rNN4WyYv1X493VsH5yejsLNol7XZ74gRp8yE7eEDHYU87t1urn3Oeaifrjrd5FRTmk3WCNylKeE2UQhPRH" if devicesId: self.session.httpClint.set_cookies(RAIL_DEVICEID=devicesId) diff --git a/init/select_ticket_info.py b/init/select_ticket_info.py index 6b08c86..7e8f33c 100755 --- a/init/select_ticket_info.py +++ b/init/select_ticket_info.py @@ -6,16 +6,15 @@ import socket import sys import threading import time - +import TickerConfig import wrapcache - from agency.cdn_utils import CDNProxy from config import urlConf, configCommon from config.TicketEnmu import ticket -from config.configCommon import seat_conf, checkDate, seat_conf_2 -from config.ticketConf import _get_yaml +from config.configCommon import seat_conf_2, seat_conf from init.login import GoLogin from inter.AutoSubmitOrderRequest import autoSubmitOrderRequest +from inter.ChechFace import chechFace from inter.CheckUser import checkUser from inter.GetPassengerDTOs import getPassengerDTOs from inter.LiftTicketInit import liftTicketInit @@ -27,13 +26,6 @@ from myException.ticketConfigException import ticketConfigException from myException.ticketIsExitsException import ticketIsExitsException from myException.ticketNumOutException import ticketNumOutException from myUrllib.httpUtils import HTTPClient -from utils.timeUtil import time_to_minutes, minutes_to_time - -try: - reload(sys) - sys.setdefaultencoding('utf-8') -except NameError: - pass class select: @@ -42,57 +34,25 @@ class select: """ def __init__(self): - self.from_station, self.to_station, self.station_dates, self._station_seat, self.is_more_ticket, \ - self.ticke_peoples, self.station_trains, self.ticket_black_list_time, \ - self.order_type, self.is_by_time, self.train_types, self.departure_time, \ - self.arrival_time, self.take_time, self.order_model, self.open_time, self.is_proxy = self.get_ticket_info() - self.is_auto_code = _get_yaml()["is_auto_code"] - self.auto_code_type = _get_yaml()["auto_code_type"] - self.is_cdn = _get_yaml()["is_cdn"] - self.httpClint = HTTPClient(self.is_proxy) + self.get_ticket_info() + self._station_seat = [seat_conf[x] for x in TickerConfig.SET_TYPE] + self.auto_code_type = 2 + self.httpClint = HTTPClient(TickerConfig.IS_PROXY) self.urls = urlConf.urls - self.login = GoLogin(self, self.is_auto_code, self.auto_code_type) + self.login = GoLogin(self, TickerConfig.IS_AUTO_CODE, self.auto_code_type) self.cdn_list = [] - self.queryUrl = "leftTicket/query" + self.queryUrl = "leftTicket/queryT" self.passengerTicketStrList = "" + self.passengerTicketStrByAfterLate = "" self.oldPassengerStr = "" self.set_type = "" - def get_ticket_info(self): + @staticmethod + def get_ticket_info(): """ 获取配置信息 :return: """ - ticket_info_config = _get_yaml() - from_station = ticket_info_config["set"]["from_station"] - to_station = ticket_info_config["set"]["to_station"] - station_dates = checkDate(ticket_info_config["set"]["station_dates"]) - - set_names = ticket_info_config["set"]["set_type"] - try: - set_type = [seat_conf[x.encode("utf-8")] for x in ticket_info_config["set"]["set_type"]] - except KeyError: - set_type = [seat_conf[x] for x in ticket_info_config["set"]["set_type"]] - is_more_ticket = ticket_info_config["set"]["is_more_ticket"] - ticke_peoples = ticket_info_config["set"]["ticke_peoples"] - station_trains = ticket_info_config["set"]["station_trains"] - ticket_black_list_time = ticket_info_config["ticket_black_list_time"] - order_type = ticket_info_config["order_type"] - - # by time - is_by_time = ticket_info_config["set"]["is_by_time"] - train_types = ticket_info_config["set"]["train_types"] - departure_time = time_to_minutes(ticket_info_config["set"]["departure_time"]) - arrival_time = time_to_minutes(ticket_info_config["set"]["arrival_time"]) - take_time = time_to_minutes(ticket_info_config["set"]["take_time"]) - - # 下单模式 - order_model = ticket_info_config["order_model"] - open_time = ticket_info_config["open_time"] - - # 代理模式 - is_proxy = ticket_info_config["is_proxy"] - print(u"*" * 50) print(u"检查当前python版本为:{},目前版本只支持3.6以上".format(sys.version.split(" ")[0])) print(u"12306刷票小助手,最后更新于2019.01.08,请勿作为商业用途,交流群号:286271084(已满)," @@ -103,31 +63,10 @@ class select: u" 6群: 444101020(未满)\n" u" 7群: 660689659(未满)\n" ) - if is_by_time: - method_notie = u"购票方式:根据时间区间购票\n可接受最早出发时间:{0}\n可接受最晚抵达时间:{1}\n可接受最长旅途时间:{2}\n可接受列车类型:{3}\n" \ - .format(minutes_to_time(departure_time), minutes_to_time(arrival_time), minutes_to_time(take_time), - " , ".join(train_types)) - else: - method_notie = u"购票方式:根据候选车次购买\n候选购买车次:{0}".format(",".join(station_trains)) - print (u"当前配置:\n出发站:{0}\n到达站:{1}\n乘车日期:{2}\n坐席:{3}\n是否有票优先提交:{4}\n乘车人:{5}\n" \ - u"刷新间隔: 随机(1-3S)\n{6}\n僵尸票关小黑屋时长: {7}\n下单接口: {8}\n下单模式: {9}\n预售踩点时间:{10} ".format \ - ( - from_station, - to_station, - station_dates, - ",".join(set_names), - is_more_ticket, - ",".join(ticke_peoples), - method_notie, - ticket_black_list_time, - order_type, - order_model, - open_time, - )) - print (u"*" * 50) - return from_station, to_station, station_dates, set_type, is_more_ticket, ticke_peoples, station_trains, \ - ticket_black_list_time, order_type, is_by_time, train_types, departure_time, arrival_time, take_time, \ - order_model, open_time, is_proxy + print( + f"当前配置:\n出发站:{TickerConfig.FROM_STATION}\n到达站:{TickerConfig.TO_STATION}\n乘车日期:{','.join(TickerConfig.STATION_DATES)}\n坐席:{','.join(TickerConfig.SET_TYPE)}\n是否有票优先提交:{TickerConfig.IS_MORE_TICKET}\n乘车人:{TickerConfig.TICKET_PEOPLES}\n" \ + f"刷新间隔: 随机(1-3S)\n僵尸票关小黑屋时长: {TickerConfig.TICKET_BLACK_LIST_TIME}\n下单接口: {TickerConfig.ORDER_TYPE}\n下单模式: {TickerConfig.ORDER_MODEL}\n预售踩点时间:{TickerConfig.OPEN_TIME}") + print(u"*" * 50) def station_table(self, from_station, to_station): """ @@ -184,7 +123,7 @@ class select: cdn 认证 :return: """ - if self.is_cdn == 1: + if TickerConfig.IS_CDN == 1: CDN = CDNProxy() all_cdn = CDN.open_cdn_file() if all_cdn: @@ -200,7 +139,6 @@ class select: raise ticketConfigException(u"cdn列表为空,请先加载cdn") def main(self): - # autoSynchroTime() # 同步时间 self.cdn_certification() l = liftTicketInit(self) l.reqLiftTicketInit() @@ -209,23 +147,23 @@ class select: t = threading.Thread(target=check_user.sendCheckUser) t.setDaemon(True) t.start() - from_station, to_station = self.station_table(self.from_station, self.to_station) + from_station, to_station = self.station_table(TickerConfig.FROM_STATION, TickerConfig.TO_STATION) num = 0 - s = getPassengerDTOs(session=self, ticket_peoples=self.ticke_peoples) + s = getPassengerDTOs(session=self, ticket_peoples=TickerConfig.TICKET_PEOPLES) passenger = s.sendGetPassengerDTOs() wrapcache.set("user_info", passenger, timeout=9999999) while 1: try: num += 1 now = datetime.datetime.now() # 感谢群里大佬提供整点代码 - configCommon.checkSleepTime(self) # 晚上到点休眠 - if self.order_model is 1: + configCommon.checkSleepTime(self) # 晚上到点休眠 + if TickerConfig.ORDER_MODEL is 1: sleep_time_s = 0.5 sleep_time_t = 0.6 # 测试了一下有微妙级的误差,应该不影响,测试结果:2019-01-02 22:30:00.004555,预售还是会受到前一次刷新的时间影响,暂时没想到好的解决方案 - while not now.strftime("%H:%M:%S") == self.open_time: + while not now.strftime("%H:%M:%S") == TickerConfig.OPEN_TIME: now = datetime.datetime.now() - if now.strftime("%H:%M:%S") > self.open_time: + if now.strftime("%H:%M:%S") > TickerConfig.OPEN_TIME: break time.sleep(0.0001) else: @@ -234,12 +172,12 @@ class select: q = query(session=self, from_station=from_station, to_station=to_station, - from_station_h=self.from_station, - to_station_h=self.to_station, + from_station_h=TickerConfig.FROM_STATION, + to_station_h=TickerConfig.FROM_STATION, _station_seat=self._station_seat, - station_trains=self.station_trains, - station_dates=self.station_dates, - ticke_peoples_num=len(self.ticke_peoples), + station_trains=TickerConfig.STATION_TRAINS, + station_dates=TickerConfig.STATION_DATES, + ticke_peoples_num=len(TickerConfig.TICKET_PEOPLES), ) queryResult = q.sendQuery() # 查询接口 @@ -252,46 +190,55 @@ class select: leftTicket = queryResult.get("leftTicket", "") query_from_station_name = queryResult.get("query_from_station_name", "") query_to_station_name = queryResult.get("query_to_station_name", "") - is_more_ticket_num = queryResult.get("is_more_ticket_num", len(self.ticke_peoples)) + is_more_ticket_num = queryResult.get("is_more_ticket_num", len(TickerConfig.TICKET_PEOPLES)) if wrapcache.get(train_no): print(ticket.QUEUE_WARNING_MSG.format(train_no)) else: # 获取联系人 - s = getPassengerDTOs(session=self, ticket_peoples=self.ticke_peoples, - set_type=seat_conf_2[seat], + s = getPassengerDTOs(session=self, ticket_peoples=TickerConfig.TICKET_PEOPLES, + set_type="" if isinstance(seat, list) else seat_conf_2[seat], + # 候补订单需要设置多个坐席 is_more_ticket_num=is_more_ticket_num) getPassengerDTOsResult = s.getPassengerTicketStrListAndOldPassengerStr() if getPassengerDTOsResult.get("status", False): self.passengerTicketStrList = getPassengerDTOsResult.get("passengerTicketStrList", "") + self.passengerTicketStrByAfterLate = getPassengerDTOsResult.get( + "passengerTicketStrByAfterLate", "") self.oldPassengerStr = getPassengerDTOsResult.get("oldPassengerStr", "") self.set_type = getPassengerDTOsResult.get("set_type", "") # 提交订单 - if self.order_type == 1: # 快读下单 - a = autoSubmitOrderRequest(session=self, - secretStr=secretStr, - train_date=train_date, - passengerTicketStr=self.passengerTicketStrList, - oldPassengerStr=self.oldPassengerStr, - train_no=train_no, - stationTrainCode=stationTrainCode, - leftTicket=leftTicket, - set_type=self.set_type, - query_from_station_name=query_from_station_name, - query_to_station_name=query_to_station_name, - ) - a.sendAutoSubmitOrderRequest() - elif self.order_type == 2: # 普通下单 - sor = submitOrderRequest(self, secretStr, from_station, to_station, train_no, self.set_type, - self.passengerTicketStrList, self.oldPassengerStr, train_date, - self.ticke_peoples) - sor.sendSubmitOrderRequest() + # 订单分为两种,一种为抢单,一种为候补订单 + if TickerConfig.TICKET_TYPE == 1: + if TickerConfig.ORDER_TYPE == 1: # 快速下单 + a = autoSubmitOrderRequest(session=self, + secretStr=secretStr, + train_date=train_date, + passengerTicketStr=self.passengerTicketStrList, + oldPassengerStr=self.oldPassengerStr, + train_no=train_no, + stationTrainCode=stationTrainCode, + leftTicket=leftTicket, + set_type=self.set_type, + query_from_station_name=query_from_station_name, + query_to_station_name=query_to_station_name, + ) + a.sendAutoSubmitOrderRequest() + elif TickerConfig.ORDER_TYPE == 2: # 普通下单 + sor = submitOrderRequest(self, secretStr, from_station, to_station, train_no, + self.set_type, + self.passengerTicketStrList, self.oldPassengerStr, train_date, + TickerConfig.TICKET_PEOPLES) + sor.sendSubmitOrderRequest() + elif TickerConfig.TICKET_TYPE == 2: + c = chechFace(self, secretStr) + c.sendChechFace() else: random_time = round(random.uniform(sleep_time_s, sleep_time_t), 2) print(u"正在第{0}次查询 随机停留时长:{6} 乘车日期: {1} 车次:{2} 查询无票 cdn轮询IP:{4}当前cdn总数:{5} 总耗时:{3}ms".format(num, ",".join( - self.station_dates), + TickerConfig.STATION_DATES), ",".join( - self.station_trains), + TickerConfig.STATION_TRAINS), ( datetime.datetime.now() - now).microseconds / 1000, queryResult.get( diff --git a/inter/AutoSubmitOrderRequest.py b/inter/AutoSubmitOrderRequest.py index 92bdacf..8788e6a 100644 --- a/inter/AutoSubmitOrderRequest.py +++ b/inter/AutoSubmitOrderRequest.py @@ -3,7 +3,6 @@ import urllib from collections import OrderedDict from config.TicketEnmu import ticket -from config.ticketConf import _get_yaml from inter.CheckRandCodeAnsyn import checkRandCodeAnsyn from inter.GetQueueCountAsync import getQueueCountAsync from inter.GetRandCode import getRandCode @@ -104,7 +103,7 @@ class autoSubmitOrderRequest: print(u"需要验证码") print(u"正在使用自动识别验证码功能") for i in range(3): - randCode = getRandCode(is_auto_code=True, auto_code_type=_get_yaml()["auto_code_type"]) + randCode = getRandCode(is_auto_code=True, auto_code_type=2) checkcode = checkRandCodeAnsyn(self.session, randCode, "") if checkcode == 'TRUE': print(u"验证码通过,正在提交订单") diff --git a/inter/ChechFace.py b/inter/ChechFace.py new file mode 100644 index 0000000..da49f7b --- /dev/null +++ b/inter/ChechFace.py @@ -0,0 +1,37 @@ +import urllib +from collections import OrderedDict +from config.urlConf import urls +import TickerConfig +from inter.GetSuccessRate import getSuccessRate + + +class chechFace: + def __init__(self, session, secretList): + """ + 人脸识别 + """ + self.secretList = secretList + self.session = session + + def data_apr(self): + """ + secretList 9vqa9%2B%2F%2Fsdozmm22hpSeDTGqRUwSuA2D0r%2BmU%2BLZj7MK7CDuf5Ep1xpxl4Dyxfmoah%2BaB9TZSesU%0AkxBbo5oNgR1vqMfvq66VP0T7tpQtH%2BbVGBz1FolZG8jDD%2FHqnz%2FnvdBP416Og6WGS14O%2F3iBSwT8%0AkRPsNF0Vq0U082g0tlJtP%2BPn7TzW3z7TDCceMJIjFcfEOA%2BW%2BuK%2Bpy6jCQMv0TmlkXf5aKcGnE02%0APuv4I8nF%2BOWjWzv9CrJyiCZiWaXd%2Bi7p69V3a9dhF787UgS660%2BqKRFB4RLwAfic3MkAlfpGWhMY%0ACfARVQ%3D%3D#O| + _json_att + 候补一次只能补一个座位,默认取TICKET_TYPE第一个 + :return: + """ + ticker = TickerConfig.PASSENGER_TICKER_STR.get(TickerConfig.SET_TYPE[0]) + data = OrderedDict() + data["secretList"] = f"{self.secretList}#{ticker}|" + data["_json_att"] = "" + return data + + def sendChechFace(self): + chechFaceRsp = self.session.httpClint.send(urls.get("chechFace"), self.data_apr()) + if not chechFaceRsp.get("status"): + print("".join(chechFaceRsp.get("messages")) or chechFaceRsp.get("validateMessages")) + return + g = getSuccessRate(self.session, self.secretList) + g.sendSuccessRate() + + diff --git a/inter/ConfirmHB.py b/inter/ConfirmHB.py new file mode 100644 index 0000000..bc72540 --- /dev/null +++ b/inter/ConfirmHB.py @@ -0,0 +1,43 @@ +from collections import OrderedDict +from config.urlConf import urls +import TickerConfig +from inter.GetQueueCount import queryQueueByAfterNate + + +class confirmHB: + def __init__(self, secretList, session, tickerNo): + """ + 人脸识别 + """ + self.secretList = secretList + self.session = session + self.passengerTicketStrByAfterLate = session.passengerTicketStrByAfterLate + self.tickerNo = tickerNo + + def data_apr(self): + """ + passengerInfo 1#XXXX#1#***************77X#bf6ae40d3655ae7eff005ee21d95876b38ab97a8031b464bc2f74a067e3ec957; + jzParam 2019-08-31#19#00 + hbTrain 5l000G177230,O# + lkParam + :return: + """ + ticker = TickerConfig.PASSENGER_TICKER_STR.get(TickerConfig.SET_TYPE[0]) + data = OrderedDict() + data["passengerInfo"] = self.passengerTicketStrByAfterLate + data["jzParam"] = TickerConfig.J_Z_PARAM + data["hbTrain"] = f"{self.tickerNo},{ticker}#" + data["lkParam"] = "" + return data + + def sendChechFace(self): + ChechFaceRsp = self.session.httpClint.send(urls.get("confirmHB"), self.data_apr()) + if not ChechFaceRsp.get("status"): + print("".join(ChechFaceRsp.get("messages")) or ChechFaceRsp.get("validateMessages")) + return + queue = queryQueueByAfterNate(self.session) + queue.sendQueryQueueByAfterNate() + + + + diff --git a/inter/ConfirmSingleForQueue.py b/inter/ConfirmSingleForQueue.py index d213006..9c6ecf0 100644 --- a/inter/ConfirmSingleForQueue.py +++ b/inter/ConfirmSingleForQueue.py @@ -2,7 +2,6 @@ import datetime import time -from config.ticketConf import _get_yaml from inter.CheckRandCodeAnsyn import checkRandCodeAnsyn from inter.GetPassengerDTOs import getPassengerDTOs from inter.GetRandCode import getRandCode @@ -63,7 +62,7 @@ class confirmSingleForQueue: if self.is_node_code: print(u"正在使用自动识别验证码功能") for i in range(3): - randCode = getRandCode(is_auto_code=True, auto_code_type=_get_yaml()["auto_code_type"]) + randCode = getRandCode(is_auto_code=True, auto_code_type=2) checkcode = checkRandCodeAnsyn(self.session, randCode, self.token) if checkcode == 'TRUE': print(u"验证码通过,正在提交订单") diff --git a/inter/GetPassengerDTOs.py b/inter/GetPassengerDTOs.py index 5d90f87..4055375 100644 --- a/inter/GetPassengerDTOs.py +++ b/inter/GetPassengerDTOs.py @@ -4,11 +4,7 @@ import json from config.TicketEnmu import ticket from myException.PassengerUserException import PassengerUserException import wrapcache - -try: - xrange # Python 2 -except NameError: - xrange = range # Python 3 +import TickerConfig class getPassengerDTOs: @@ -76,37 +72,49 @@ class getPassengerDTOs: """ passengerTicketStrList = [] oldPassengerStr = [] + tickers = [] + set_type = "" if wrapcache.get("user_info"): # 如果缓存中有联系人方式,则读取缓存中的联系人 user_info = wrapcache.get("user_info") print(u"使用缓存中查找的联系人信息") else: user_info = self.sendGetPassengerDTOs() wrapcache.set("user_info", user_info, timeout=9999999) - set_type = self.getPassengerTicketStr(self.set_type) if not user_info: raise PassengerUserException(ticket.DTO_NOT_IN_LIST) if len(user_info) < self.is_more_ticket_num: # 如果乘车人填错了导致没有这个乘车人的话,可能乘车人数会小于自动乘车人 self.is_more_ticket_num = len(user_info) - if self.is_more_ticket_num is 1: - passengerTicketStrList.append( - '0,' + user_info[0]['passenger_type'] + "," + user_info[0][ - "passenger_name"] + "," + - user_info[0]['passenger_id_type_code'] + "," + user_info[0]['passenger_id_no'] + "," + - user_info[0]['mobile_no'] + ',N,' + user_info[0]["allEncStr"]) - oldPassengerStr.append( - user_info[0]['passenger_name'] + "," + user_info[0]['passenger_id_type_code'] + "," + - user_info[0]['passenger_id_no'] + "," + user_info[0]['passenger_type'] + '_') - else: - for i in xrange(self.is_more_ticket_num): + if TickerConfig.TICKET_TYPE is 1: + set_type = self.getPassengerTicketStr(self.set_type) + if self.is_more_ticket_num is 1: passengerTicketStrList.append( - '0,' + user_info[i]['passenger_type'] + "," + user_info[i][ - "passenger_name"] + "," + user_info[i]['passenger_id_type_code'] + "," + user_info[i][ - 'passenger_id_no'] + "," + user_info[i]['mobile_no'] + ',N,' + user_info[i]["allEncStr"] + '_' + set_type) + '0,' + user_info[0]['passenger_type'] + "," + user_info[0][ + "passenger_name"] + "," + + user_info[0]['passenger_id_type_code'] + "," + user_info[0]['passenger_id_no'] + "," + + user_info[0]['mobile_no'] + ',N,' + user_info[0]["allEncStr"]) oldPassengerStr.append( - user_info[i]['passenger_name'] + "," + user_info[i]['passenger_id_type_code'] + "," + - user_info[i]['passenger_id_no'] + "," + user_info[i]['passenger_type'] + '_') + user_info[0]['passenger_name'] + "," + user_info[0]['passenger_id_type_code'] + "," + + user_info[0]['passenger_id_no'] + "," + user_info[0]['passenger_type'] + '_') + else: + for i in range(self.is_more_ticket_num): + passengerTicketStrList.append( + '0,' + user_info[i]['passenger_type'] + "," + user_info[i][ + "passenger_name"] + "," + user_info[i]['passenger_id_type_code'] + "," + user_info[i][ + 'passenger_id_no'] + "," + user_info[i]['mobile_no'] + ',N,' + user_info[i]["allEncStr"] + '_' + set_type) + oldPassengerStr.append( + user_info[i]['passenger_name'] + "," + user_info[i]['passenger_id_type_code'] + "," + + user_info[i]['passenger_id_no'] + "," + user_info[i]['passenger_type'] + '_') + elif TickerConfig.TICKET_TYPE is 2: + """ + 候补订单有多少个联系人,就候补多少个联系人了,没有优先提交之说 + 1#XXXX#1#***************77X#bf6ae40d3655ae7eff005ee21d95876b38ab97a8031b464bc2f74a067e3ec957; + """ + for user in user_info: + tickers.append(f"1#{user['passenger_name']}#1#{user['passenger_id_no']}#{user['allEncStr']};") + return { "passengerTicketStrList": set_type + "," + ",".join(passengerTicketStrList), + "passengerTicketStrByAfterLate": "".join(tickers), "oldPassengerStr": "".join(oldPassengerStr), "code": ticket.SUCCESS_CODE, "set_type": set_type, diff --git a/inter/GetQueueCount.py b/inter/GetQueueCount.py index 86d5ed3..e2c98b5 100644 --- a/inter/GetQueueCount.py +++ b/inter/GetQueueCount.py @@ -5,8 +5,13 @@ import time from collections import OrderedDict import wrapcache -from config.ticketConf import _get_yaml +import TickerConfig +from config.TicketEnmu import ticket +from config.emailConf import sendEmail +from config.pushbearConf import sendPushBear +from config.urlConf import urls from inter.ConfirmSingleForQueue import confirmSingleForQueue +from myException.ticketIsExitsException import ticketIsExitsException def conversion_int(str): @@ -94,15 +99,37 @@ class getQueueCount: else: print(u"排队发现未知错误{0},将此列车 {1}加入小黑屋".format(getQueueCountResult, self.train_no)) wrapcache.set(key=self.train_no, value=datetime.datetime.now(), - timeout=int(_get_yaml()["ticket_black_list_time"]) * 60) + timeout=TickerConfig.TICKET_BLACK_LIST_TIME * 60) elif "messages" in getQueueCountResult and getQueueCountResult["messages"]: print(u"排队异常,错误信息:{0}, 将此列车 {1}加入小黑屋".format(getQueueCountResult["messages"][0], self.train_no)) wrapcache.set(key=self.train_no, value=datetime.datetime.now(), - timeout=int(_get_yaml()["ticket_black_list_time"]) * 60) + timeout=TickerConfig.TICKET_BLACK_LIST_TIME * 60) else: if "validateMessages" in getQueueCountResult and getQueueCountResult["validateMessages"]: print(str(getQueueCountResult["validateMessages"])) wrapcache.set(key=self.train_no, value=datetime.datetime.now(), - timeout=int(_get_yaml()["ticket_black_list_time"]) * 60) + timeout=TickerConfig.TICKET_BLACK_LIST_TIME * 60) else: print(u"未知错误 {0}".format("".join(getQueueCountResult))) + + +class queryQueueByAfterNate: + def __init__(self, session): + """ + 候补排队 + :param session: + """ + self.session = session + + def sendQueryQueueByAfterNate(self): + for i in range(10): + queryQueueByAfterNateRsp = self.session.httpClint.send(urls.get("queryQueue")) + if not queryQueueByAfterNateRsp.get("status"): + print("".join(queryQueueByAfterNateRsp.get("messages")) or queryQueueByAfterNateRsp.get("validateMessages")) + time.sleep(1) + else: + sendEmail(ticket.WAIT_ORDER_SUCCESS) + sendPushBear(sendEmail(ticket.WAIT_ORDER_SUCCESS)) + raise ticketIsExitsException(ticket.WAIT_AFTER_NATE_SUCCESS) + + diff --git a/inter/GetQueueCountAsync.py b/inter/GetQueueCountAsync.py index 50c7682..4df24b7 100644 --- a/inter/GetQueueCountAsync.py +++ b/inter/GetQueueCountAsync.py @@ -1,3 +1,5 @@ +import TickerConfig + []# coding=utf-8 import datetime import sys @@ -6,8 +8,6 @@ from collections import OrderedDict import wrapcache -from config.TicketEnmu import ticket -from config.ticketConf import _get_yaml from inter.ConfirmSingleForQueueAsys import confirmSingleForQueueAsys @@ -107,11 +107,11 @@ class getQueueCountAsync: else: print(u"排队发现未知错误{0},将此列车 {1}加入小黑屋".format(getQueueCountAsyncResult, self.train_no)) wrapcache.set(key=self.train_no, value=datetime.datetime.now(), - timeout=int(_get_yaml()["ticket_black_list_time"]) * 60) + timeout=TickerConfig.TICKET_BLACK_LIST_TIME * 60) elif "messages" in getQueueCountAsyncResult and getQueueCountAsyncResult["messages"]: print(u"排队异常,错误信息:{0}, 将此列车 {1}加入小黑屋".format(getQueueCountAsyncResult["messages"][0], self.train_no)) wrapcache.set(key=self.train_no, value=datetime.datetime.now(), - timeout=int(_get_yaml()["ticket_black_list_time"]) * 60) + timeout=TickerConfig.TICKET_BLACK_LIST_TIME * 60) else: if "validateMessages" in getQueueCountAsyncResult and getQueueCountAsyncResult["validateMessages"]: print(str(getQueueCountAsyncResult["validateMessages"])) diff --git a/inter/GetRandCode.py b/inter/GetRandCode.py index b51d4d0..b953d55 100644 --- a/inter/GetRandCode.py +++ b/inter/GetRandCode.py @@ -1,14 +1,7 @@ # coding=utf-8 from PIL import Image -from config.ticketConf import _get_yaml from verify.localVerifyCode import verify -from verify.ruokuai import RClient - -try: - raw_input # Python 2 -except NameError: # Python 3 - raw_input = input def getRandCode(is_auto_code, auto_code_type, result): @@ -48,7 +41,7 @@ def codexy(Ofset=None, is_raw_input=True): print(u"验证码分为8个,对应上面数字,例如第一和第二张,输入1, 2 如果开启cdn查询的话,会冲掉提示,直接鼠标点击命令行获取焦点,输入即可,不要输入空格") print(u"如果是linux无图形界面,请使用自动打码,is_auto_code: True") print(u"如果没有弹出验证码,请手动双击根目录下的tkcode.png文件") - Ofset = raw_input(u"输入对应的验证码: ") + Ofset = input(u"输入对应的验证码: ") if isinstance(Ofset, list): select = Ofset else: diff --git a/inter/GetSuccessRate.py b/inter/GetSuccessRate.py new file mode 100644 index 0000000..e5500f4 --- /dev/null +++ b/inter/GetSuccessRate.py @@ -0,0 +1,41 @@ +from collections import OrderedDict + + +from config.urlConf import urls +import TickerConfig +from inter.SubmitOrderRequest import submitOrderRequestByAfterNate + + +class getSuccessRate: + def __init__(self, session, secretList): + """ + 获取成功信息 + """ + self.secretList = secretList + self.session = session + + def data_apr(self): + """ + secretList 9vqa9%2B%2F%2Fsdozmm22hpSeDTGqRUwSuA2D0r%2BmU%2BLZj7MK7CDuf5Ep1xpxl4Dyxfmoah%2BaB9TZSesU%0AkxBbo5oNgR1vqMfvq66VP0T7tpQtH%2BbVGBz1FolZG8jDD%2FHqnz%2FnvdBP416Og6WGS14O%2F3iBSwT8%0AkRPsNF0Vq0U082g0tlJtP%2BPn7TzW3z7TDCceMJIjFcfEOA%2BW%2BuK%2Bpy6jCQMv0TmlkXf5aKcGnE02%0APuv4I8nF%2BOWjWzv9CrJyiCZiWaXd%2Bi7p69V3a9dhF787UgS660%2BqKRFB4RLwAfic3MkAlfpGWhMY%0ACfARVQ%3D%3D#O + _json_att + 候补一次只能补一个座位,默认取TICKET_TYPE第一个 + :return: + """ + + ticker = TickerConfig.PASSENGER_TICKER_STR.get(TickerConfig.SET_TYPE[0]) + data = OrderedDict() + data["successSecret"] = f"{self.secretList}#{ticker}" + data["_json_att"] = "" + return data + + def sendSuccessRate(self): + successRateRsp = self.session.httpClint.send(urls.get("getSuccessRate"), self.data_apr()) + if not successRateRsp.get("status"): + print("".join(successRateRsp.get("messages")) or successRateRsp.get("validateMessages")) + return + flag = successRateRsp.get("data", {}).get("flag")[0] + train_no = flag.get("train_no") + print(f"准备提交候补订单,{flag.get('info')}") + submit = submitOrderRequestByAfterNate(self.session, self.secretList, train_no) + submit.sendSubmitOrderRequest() + diff --git a/inter/Query.py b/inter/Query.py index 061e7de..15b9b58 100644 --- a/inter/Query.py +++ b/inter/Query.py @@ -1,16 +1,12 @@ # coding=utf-8 import copy -import threading -import time - import random - import wrapcache from config import urlConf from config.TicketEnmu import ticket from myUrllib.httpUtils import HTTPClient from config.configCommon import seat_conf_2 -from utils.timeUtil import time_to_minutes +import TickerConfig class query: @@ -21,7 +17,7 @@ class query: def __init__(self, session, from_station, to_station, from_station_h, to_station_h, _station_seat, station_trains, ticke_peoples_num, station_dates=None, ): self.session = session - self.httpClint = HTTPClient(session.is_proxy) + self.httpClint = HTTPClient(TickerConfig.IS_PROXY) self.urls = urlConf.urls self.from_station = from_station self.to_station = to_station @@ -32,14 +28,7 @@ class query: self.station_dates = station_dates if isinstance(station_dates, list) else list(station_dates) self.ticket_black_list = dict() self.ticke_peoples_num = ticke_peoples_num - # by time - self.is_by_time = session.is_by_time - self.train_types = session.train_types - self.departure_time = session.departure_time - self.arrival_time = session.arrival_time - self.take_time = session.take_time - @classmethod def station_seat(self, index): """ 获取车票对应坐席 @@ -57,37 +46,15 @@ class query: } return seat[index] - def check_time_interval(self, ticket_info): - """ - 判断日期是否符合当前设置时间 - fix: https://github.com/testerSunshine/12306/issues/256 - :param ticket_info: - :return: - """ - return self.departure_time <= time_to_minutes(ticket_info[8]) <= self.arrival_time and \ - time_to_minutes(ticket_info[9]) <= self.arrival_time and \ - time_to_minutes(ticket_info[10]) <= self.take_time - - def check_train_types(self, train): - train_type = train[0] - if train_type != "G" and train_type != "D": train_type = "O" - if train_type in self.train_types: - return True - else: - return False - def check_is_need_train(self, ticket_info): - if self.is_by_time: - return self.check_train_types(ticket_info[3]) and self.check_time_interval(ticket_info) - else: - return ticket_info[3] in self.station_trains + return ticket_info[3] in self.station_trains def sendQuery(self): """ 查询 :return: """ - if self.session.is_cdn == 1: + if TickerConfig.IS_CDN == 1: if self.session.cdn_list: self.httpClint.cdn = self.session.cdn_list[random.randint(0, len(self.session.cdn_list) - 1)] for station_date in self.station_dates: @@ -101,7 +68,7 @@ class query: continue value = station_ticket.get("data", "") if not value: - print(u'{0}-{1} 车次坐席查询为空,ip网络异常,可能是时间配置未正确,查询url: https://kyfw.12306.cn{2}, 可以手动查询是否有票'.format( + print(u'{0}-{1} 车次坐席查询为空,查询url: https://kyfw.12306.cn{2}, 可以手动查询是否有票'.format( self.from_station_h, self.to_station_h, select_url["req_url"])) @@ -110,63 +77,73 @@ class query: if result: for i in value['result']: ticket_info = i.split('|') - if ticket_info[11] == "Y" and ticket_info[1] == "预订": # 筛选未在开始时间内的车次 - for j in self._station_seat: - is_ticket_pass = ticket_info[j] - if is_ticket_pass != '' and is_ticket_pass != '无' and is_ticket_pass != '*' and self.check_is_need_train( - ticket_info): # 过滤有效目标车次 - secretStr = ticket_info[0] - train_no = ticket_info[2] - query_from_station_name = ticket_info[6] - query_to_station_name = ticket_info[7] - train_location = ticket_info[15] - stationTrainCode = ticket_info[3] - leftTicket = ticket_info[12] - start_time = ticket_info[8] - arrival_time = ticket_info[9] - distance_time = ticket_info[10] - print(start_time, arrival_time, distance_time) - seat = j - try: - ticket_num = int(ticket_info[j]) - except ValueError: - ticket_num = "有" - print(u'车次: {0} 始发车站: {1} 终点站: {2} {3}: {4}'.format(ticket_info[3], - self.from_station_h, - self.to_station_h, - seat_conf_2[j], - ticket_num)) - if wrapcache.get(train_no): - print(ticket.QUERY_IN_BLACK_LIST.format(train_no)) - continue - else: - if ticket_num != "有" and self.ticke_peoples_num > ticket_num: - if self.session.is_more_ticket: - print( - u"余票数小于乘车人数,当前余票数: {}, 删减人车人数到: {}".format(ticket_num, ticket_num)) - is_more_ticket_num = ticket_num - else: - print(u"余票数小于乘车人数,当前设置不提交,放弃此次提交机会") - continue + if TickerConfig.TICKET_TYPE is 2 and self.check_is_need_train(ticket_info): + # 如果最后一位为1,则是可以候补的,不知道这些正确嘛? + if ticket_info[-2] == "1": + print("当前订单可以候补,尝试提交候补订单") + return { + "secretStr": ticket_info[0], + "seat": TickerConfig.SET_TYPE, + "status": True, + } + elif TickerConfig.TICKET_TYPE is 1: + if ticket_info[11] == "Y" and ticket_info[1] == "预订": # 筛选未在开始时间内的车次 + for j in self._station_seat: + is_ticket_pass = ticket_info[j] + if is_ticket_pass != '' and is_ticket_pass != '无' and is_ticket_pass != '*' and self.check_is_need_train( + ticket_info): # 过滤有效目标车次 + secretStr = ticket_info[0] + train_no = ticket_info[2] + query_from_station_name = ticket_info[6] + query_to_station_name = ticket_info[7] + train_location = ticket_info[15] + stationTrainCode = ticket_info[3] + leftTicket = ticket_info[12] + start_time = ticket_info[8] + arrival_time = ticket_info[9] + distance_time = ticket_info[10] + print(start_time, arrival_time, distance_time) + seat = j + try: + ticket_num = int(ticket_info[j]) + except ValueError: + ticket_num = "有" + print(u'车次: {0} 始发车站: {1} 终点站: {2} {3}: {4}'.format(ticket_info[3], + self.from_station_h, + self.to_station_h, + seat_conf_2[j], + ticket_num)) + if wrapcache.get(train_no): + print(ticket.QUERY_IN_BLACK_LIST.format(train_no)) + continue else: - print(u"设置乘车人数为: {}".format(self.ticke_peoples_num)) - is_more_ticket_num = self.ticke_peoples_num - print(ticket.QUERY_C) - return { - "secretStr": secretStr, - "train_no": train_no, - "stationTrainCode": stationTrainCode, - "train_date": station_date, - "query_from_station_name": query_from_station_name, - "query_to_station_name": query_to_station_name, - "seat": seat, - "leftTicket": leftTicket, - "train_location": train_location, - "code": ticket.SUCCESS_CODE, - "is_more_ticket_num": is_more_ticket_num, - "cdn": self.httpClint.cdn, - "status": True, - } + if ticket_num != "有" and self.ticke_peoples_num > ticket_num: + if self.session.is_more_ticket: + print( + u"余票数小于乘车人数,当前余票数: {}, 删减人车人数到: {}".format(ticket_num, ticket_num)) + is_more_ticket_num = ticket_num + else: + print(u"余票数小于乘车人数,当前设置不提交,放弃此次提交机会") + continue + else: + print(u"设置乘车人数为: {}".format(self.ticke_peoples_num)) + is_more_ticket_num = self.ticke_peoples_num + print(ticket.QUERY_C) + return { + "secretStr": secretStr, + "train_no": train_no, + "stationTrainCode": stationTrainCode, + "train_date": station_date, + "query_from_station_name": query_from_station_name, + "query_to_station_name": query_to_station_name, + "seat": seat, + "leftTicket": leftTicket, + "train_location": train_location, + "code": ticket.SUCCESS_CODE, + "is_more_ticket_num": is_more_ticket_num, + "cdn": self.httpClint.cdn, + "status": True, + } else: print(u"车次配置信息有误,或者返回数据异常,请检查 {}".format(station_ticket)) return {"code": ticket.FAIL_CODE, "status": False, "cdn": self.httpClint.cdn, } diff --git a/inter/SubmitOrderRequest.py b/inter/SubmitOrderRequest.py index cb422dc..7cac605 100644 --- a/inter/SubmitOrderRequest.py +++ b/inter/SubmitOrderRequest.py @@ -1,8 +1,11 @@ # coding=utf-8 import datetime import urllib - +from collections import OrderedDict +import TickerConfig +from config.urlConf import urls from inter.CheckOrderInfo import checkOrderInfo +from inter.ConfirmHB import confirmHB from myException.ticketIsExitsException import ticketIsExitsException @@ -19,6 +22,7 @@ class submitOrderRequest: def __init__(self, session, secretStr, from_station, to_station, train_no, set_type, passengerTicketStrList, oldPassengerStr, train_date, ticke_peoples): self.session = session + # self.secretStr = secretStr try: self.secretStr = urllib.unquote(secretStr) except AttributeError: @@ -42,8 +46,9 @@ class submitOrderRequest: ('back_train_date', time()), # 返程时间 ('tour_flag', 'dc'), # 旅途类型 ('purpose_codes', 'ADULT'), # 成人票还是学生票 - ('query_from_station_name', self.from_station), # 起始车站 - ('query_to_station_name', self.to_station), # 终点车站 + ('query_from_station_name', TickerConfig.FROM_STATION), # 起始车站 + ('query_to_station_name', TickerConfig.TO_STATION), # 终点车站 + ('undefined', ''), ] return data @@ -68,3 +73,37 @@ class submitOrderRequest: print (u'出票失败') elif 'messages' in submitResult and submitResult['messages']: raise ticketIsExitsException(submitResult['messages'][0]) + + +class submitOrderRequestByAfterNate: + def __init__(self, session, secretList, tickerNo): + """ + 提交候补订单 + :param secretList: + :param session: + """ + self.secretList = secretList + self.session = session + self.tickerNo = tickerNo + + def data_apr(self): + """ + secretList 9vqa9%2B%2F%2Fsdozmm22hpSeDTGqRUwSuA2D0r%2BmU%2BLZj7MK7CDuf5Ep1xpxl4Dyxfmoah%2BaB9TZSesU%0AkxBbo5oNgR1vqMfvq66VP0T7tpQtH%2BbVGBz1FolZG8jDD%2FHqnz%2FnvdBP416Og6WGS14O%2F3iBSwT8%0AkRPsNF0Vq0U082g0tlJtP%2BPn7TzW3z7TDCceMJIjFcfEOA%2BW%2BuK%2Bpy6jCQMv0TmlkXf5aKcGnE02%0APuv4I8nF%2BOWjWzv9CrJyiCZiWaXd%2Bi7p69V3a9dhF787UgS660%2BqKRFB4RLwAfic3MkAlfpGWhMY%0ACfARVQ%3D%3D#O| + _json_att + 候补一次只能补一个座位,默认取TICKET_TYPE第一个 + :return: + """ + + ticker = TickerConfig.PASSENGER_TICKER_STR.get(TickerConfig.SET_TYPE[0]) + data = OrderedDict() + data["secretList"] = f"{self.secretList}#{ticker}|" + data["_json_att"] = "" + return data + + def sendSubmitOrderRequest(self, ): + submitOrderRequestRsp = self.session.httpClint.send(urls.get("SubmitOrderRequestRsp"), self.data_apr()) + if not submitOrderRequestRsp.get("status"): + print("".join(submitOrderRequestRsp.get("messages")) or submitOrderRequestRsp.get("validateMessages")) + return + confirm = confirmHB(self.secretList, self.session, self.tickerNo) + confirm.sendChechFace() diff --git a/tkcode.png b/tkcode.png index b8faea4164d80bbc8b7ca44719e88be3d7391173..8416dcabd3807218494c320fec5bd37597bfe4bf 100644 GIT binary patch delta 12210 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 delta 14249 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