新增候补功能

pull/302/head
文贤平 2019-09-01 12:41:25 +08:00
parent c5c9450ef5
commit 808c302088
25 changed files with 588 additions and 467 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@
*.log *.log
.idea/ .idea/
*.h5 *.h5
tkcode.png

View File

@ -18,13 +18,13 @@
- 非root用户避免安装和运行时使用了不同环境: `pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt` - 非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) - 可以配置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 python run.py`,即可开始
- 由于新增对时功能,请务必用**sudosudosudo** 执行否则会报权限错误windows打开ide或者cmd请用管理员身份执行`python run.py`,不需要加`sudo` - 由于新增对时功能,请务必用**sudosudosudo** 执行否则会报权限错误windows打开ide或者cmd请用管理员身份执行`python run.py`,不需要加`sudo`
- 如果你的服务器安装了docker与docker-compose, 那么就可以通过`docker-compose`进行启动,`docker.sh`脚本对此进行了封装,可以通过如下命令进行启动 - 如果你的服务器安装了docker与docker-compose, 那么就可以通过`docker-compose`进行启动,`docker.sh`脚本对此进行了封装,可以通过如下命令进行启动

119
TickerConfig.py Normal file
View File

@ -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,
}

View File

@ -53,6 +53,5 @@ class testAll(unittest.TestCase):
# :return: # :return:
# """ # """
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -157,3 +157,9 @@
- 删除若快打码 - 删除若快打码
- 修复不能下单问题 - 修复不能下单问题
- 放弃支持python2.7,只支持3.6以上版本 - 放弃支持python2.7,只支持3.6以上版本
- 2019.09.01更新
- 去除yaml配置文件改为py文件配置
- 增加候补订单功能
- 新增TICKET_TYPE字段1=刷票 2=候补
- 目前候补只支持单车次,多乘车人候补,由于目前不是很懂候补的需求,所以暂时这样做

View File

@ -1,4 +1,5 @@
# coding=utf-8 # coding=utf-8
from enum import Enum
class ticket(object): class ticket(object):
@ -25,6 +26,7 @@ class ticket(object):
OUT_NUM = 120 # 排队请求12306的次数 OUT_NUM = 120 # 排队请求12306的次数
WAIT_OUT_NUM = u"超出排队时间,自动放弃,正在重新刷票" WAIT_OUT_NUM = u"超出排队时间,自动放弃,正在重新刷票"
WAIT_ORDER_SUCCESS = u"恭喜您订票成功,订单号为:{0}, 请立即打开浏览器登录12306访问未完成订单在30分钟内完成支付!" WAIT_ORDER_SUCCESS = u"恭喜您订票成功,订单号为:{0}, 请立即打开浏览器登录12306访问未完成订单在30分钟内完成支付!"
WAIT_AFTER_NATE_SUCCESS = u"候补订单已完成请立即打开浏览器登录12306访问候补订单在30分钟内完成支付!"
WAIT_ORDER_CONTINUE = u"排队等待时间预计还剩 {0} ms" WAIT_ORDER_CONTINUE = u"排队等待时间预计还剩 {0} ms"
WAIT_ORDER_FAIL = u"排队等待失败,错误消息:{0}" WAIT_ORDER_FAIL = u"排队等待失败,错误消息:{0}"
WAIT_ORDER_NUM = u"{0}次排队中,请耐心等待" WAIT_ORDER_NUM = u"{0}次排队中,请耐心等待"
@ -37,4 +39,3 @@ class ticket(object):
REST_TIME_PAST = u"休息时间已过,重新开启检票功能" REST_TIME_PAST = u"休息时间已过,重新开启检票功能"
LOGIN_SESSION_FAIL = u"用户检查失败:{0}可能未登录可能session已经失效, 正在重新登录中" LOGIN_SESSION_FAIL = u"用户检查失败:{0}可能未登录可能session已经失效, 正在重新登录中"

View File

@ -1,10 +1,9 @@
# -*- coding: utf8 -*- # -*- coding: utf8 -*-
import socket import socket
__author__ = 'MR.wen' __author__ = 'MR.wen'
import TickerConfig
from email.header import Header from email.header import Header
from email.mime.text import MIMEText from email.mime.text import MIMEText
from config.ticketConf import _get_yaml
import smtplib import smtplib
@ -14,38 +13,33 @@ def sendEmail(msg):
:param str: email content :param str: email content
:return: :return:
""" """
email_conf = _get_yaml() try:
is_email = email_conf["email_conf"]["is_email"] sender = TickerConfig.EMAIL_CONF["email"]
if is_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: try:
sender = email_conf["email_conf"]["email"] smtp = smtplib.SMTP_SSL()
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.connect(host) smtp.connect(host)
smtp.login(username, password) except socket.error:
smtp.sendmail(sender, receiver.split(","), msg.as_string()) smtp = smtplib.SMTP()
smtp.quit() smtp.connect(host)
print(u"邮件已通知, 请查收") smtp.connect(host)
except Exception as e: smtp.login(username, password)
print(u"邮件配置有误{}".format(e)) smtp.sendmail(sender, receiver.split(","), msg.as_string())
else: smtp.quit()
pass print(u"邮件已通知, 请查收")
except Exception as e:
print(u"邮件配置有误{}".format(e))
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,8 +1,5 @@
# -*- coding: utf8 -*- # -*- coding: utf8 -*-
import time import TickerConfig
import requests
from config.ticketConf import _get_yaml
from config.urlConf import urls from config.urlConf import urls
from myUrllib.httpUtils import HTTPClient from myUrllib.httpUtils import HTTPClient
@ -15,12 +12,11 @@ def sendPushBear(msg):
:param str: 通知内容 content :param str: 通知内容 content
:return: :return:
""" """
conf = _get_yaml() if TickerConfig.PUSHBEAR_CONF["is_pushbear"] and TickerConfig.PUSHBEAR_CONF["pushbear_conf"]["send_key"].strip() != "":
if conf["pushbear_conf"]["is_pushbear"] and conf["pushbear_conf"]["send_key"].strip() != "":
try: try:
sendPushBearUrls = urls.get("Pushbear") sendPushBearUrls = urls.get("Pushbear")
data = { data = {
"sendkey": conf["pushbear_conf"]["send_key"].strip(), "sendkey": TickerConfig.PUSHBEAR_CONF["pushbear_conf"]["send_key"].strip(),
"text": "易行购票成功通知", "text": "易行购票成功通知",
"desp": msg "desp": msg
} }

View File

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

View File

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

View File

@ -139,7 +139,7 @@ urls = {
"is_json": False, "is_json": False,
}, },
"getDevicesId": { # 获取用户信息 "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&timestamp={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&timestamp={0}",
"req_type": "get", "req_type": "get",
"Referer": "https://kyfw.12306.cn/otn/passport?redirect=/otn/", "Referer": "https://kyfw.12306.cn/otn/passport?redirect=/otn/",
"Host": "kyfw.12306.cn", "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,
},
} }

View File

@ -3,8 +3,7 @@ import copy
import time import time
from collections import OrderedDict from collections import OrderedDict
from time import sleep from time import sleep
import TickerConfig
from config.ticketConf import _get_yaml
from inter.GetPassCodeNewOrderAndLogin import getPassCodeNewOrderAndLogin1 from inter.GetPassCodeNewOrderAndLogin import getPassCodeNewOrderAndLogin1
from inter.GetRandCode import getRandCode from inter.GetRandCode import getRandCode
from inter.LoginAysnSuggest import loginAysnSuggest from inter.LoginAysnSuggest import loginAysnSuggest
@ -126,7 +125,7 @@ class GoLogin:
:param passwd: 密码 :param passwd: 密码
:return: :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: if not user or not passwd:
raise UserPasswordException(u"温馨提示: 用户名或者密码为空,请仔细检查") raise UserPasswordException(u"温馨提示: 用户名或者密码为空,请仔细检查")
login_num = 0 login_num = 0
@ -138,7 +137,7 @@ class GoLogin:
devicesIdUrl["req_url"] = devicesIdUrl["req_url"].format(int(time.time() * 1000)) devicesIdUrl["req_url"] = devicesIdUrl["req_url"].format(int(time.time() * 1000))
# devicesIdRsp = self.session.httpClint.send(devicesIdUrl) # devicesIdRsp = self.session.httpClint.send(devicesIdUrl)
# devicesId = eval(devicesIdRsp.split("(")[1].split(")")[0].replace("'", ""))["dfp"] # devicesId = eval(devicesIdRsp.split("(")[1].split(")")[0].replace("'", ""))["dfp"]
devicesId = "UysLb2cYwsVjyInSzZ0pGOmYplvokmhBjoGNjrinquaUD0id7gkifgF6FvM2TRCL7Df89GZL1lVV763tGhiPhxlNdlE7iQkk496KUGCFZyyWxE4d0XjyHYv9DlsXfKTlrd8RBUdYIYjmWBXWMN65ElDQiO_Rnrul" devicesId = "K1OnaaicUR1DrGl2vRS1HrLLna8UBoXkESCnuPMBzVtrO6fG4URi2RWJHpM7urYlYx-fpp0AeM4Ca8rNN4WyYv1X493VsH5yejsLNol7XZ74gRp8yE7eEDHYU87t1urn3Oeaifrjrd5FRTmk3WCNylKeE2UQhPRH"
if devicesId: if devicesId:
self.session.httpClint.set_cookies(RAIL_DEVICEID=devicesId) self.session.httpClint.set_cookies(RAIL_DEVICEID=devicesId)

View File

@ -6,16 +6,15 @@ import socket
import sys import sys
import threading import threading
import time import time
import TickerConfig
import wrapcache import wrapcache
from agency.cdn_utils import CDNProxy from agency.cdn_utils import CDNProxy
from config import urlConf, configCommon from config import urlConf, configCommon
from config.TicketEnmu import ticket from config.TicketEnmu import ticket
from config.configCommon import seat_conf, checkDate, seat_conf_2 from config.configCommon import seat_conf_2, seat_conf
from config.ticketConf import _get_yaml
from init.login import GoLogin from init.login import GoLogin
from inter.AutoSubmitOrderRequest import autoSubmitOrderRequest from inter.AutoSubmitOrderRequest import autoSubmitOrderRequest
from inter.ChechFace import chechFace
from inter.CheckUser import checkUser from inter.CheckUser import checkUser
from inter.GetPassengerDTOs import getPassengerDTOs from inter.GetPassengerDTOs import getPassengerDTOs
from inter.LiftTicketInit import liftTicketInit from inter.LiftTicketInit import liftTicketInit
@ -27,13 +26,6 @@ from myException.ticketConfigException import ticketConfigException
from myException.ticketIsExitsException import ticketIsExitsException from myException.ticketIsExitsException import ticketIsExitsException
from myException.ticketNumOutException import ticketNumOutException from myException.ticketNumOutException import ticketNumOutException
from myUrllib.httpUtils import HTTPClient 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: class select:
@ -42,57 +34,25 @@ class select:
""" """
def __init__(self): def __init__(self):
self.from_station, self.to_station, self.station_dates, self._station_seat, self.is_more_ticket, \ self.get_ticket_info()
self.ticke_peoples, self.station_trains, self.ticket_black_list_time, \ self._station_seat = [seat_conf[x] for x in TickerConfig.SET_TYPE]
self.order_type, self.is_by_time, self.train_types, self.departure_time, \ self.auto_code_type = 2
self.arrival_time, self.take_time, self.order_model, self.open_time, self.is_proxy = self.get_ticket_info() self.httpClint = HTTPClient(TickerConfig.IS_PROXY)
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.urls = urlConf.urls 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.cdn_list = []
self.queryUrl = "leftTicket/query" self.queryUrl = "leftTicket/queryT"
self.passengerTicketStrList = "" self.passengerTicketStrList = ""
self.passengerTicketStrByAfterLate = ""
self.oldPassengerStr = "" self.oldPassengerStr = ""
self.set_type = "" self.set_type = ""
def get_ticket_info(self): @staticmethod
def get_ticket_info():
""" """
获取配置信息 获取配置信息
:return: :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"*" * 50)
print(u"检查当前python版本为{}目前版本只支持3.6以上".format(sys.version.split(" ")[0])) print(u"检查当前python版本为{}目前版本只支持3.6以上".format(sys.version.split(" ")[0]))
print(u"12306刷票小助手最后更新于2019.01.08请勿作为商业用途交流群号286271084(已满)" print(u"12306刷票小助手最后更新于2019.01.08请勿作为商业用途交流群号286271084(已满)"
@ -103,31 +63,10 @@ class select:
u" 6群: 444101020(未满)\n" u" 6群: 444101020(未满)\n"
u" 7群: 660689659(未满)\n" u" 7群: 660689659(未满)\n"
) )
if is_by_time: print(
method_notie = u"购票方式:根据时间区间购票\n可接受最早出发时间:{0}\n可接受最晚抵达时间:{1}\n可接受最长旅途时间:{2}\n可接受列车类型:{3}\n" \ 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" \
.format(minutes_to_time(departure_time), minutes_to_time(arrival_time), minutes_to_time(take_time), f"刷新间隔: 随机(1-3S)\n僵尸票关小黑屋时长: {TickerConfig.TICKET_BLACK_LIST_TIME}\n下单接口: {TickerConfig.ORDER_TYPE}\n下单模式: {TickerConfig.ORDER_MODEL}\n预售踩点时间:{TickerConfig.OPEN_TIME}")
" , ".join(train_types)) print(u"*" * 50)
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
def station_table(self, from_station, to_station): def station_table(self, from_station, to_station):
""" """
@ -184,7 +123,7 @@ class select:
cdn 认证 cdn 认证
:return: :return:
""" """
if self.is_cdn == 1: if TickerConfig.IS_CDN == 1:
CDN = CDNProxy() CDN = CDNProxy()
all_cdn = CDN.open_cdn_file() all_cdn = CDN.open_cdn_file()
if all_cdn: if all_cdn:
@ -200,7 +139,6 @@ class select:
raise ticketConfigException(u"cdn列表为空请先加载cdn") raise ticketConfigException(u"cdn列表为空请先加载cdn")
def main(self): def main(self):
# autoSynchroTime() # 同步时间
self.cdn_certification() self.cdn_certification()
l = liftTicketInit(self) l = liftTicketInit(self)
l.reqLiftTicketInit() l.reqLiftTicketInit()
@ -209,23 +147,23 @@ class select:
t = threading.Thread(target=check_user.sendCheckUser) t = threading.Thread(target=check_user.sendCheckUser)
t.setDaemon(True) t.setDaemon(True)
t.start() 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 num = 0
s = getPassengerDTOs(session=self, ticket_peoples=self.ticke_peoples) s = getPassengerDTOs(session=self, ticket_peoples=TickerConfig.TICKET_PEOPLES)
passenger = s.sendGetPassengerDTOs() passenger = s.sendGetPassengerDTOs()
wrapcache.set("user_info", passenger, timeout=9999999) wrapcache.set("user_info", passenger, timeout=9999999)
while 1: while 1:
try: try:
num += 1 num += 1
now = datetime.datetime.now() # 感谢群里大佬提供整点代码 now = datetime.datetime.now() # 感谢群里大佬提供整点代码
configCommon.checkSleepTime(self) # 晚上到点休眠 configCommon.checkSleepTime(self) # 晚上到点休眠
if self.order_model is 1: if TickerConfig.ORDER_MODEL is 1:
sleep_time_s = 0.5 sleep_time_s = 0.5
sleep_time_t = 0.6 sleep_time_t = 0.6
# 测试了一下有微妙级的误差应该不影响测试结果2019-01-02 22:30:00.004555,预售还是会受到前一次刷新的时间影响,暂时没想到好的解决方案 # 测试了一下有微妙级的误差应该不影响测试结果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() now = datetime.datetime.now()
if now.strftime("%H:%M:%S") > self.open_time: if now.strftime("%H:%M:%S") > TickerConfig.OPEN_TIME:
break break
time.sleep(0.0001) time.sleep(0.0001)
else: else:
@ -234,12 +172,12 @@ class select:
q = query(session=self, q = query(session=self,
from_station=from_station, from_station=from_station,
to_station=to_station, to_station=to_station,
from_station_h=self.from_station, from_station_h=TickerConfig.FROM_STATION,
to_station_h=self.to_station, to_station_h=TickerConfig.FROM_STATION,
_station_seat=self._station_seat, _station_seat=self._station_seat,
station_trains=self.station_trains, station_trains=TickerConfig.STATION_TRAINS,
station_dates=self.station_dates, station_dates=TickerConfig.STATION_DATES,
ticke_peoples_num=len(self.ticke_peoples), ticke_peoples_num=len(TickerConfig.TICKET_PEOPLES),
) )
queryResult = q.sendQuery() queryResult = q.sendQuery()
# 查询接口 # 查询接口
@ -252,46 +190,55 @@ class select:
leftTicket = queryResult.get("leftTicket", "") leftTicket = queryResult.get("leftTicket", "")
query_from_station_name = queryResult.get("query_from_station_name", "") query_from_station_name = queryResult.get("query_from_station_name", "")
query_to_station_name = queryResult.get("query_to_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): if wrapcache.get(train_no):
print(ticket.QUEUE_WARNING_MSG.format(train_no)) print(ticket.QUEUE_WARNING_MSG.format(train_no))
else: else:
# 获取联系人 # 获取联系人
s = getPassengerDTOs(session=self, ticket_peoples=self.ticke_peoples, s = getPassengerDTOs(session=self, ticket_peoples=TickerConfig.TICKET_PEOPLES,
set_type=seat_conf_2[seat], set_type="" if isinstance(seat, list) else seat_conf_2[seat],
# 候补订单需要设置多个坐席
is_more_ticket_num=is_more_ticket_num) is_more_ticket_num=is_more_ticket_num)
getPassengerDTOsResult = s.getPassengerTicketStrListAndOldPassengerStr() getPassengerDTOsResult = s.getPassengerTicketStrListAndOldPassengerStr()
if getPassengerDTOsResult.get("status", False): if getPassengerDTOsResult.get("status", False):
self.passengerTicketStrList = getPassengerDTOsResult.get("passengerTicketStrList", "") self.passengerTicketStrList = getPassengerDTOsResult.get("passengerTicketStrList", "")
self.passengerTicketStrByAfterLate = getPassengerDTOsResult.get(
"passengerTicketStrByAfterLate", "")
self.oldPassengerStr = getPassengerDTOsResult.get("oldPassengerStr", "") self.oldPassengerStr = getPassengerDTOsResult.get("oldPassengerStr", "")
self.set_type = getPassengerDTOsResult.get("set_type", "") self.set_type = getPassengerDTOsResult.get("set_type", "")
# 提交订单 # 提交订单
if self.order_type == 1: # 快读下单 # 订单分为两种,一种为抢单,一种为候补订单
a = autoSubmitOrderRequest(session=self, if TickerConfig.TICKET_TYPE == 1:
secretStr=secretStr, if TickerConfig.ORDER_TYPE == 1: # 快速下单
train_date=train_date, a = autoSubmitOrderRequest(session=self,
passengerTicketStr=self.passengerTicketStrList, secretStr=secretStr,
oldPassengerStr=self.oldPassengerStr, train_date=train_date,
train_no=train_no, passengerTicketStr=self.passengerTicketStrList,
stationTrainCode=stationTrainCode, oldPassengerStr=self.oldPassengerStr,
leftTicket=leftTicket, train_no=train_no,
set_type=self.set_type, stationTrainCode=stationTrainCode,
query_from_station_name=query_from_station_name, leftTicket=leftTicket,
query_to_station_name=query_to_station_name, set_type=self.set_type,
) query_from_station_name=query_from_station_name,
a.sendAutoSubmitOrderRequest() query_to_station_name=query_to_station_name,
elif self.order_type == 2: # 普通下单 )
sor = submitOrderRequest(self, secretStr, from_station, to_station, train_no, self.set_type, a.sendAutoSubmitOrderRequest()
self.passengerTicketStrList, self.oldPassengerStr, train_date, elif TickerConfig.ORDER_TYPE == 2: # 普通下单
self.ticke_peoples) sor = submitOrderRequest(self, secretStr, from_station, to_station, train_no,
sor.sendSubmitOrderRequest() 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: else:
random_time = round(random.uniform(sleep_time_s, sleep_time_t), 2) 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, print(u"正在第{0}次查询 随机停留时长:{6} 乘车日期: {1} 车次:{2} 查询无票 cdn轮询IP{4}当前cdn总数{5} 总耗时:{3}ms".format(num,
",".join( ",".join(
self.station_dates), TickerConfig.STATION_DATES),
",".join( ",".join(
self.station_trains), TickerConfig.STATION_TRAINS),
( (
datetime.datetime.now() - now).microseconds / 1000, datetime.datetime.now() - now).microseconds / 1000,
queryResult.get( queryResult.get(

View File

@ -3,7 +3,6 @@ import urllib
from collections import OrderedDict from collections import OrderedDict
from config.TicketEnmu import ticket from config.TicketEnmu import ticket
from config.ticketConf import _get_yaml
from inter.CheckRandCodeAnsyn import checkRandCodeAnsyn from inter.CheckRandCodeAnsyn import checkRandCodeAnsyn
from inter.GetQueueCountAsync import getQueueCountAsync from inter.GetQueueCountAsync import getQueueCountAsync
from inter.GetRandCode import getRandCode from inter.GetRandCode import getRandCode
@ -104,7 +103,7 @@ class autoSubmitOrderRequest:
print(u"需要验证码") print(u"需要验证码")
print(u"正在使用自动识别验证码功能") print(u"正在使用自动识别验证码功能")
for i in range(3): 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, "") checkcode = checkRandCodeAnsyn(self.session, randCode, "")
if checkcode == 'TRUE': if checkcode == 'TRUE':
print(u"验证码通过,正在提交订单") print(u"验证码通过,正在提交订单")

37
inter/ChechFace.py Normal file
View File

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

43
inter/ConfirmHB.py Normal file
View File

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

View File

@ -2,7 +2,6 @@
import datetime import datetime
import time import time
from config.ticketConf import _get_yaml
from inter.CheckRandCodeAnsyn import checkRandCodeAnsyn from inter.CheckRandCodeAnsyn import checkRandCodeAnsyn
from inter.GetPassengerDTOs import getPassengerDTOs from inter.GetPassengerDTOs import getPassengerDTOs
from inter.GetRandCode import getRandCode from inter.GetRandCode import getRandCode
@ -63,7 +62,7 @@ class confirmSingleForQueue:
if self.is_node_code: if self.is_node_code:
print(u"正在使用自动识别验证码功能") print(u"正在使用自动识别验证码功能")
for i in range(3): 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) checkcode = checkRandCodeAnsyn(self.session, randCode, self.token)
if checkcode == 'TRUE': if checkcode == 'TRUE':
print(u"验证码通过,正在提交订单") print(u"验证码通过,正在提交订单")

View File

@ -4,11 +4,7 @@ import json
from config.TicketEnmu import ticket from config.TicketEnmu import ticket
from myException.PassengerUserException import PassengerUserException from myException.PassengerUserException import PassengerUserException
import wrapcache import wrapcache
import TickerConfig
try:
xrange # Python 2
except NameError:
xrange = range # Python 3
class getPassengerDTOs: class getPassengerDTOs:
@ -76,37 +72,49 @@ class getPassengerDTOs:
""" """
passengerTicketStrList = [] passengerTicketStrList = []
oldPassengerStr = [] oldPassengerStr = []
tickers = []
set_type = ""
if wrapcache.get("user_info"): # 如果缓存中有联系人方式,则读取缓存中的联系人 if wrapcache.get("user_info"): # 如果缓存中有联系人方式,则读取缓存中的联系人
user_info = wrapcache.get("user_info") user_info = wrapcache.get("user_info")
print(u"使用缓存中查找的联系人信息") print(u"使用缓存中查找的联系人信息")
else: else:
user_info = self.sendGetPassengerDTOs() user_info = self.sendGetPassengerDTOs()
wrapcache.set("user_info", user_info, timeout=9999999) wrapcache.set("user_info", user_info, timeout=9999999)
set_type = self.getPassengerTicketStr(self.set_type)
if not user_info: if not user_info:
raise PassengerUserException(ticket.DTO_NOT_IN_LIST) raise PassengerUserException(ticket.DTO_NOT_IN_LIST)
if len(user_info) < self.is_more_ticket_num: # 如果乘车人填错了导致没有这个乘车人的话,可能乘车人数会小于自动乘车人 if len(user_info) < self.is_more_ticket_num: # 如果乘车人填错了导致没有这个乘车人的话,可能乘车人数会小于自动乘车人
self.is_more_ticket_num = len(user_info) self.is_more_ticket_num = len(user_info)
if self.is_more_ticket_num is 1: if TickerConfig.TICKET_TYPE is 1:
passengerTicketStrList.append( set_type = self.getPassengerTicketStr(self.set_type)
'0,' + user_info[0]['passenger_type'] + "," + user_info[0][ if self.is_more_ticket_num is 1:
"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):
passengerTicketStrList.append( passengerTicketStrList.append(
'0,' + user_info[i]['passenger_type'] + "," + user_info[i][ '0,' + user_info[0]['passenger_type'] + "," + user_info[0][
"passenger_name"] + "," + user_info[i]['passenger_id_type_code'] + "," + user_info[i][ "passenger_name"] + "," +
'passenger_id_no'] + "," + user_info[i]['mobile_no'] + ',N,' + user_info[i]["allEncStr"] + '_' + set_type) 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( oldPassengerStr.append(
user_info[i]['passenger_name'] + "," + user_info[i]['passenger_id_type_code'] + "," + user_info[0]['passenger_name'] + "," + user_info[0]['passenger_id_type_code'] + "," +
user_info[i]['passenger_id_no'] + "," + user_info[i]['passenger_type'] + '_') 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 { return {
"passengerTicketStrList": set_type + "," + ",".join(passengerTicketStrList), "passengerTicketStrList": set_type + "," + ",".join(passengerTicketStrList),
"passengerTicketStrByAfterLate": "".join(tickers),
"oldPassengerStr": "".join(oldPassengerStr), "oldPassengerStr": "".join(oldPassengerStr),
"code": ticket.SUCCESS_CODE, "code": ticket.SUCCESS_CODE,
"set_type": set_type, "set_type": set_type,

View File

@ -5,8 +5,13 @@ import time
from collections import OrderedDict from collections import OrderedDict
import wrapcache 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 inter.ConfirmSingleForQueue import confirmSingleForQueue
from myException.ticketIsExitsException import ticketIsExitsException
def conversion_int(str): def conversion_int(str):
@ -94,15 +99,37 @@ class getQueueCount:
else: else:
print(u"排队发现未知错误{0},将此列车 {1}加入小黑屋".format(getQueueCountResult, self.train_no)) print(u"排队发现未知错误{0},将此列车 {1}加入小黑屋".format(getQueueCountResult, self.train_no))
wrapcache.set(key=self.train_no, value=datetime.datetime.now(), 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"]: elif "messages" in getQueueCountResult and getQueueCountResult["messages"]:
print(u"排队异常,错误信息:{0}, 将此列车 {1}加入小黑屋".format(getQueueCountResult["messages"][0], self.train_no)) print(u"排队异常,错误信息:{0}, 将此列车 {1}加入小黑屋".format(getQueueCountResult["messages"][0], self.train_no))
wrapcache.set(key=self.train_no, value=datetime.datetime.now(), 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: else:
if "validateMessages" in getQueueCountResult and getQueueCountResult["validateMessages"]: if "validateMessages" in getQueueCountResult and getQueueCountResult["validateMessages"]:
print(str(getQueueCountResult["validateMessages"])) print(str(getQueueCountResult["validateMessages"]))
wrapcache.set(key=self.train_no, value=datetime.datetime.now(), 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: else:
print(u"未知错误 {0}".format("".join(getQueueCountResult))) 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)

View File

@ -1,3 +1,5 @@
import TickerConfig
[]# coding=utf-8 []# coding=utf-8
import datetime import datetime
import sys import sys
@ -6,8 +8,6 @@ from collections import OrderedDict
import wrapcache import wrapcache
from config.TicketEnmu import ticket
from config.ticketConf import _get_yaml
from inter.ConfirmSingleForQueueAsys import confirmSingleForQueueAsys from inter.ConfirmSingleForQueueAsys import confirmSingleForQueueAsys
@ -107,11 +107,11 @@ class getQueueCountAsync:
else: else:
print(u"排队发现未知错误{0},将此列车 {1}加入小黑屋".format(getQueueCountAsyncResult, self.train_no)) print(u"排队发现未知错误{0},将此列车 {1}加入小黑屋".format(getQueueCountAsyncResult, self.train_no))
wrapcache.set(key=self.train_no, value=datetime.datetime.now(), 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"]: elif "messages" in getQueueCountAsyncResult and getQueueCountAsyncResult["messages"]:
print(u"排队异常,错误信息:{0}, 将此列车 {1}加入小黑屋".format(getQueueCountAsyncResult["messages"][0], self.train_no)) print(u"排队异常,错误信息:{0}, 将此列车 {1}加入小黑屋".format(getQueueCountAsyncResult["messages"][0], self.train_no))
wrapcache.set(key=self.train_no, value=datetime.datetime.now(), 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: else:
if "validateMessages" in getQueueCountAsyncResult and getQueueCountAsyncResult["validateMessages"]: if "validateMessages" in getQueueCountAsyncResult and getQueueCountAsyncResult["validateMessages"]:
print(str(getQueueCountAsyncResult["validateMessages"])) print(str(getQueueCountAsyncResult["validateMessages"]))

View File

@ -1,14 +1,7 @@
# coding=utf-8 # coding=utf-8
from PIL import Image from PIL import Image
from config.ticketConf import _get_yaml
from verify.localVerifyCode import verify 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): 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"验证码分为8个对应上面数字例如第一和第二张输入1, 2 如果开启cdn查询的话会冲掉提示直接鼠标点击命令行获取焦点输入即可不要输入空格")
print(u"如果是linux无图形界面请使用自动打码is_auto_code: True") print(u"如果是linux无图形界面请使用自动打码is_auto_code: True")
print(u"如果没有弹出验证码请手动双击根目录下的tkcode.png文件") print(u"如果没有弹出验证码请手动双击根目录下的tkcode.png文件")
Ofset = raw_input(u"输入对应的验证码: ") Ofset = input(u"输入对应的验证码: ")
if isinstance(Ofset, list): if isinstance(Ofset, list):
select = Ofset select = Ofset
else: else:

41
inter/GetSuccessRate.py Normal file
View File

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

View File

@ -1,16 +1,12 @@
# coding=utf-8 # coding=utf-8
import copy import copy
import threading
import time
import random import random
import wrapcache import wrapcache
from config import urlConf from config import urlConf
from config.TicketEnmu import ticket from config.TicketEnmu import ticket
from myUrllib.httpUtils import HTTPClient from myUrllib.httpUtils import HTTPClient
from config.configCommon import seat_conf_2 from config.configCommon import seat_conf_2
from utils.timeUtil import time_to_minutes import TickerConfig
class query: 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, def __init__(self, session, from_station, to_station, from_station_h, to_station_h, _station_seat, station_trains,
ticke_peoples_num, station_dates=None, ): ticke_peoples_num, station_dates=None, ):
self.session = session self.session = session
self.httpClint = HTTPClient(session.is_proxy) self.httpClint = HTTPClient(TickerConfig.IS_PROXY)
self.urls = urlConf.urls self.urls = urlConf.urls
self.from_station = from_station self.from_station = from_station
self.to_station = to_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.station_dates = station_dates if isinstance(station_dates, list) else list(station_dates)
self.ticket_black_list = dict() self.ticket_black_list = dict()
self.ticke_peoples_num = ticke_peoples_num 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): def station_seat(self, index):
""" """
获取车票对应坐席 获取车票对应坐席
@ -57,37 +46,15 @@ class query:
} }
return seat[index] 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): def check_is_need_train(self, ticket_info):
if self.is_by_time: return ticket_info[3] in self.station_trains
return self.check_train_types(ticket_info[3]) and self.check_time_interval(ticket_info)
else:
return ticket_info[3] in self.station_trains
def sendQuery(self): def sendQuery(self):
""" """
查询 查询
:return: :return:
""" """
if self.session.is_cdn == 1: if TickerConfig.IS_CDN == 1:
if self.session.cdn_list: if self.session.cdn_list:
self.httpClint.cdn = self.session.cdn_list[random.randint(0, len(self.session.cdn_list) - 1)] self.httpClint.cdn = self.session.cdn_list[random.randint(0, len(self.session.cdn_list) - 1)]
for station_date in self.station_dates: for station_date in self.station_dates:
@ -101,7 +68,7 @@ class query:
continue continue
value = station_ticket.get("data", "") value = station_ticket.get("data", "")
if not value: 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.from_station_h,
self.to_station_h, self.to_station_h,
select_url["req_url"])) select_url["req_url"]))
@ -110,63 +77,73 @@ class query:
if result: if result:
for i in value['result']: for i in value['result']:
ticket_info = i.split('|') ticket_info = i.split('|')
if ticket_info[11] == "Y" and ticket_info[1] == "预订": # 筛选未在开始时间内的车次 if TickerConfig.TICKET_TYPE is 2 and self.check_is_need_train(ticket_info):
for j in self._station_seat: # 如果最后一位为1则是可以候补的不知道这些正确嘛
is_ticket_pass = ticket_info[j] if ticket_info[-2] == "1":
if is_ticket_pass != '' and is_ticket_pass != '' and is_ticket_pass != '*' and self.check_is_need_train( print("当前订单可以候补,尝试提交候补订单")
ticket_info): # 过滤有效目标车次 return {
secretStr = ticket_info[0] "secretStr": ticket_info[0],
train_no = ticket_info[2] "seat": TickerConfig.SET_TYPE,
query_from_station_name = ticket_info[6] "status": True,
query_to_station_name = ticket_info[7] }
train_location = ticket_info[15] elif TickerConfig.TICKET_TYPE is 1:
stationTrainCode = ticket_info[3] if ticket_info[11] == "Y" and ticket_info[1] == "预订": # 筛选未在开始时间内的车次
leftTicket = ticket_info[12] for j in self._station_seat:
start_time = ticket_info[8] is_ticket_pass = ticket_info[j]
arrival_time = ticket_info[9] if is_ticket_pass != '' and is_ticket_pass != '' and is_ticket_pass != '*' and self.check_is_need_train(
distance_time = ticket_info[10] ticket_info): # 过滤有效目标车次
print(start_time, arrival_time, distance_time) secretStr = ticket_info[0]
seat = j train_no = ticket_info[2]
try: query_from_station_name = ticket_info[6]
ticket_num = int(ticket_info[j]) query_to_station_name = ticket_info[7]
except ValueError: train_location = ticket_info[15]
ticket_num = "" stationTrainCode = ticket_info[3]
print(u'车次: {0} 始发车站: {1} 终点站: {2} {3}: {4}'.format(ticket_info[3], leftTicket = ticket_info[12]
self.from_station_h, start_time = ticket_info[8]
self.to_station_h, arrival_time = ticket_info[9]
seat_conf_2[j], distance_time = ticket_info[10]
ticket_num)) print(start_time, arrival_time, distance_time)
if wrapcache.get(train_no): seat = j
print(ticket.QUERY_IN_BLACK_LIST.format(train_no)) try:
continue ticket_num = int(ticket_info[j])
else: except ValueError:
if ticket_num != "" and self.ticke_peoples_num > ticket_num: ticket_num = ""
if self.session.is_more_ticket: print(u'车次: {0} 始发车站: {1} 终点站: {2} {3}: {4}'.format(ticket_info[3],
print( self.from_station_h,
u"余票数小于乘车人数,当前余票数: {}, 删减人车人数到: {}".format(ticket_num, ticket_num)) self.to_station_h,
is_more_ticket_num = ticket_num seat_conf_2[j],
else: ticket_num))
print(u"余票数小于乘车人数,当前设置不提交,放弃此次提交机会") if wrapcache.get(train_no):
continue print(ticket.QUERY_IN_BLACK_LIST.format(train_no))
continue
else: else:
print(u"设置乘车人数为: {}".format(self.ticke_peoples_num)) if ticket_num != "" and self.ticke_peoples_num > ticket_num:
is_more_ticket_num = self.ticke_peoples_num if self.session.is_more_ticket:
print(ticket.QUERY_C) print(
return { u"余票数小于乘车人数,当前余票数: {}, 删减人车人数到: {}".format(ticket_num, ticket_num))
"secretStr": secretStr, is_more_ticket_num = ticket_num
"train_no": train_no, else:
"stationTrainCode": stationTrainCode, print(u"余票数小于乘车人数,当前设置不提交,放弃此次提交机会")
"train_date": station_date, continue
"query_from_station_name": query_from_station_name, else:
"query_to_station_name": query_to_station_name, print(u"设置乘车人数为: {}".format(self.ticke_peoples_num))
"seat": seat, is_more_ticket_num = self.ticke_peoples_num
"leftTicket": leftTicket, print(ticket.QUERY_C)
"train_location": train_location, return {
"code": ticket.SUCCESS_CODE, "secretStr": secretStr,
"is_more_ticket_num": is_more_ticket_num, "train_no": train_no,
"cdn": self.httpClint.cdn, "stationTrainCode": stationTrainCode,
"status": True, "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: else:
print(u"车次配置信息有误,或者返回数据异常,请检查 {}".format(station_ticket)) print(u"车次配置信息有误,或者返回数据异常,请检查 {}".format(station_ticket))
return {"code": ticket.FAIL_CODE, "status": False, "cdn": self.httpClint.cdn, } return {"code": ticket.FAIL_CODE, "status": False, "cdn": self.httpClint.cdn, }

View File

@ -1,8 +1,11 @@
# coding=utf-8 # coding=utf-8
import datetime import datetime
import urllib import urllib
from collections import OrderedDict
import TickerConfig
from config.urlConf import urls
from inter.CheckOrderInfo import checkOrderInfo from inter.CheckOrderInfo import checkOrderInfo
from inter.ConfirmHB import confirmHB
from myException.ticketIsExitsException import ticketIsExitsException from myException.ticketIsExitsException import ticketIsExitsException
@ -19,6 +22,7 @@ class submitOrderRequest:
def __init__(self, session, secretStr, from_station, to_station, train_no, set_type, def __init__(self, session, secretStr, from_station, to_station, train_no, set_type,
passengerTicketStrList, oldPassengerStr, train_date, ticke_peoples): passengerTicketStrList, oldPassengerStr, train_date, ticke_peoples):
self.session = session self.session = session
# self.secretStr = secretStr
try: try:
self.secretStr = urllib.unquote(secretStr) self.secretStr = urllib.unquote(secretStr)
except AttributeError: except AttributeError:
@ -42,8 +46,9 @@ class submitOrderRequest:
('back_train_date', time()), # 返程时间 ('back_train_date', time()), # 返程时间
('tour_flag', 'dc'), # 旅途类型 ('tour_flag', 'dc'), # 旅途类型
('purpose_codes', 'ADULT'), # 成人票还是学生票 ('purpose_codes', 'ADULT'), # 成人票还是学生票
('query_from_station_name', self.from_station), # 起始车站 ('query_from_station_name', TickerConfig.FROM_STATION), # 起始车站
('query_to_station_name', self.to_station), # 终点车站 ('query_to_station_name', TickerConfig.TO_STATION), # 终点车站
('undefined', ''),
] ]
return data return data
@ -68,3 +73,37 @@ class submitOrderRequest:
print (u'出票失败') print (u'出票失败')
elif 'messages' in submitResult and submitResult['messages']: elif 'messages' in submitResult and submitResult['messages']:
raise ticketIsExitsException(submitResult['messages'][0]) 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()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 14 KiB