22 Commits
v2.0 ... master

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

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

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

Signed-off-by: Gardel <sunxinao@hotmail.com>
2021-09-25 17:35:09 +08:00
Gardel
68a508e30a 添加扫码登录功能
Signed-off-by: Gardel <sunxinao@hotmail.com>
2021-09-24 21:36:37 +08:00
pjialin
49d35aabdc Merge pull request #401 from DingGHui/master
修改调用submitApi返回值判断
2021-01-07 10:02:16 +08:00
DingGuangHui
9e529c1f9f 修改调用submitApi返回值判断 2021-01-06 16:59:17 +08:00
Jalin
6b6bf41a51 更新为清华 pypi 源 #358 2020-01-10 20:56:07 +08:00
pjialin
badc8b8c31 Merge pull request #357 from pjialin/fix/drag-captcha
更新滑动验证码识别
2020-01-10 20:06:35 +08:00
Jalin
fbeb7835e5 更新滑动验证码识别 2020-01-10 20:02:33 +08:00
pjialin
f9a0f20e41 Merge pull request #352 from IronXiao/master
增加使用浏览器缓存RAIL_EXPIRATION 和 RAIL_DEVICEID 配置,解决临时无法登录问题
2020-01-07 14:49:39 +08:00
IronXiao
fd8c500128 增加用户本地打码平台配置 2020-01-06 16:34:01 +08:00
IronXiao
da1a0903c5 增加使用浏览器缓存RAIL_EXPIRATION 和 RAIL_DEVICEID 配置,解决临时无法登录问题 2020-01-06 15:24:50 +08:00
pjialin
8d0a6f05b7 Merge pull request #349 from pjialin/revert-325-szhang01/fix_env_overwrite
Revert "[szhang01/fix_env_overwrite] Comment out unnecessary setting in env.py.example"
2020-01-06 13:25:19 +08:00
pjialin
c82a94de39 Revert "[szhang01/fix_env_overwrite] Comment out unnecessary setting in env.py.example" 2020-01-06 12:28:32 +08:00
Jalin
0316e52e90 Fix 验证码下载失败,返回数据为空 #262 #297 2020-01-03 12:18:30 +08:00
18 changed files with 464 additions and 46 deletions

View File

@@ -6,12 +6,14 @@ USER_ACCOUNTS = [
{
'key': 0, # 如使用多个账号 key 不能重复
'user_name': 'your user name',
'password': 'your password'
'password': '忽略',
'type': 'qr' # qr 为扫码登录,填写其他为密码登录
},
# {
# 'key': 'wangwu',
# 'user_name': 'wangwu@qq.com',
# 'password': 'wangwu'
# 'password': 'wangwu',
# 'type': ''
# }
]
@@ -28,7 +30,8 @@ QUERY_JOB_THREAD_ENABLED = 0 # 是否开启多线程查询,开启后第个任
# 打码平台账号
# 目前只支持免费打码接口 和 若快打码注册地址http://www.ruokuai.com/login
AUTO_CODE_PLATFORM = 'free' # 免费填写 free 若快 ruokuai # 免费打码无法保证持续可用,如失效请手动切换
AUTO_CODE_PLATFORM = 'free' # 免费填写 free 若快 ruokuai # 免费打码无法保证持续可用,如失效请手动切换 #个人本地打码填写 user,并修改 API_USER_CODE_QCR_API
API_USER_CODE_QCR_API = ''
AUTO_CODE_ACCOUNT = {
'user': 'your user name',
'pwd': 'your password'

View File

@@ -6,12 +6,14 @@ USER_ACCOUNTS = [
{
'key': 0, # 如使用多个账号 key 不能重复
'user_name': 'your user name',
'password': 'your password'
'password': '忽略',
'type': 'qr' # qr 为扫码登录,填写其他为密码登录
},
# {
# 'key': 'wangwu',
# 'user_name': 'wangwu@qq.com',
# 'password': 'wangwu'
# 'password': 'wangwu',
# 'type': ''
# }
]
@@ -28,7 +30,8 @@ QUERY_JOB_THREAD_ENABLED = 0 # 是否开启多线程查询,开启后第个任
# 打码平台账号
# 目前只支持免费打码接口 和 若快打码注册地址http://www.ruokuai.com/login
AUTO_CODE_PLATFORM = 'free' # 免费填写 free 若快 ruokuai # 免费打码无法保证持续可用,如失效请手动切换
AUTO_CODE_PLATFORM = 'free' # 免费填写 free 若快 ruokuai # 免费打码无法保证持续可用,如失效请手动切换; 个人打码填写 user 并修改API_USER_CODE_QCR_API 为自己地址
API_USER_CODE_QCR_API = ''
AUTO_CODE_ACCOUNT = { # 使用 free 可用省略
'user': 'your user name',
'pwd': 'your password'
@@ -77,9 +80,7 @@ BARK_ENABLED = 0
BARK_PUSH_URL = 'https://api.day.app/:your_token'
# 输出日志到文件
# 默认设置不需要related issue: https://github.com/pjialin/py12306/issues/319
# 如需更改再 uncomment 掉下面这行
# OUT_PUT_LOG_TO_FILE_ENABLED = 0
OUT_PUT_LOG_TO_FILE_ENABLED = 0
OUT_PUT_LOG_TO_FILE_PATH = 'runtime/12306.log' # 日志目录
# 分布式集群配置
@@ -111,6 +112,11 @@ WEB_PORT = 8008 # 监听端口
CDN_ENABLED = 0
CDN_CHECK_TIME_OUT = 1 # 检测单个 cdn 是否可用超时时间
# 是否使用浏览器缓存中的RAIL_EXPIRATION 和 RAIL_DEVICEID
CACHE_RAIL_ID_ENABLED = 0
RAIL_EXPIRATION = '' #浏览12306 网站中的Cache的RAIL_EXPIRATION 值
RAIL_DEVICEID = '' #浏览12306 网站中的Cache的RAIL_DEVICEID 值
# 查询任务
QUERY_JOBS = [
{

View File

@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
import os
import signal
import sys
@@ -11,16 +10,19 @@ from py12306.log.order_log import OrderLog
def app_available_check():
# return True # Debug
if Config().IS_DEBUG:
return True
now = time_now()
if (now.hour >= 23 and now.minute >= 30) or now.hour < 6:
if now.weekday() == 1 and (now.hour > 23 and now.minute > 30 or now.hour < 5):
CommonLog.add_quick_log(CommonLog.MESSAGE_12306_IS_CLOSED.format(time_now())).flush()
open_time = datetime.datetime(now.year, now.month, now.day, 6)
open_time = datetime.datetime(now.year, now.month, now.day, 5)
if open_time < now:
open_time += datetime.timedelta(1)
sleep((open_time - now).seconds)
elif 1 < now.hour < 5:
CommonLog.add_quick_log(CommonLog.MESSAGE_12306_IS_CLOSED.format(time_now())).flush()
open_time = datetime.datetime(now.year, now.month, now.day, 5)
sleep((open_time - now).seconds)
return True
@@ -81,7 +83,7 @@ class App:
@classmethod
def check_auto_code(cls):
if Config().AUTO_CODE_PLATFORM == 'free': return True
if Config().AUTO_CODE_PLATFORM == 'free' or Config().AUTO_CODE_PLATFORM == 'user': return True
if not Config().AUTO_CODE_ACCOUNT.get('user') or not Config().AUTO_CODE_ACCOUNT.get('pwd'):
return False
return True

View File

@@ -22,6 +22,8 @@ class Config:
QUERY_JOB_THREAD_ENABLED = 0
# 打码平台账号
AUTO_CODE_PLATFORM = ''
#用户打码平台地址
API_USER_CODE_QCR_API = ''
AUTO_CODE_ACCOUNT = {'user': '', 'pwd': ''}
# 输出日志到文件
OUT_PUT_LOG_TO_FILE_ENABLED = 0
@@ -94,6 +96,10 @@ class Config:
CDN_ITEM_FILE = PROJECT_DIR + 'data/cdn.txt'
CDN_ENABLED_AVAILABLE_ITEM_FILE = QUERY_DATA_DIR + 'available.json'
CACHE_RAIL_ID_ENABLED = 0
RAIL_EXPIRATION = ''
RAIL_DEVICEID = ''
# Default time out
TIME_OUT_OF_REQUEST = 5
@@ -213,6 +219,10 @@ class Config:
def is_cdn_enabled():
return Config().CDN_ENABLED
@staticmethod
def is_cache_rail_id_enabled():
return Config().CACHE_RAIL_ID_ENABLED
class EnvLoader:
envs = []

View File

@@ -25,7 +25,7 @@ class OCR:
:return:
"""
self = cls()
if Config().AUTO_CODE_PLATFORM == 'free':
if Config().AUTO_CODE_PLATFORM == 'free' or Config().AUTO_CODE_PLATFORM == 'user':
return self.get_image_by_free_site(img)
return self.get_img_position_by_ruokuai(img)
@@ -58,7 +58,10 @@ class OCR:
data = {
'img': img
}
response = self.session.post(API_FREE_CODE_QCR_API, data=data, timeout=30)
if Config().AUTO_CODE_PLATFORM == 'free':
response = self.session.post(API_FREE_CODE_QCR_API, data=data, timeout=30)
else:
response = self.session.post(Config().API_USER_CODE_QCR_API, data=data, timeout=30)
result = response.json()
if result.get('msg') == 'success':
pos = result.get('result')

View File

@@ -1,6 +1,4 @@
# coding=utf-8
# 查询余票
import time
HOST_URL_OF_12306 = 'kyfw.12306.cn'
BASE_URL_OF_12306 = 'https://' + HOST_URL_OF_12306
@@ -15,6 +13,18 @@ API_BASE_LOGIN = {
API_USER_LOGIN_CHECK = BASE_URL_OF_12306 + '/otn/login/conf'
API_AUTH_QRCODE_BASE64_DOWNLOAD = {
'url': BASE_URL_OF_12306 + '/passport/web/create-qr64'
}
API_AUTH_QRCODE_CHECK = {
'url': BASE_URL_OF_12306 + '/passport/web/checkqr'
}
API_USER_LOGIN = {
'url': BASE_URL_OF_12306 + '/otn/login/userLogin'
}
API_AUTH_CODE_DOWNLOAD = {
'url': BASE_URL_OF_12306 + '/passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&_={random}'
}

View File

@@ -47,6 +47,7 @@ class AuthCode:
url = API_AUTH_CODE_BASE64_DOWNLOAD.format(random=random.random())
# code_path = self.data_path + 'code.png'
try:
self.session.cookies.clear_session_cookies()
UserLog.add_quick_log(UserLog.MESSAGE_DOWNLAODING_THE_CODE).flush()
# response = self.session.save_to_file(url, code_path) # TODO 返回错误情况
response = self.session.get(url)

View File

@@ -33,6 +33,11 @@ class Notification():
self = cls()
self.send_email_by_smtp(to, title, content)
@classmethod
def send_email_with_qrcode(cls, to, title='', qrcode_path=''):
self = cls()
self.send_email_by_smtp_with_qrcode(to, title, qrcode_path)
@classmethod
def send_to_telegram(cls, content=''):
self = cls()
@@ -134,6 +139,46 @@ class Notification():
except Exception as e:
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_EMAIL_FAIL.format(e)).flush()
def send_email_by_smtp_with_qrcode(self, to, title, qrcode_path):
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
to = to if isinstance(to, list) else [to]
message = MIMEMultipart()
message['Subject'] = title
message['From'] = Config().EMAIL_SENDER
message['To'] = ", ".join(to)
htmlFile = """
<html>
<head></head>
<body>
<p>
这是你的二维码
</p>
<p>
<br /><img src="cid:0", width=200, height=200 ></p>
</body>
</html>
"""
htmlApart = MIMEText(htmlFile, 'html')
imageFile = qrcode_path
imageApart = MIMEImage(open(imageFile, 'rb').read(), imageFile.split('.')[-1])
imageApart.add_header('Content-ID', '<0>')
message.attach(imageApart)
message.attach(htmlApart)
try:
server = smtplib.SMTP(Config().EMAIL_SERVER_HOST)
server.ehlo()
server.starttls()
server.login(Config().EMAIL_SERVER_USER, Config().EMAIL_SERVER_PASSWORD)
server.send_message(message)
server.quit()
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_EMAIL_WITH_QRCODE_SUCCESS).flush()
self.push_bark(CommonLog.MESSAGE_SEND_EMAIL_WITH_QRCODE_SUCCESS)
except Exception as e:
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_EMAIL_FAIL.format(e)).flush()
def send_dingtalk_by_webbook(self, content):
from dingtalkchatbot.chatbot import DingtalkChatbot
webhook = Config().DINGTALK_WEBHOOK

106
py12306/helpers/qrcode.py Normal file
View File

@@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
import png
def print_qrcode(path):
"""
将二维码输出到控制台
需要终端尺寸足够大才能显示
:param path: 二维码图片路径 (PNG 格式)
:return: None
"""
reader = png.Reader(path)
width, height, rows, info = reader.read()
lines = list(rows)
planes = info['planes'] # 通道数
threshold = (2 ** info['bitdepth']) / 2 # 色彩阈值
# 识别二维码尺寸
x_flag = -1 # x 边距标志
y_flag = -1 # y 边距标志
x_white = -1 # 定位图案白块 x 坐标
y_white = -1 # 定位图案白块 y 坐标
i = y_flag
while i < height:
if y_white > 0 and x_white > 0:
break
j = x_flag
while j < width:
total = 0
for k in range(planes):
px = lines[i][j * planes + k]
total += px
avg = total / planes
black = avg < threshold
if y_white > 0 and x_white > 0:
break
if x_flag > 0 > x_white and not black:
x_white = j
if x_flag == -1 and black:
x_flag = j
if y_flag > 0 > y_white and not black:
y_white = i
if y_flag == -1 and black:
y_flag = i
if x_flag > 0 and y_flag > 0:
i += 1
j += 1
i += 1
assert y_white - y_flag == x_white - x_flag
scale = y_white - y_flag
assert width - x_flag == height - y_flag
module_count = int((width - x_flag * 2) / scale)
whole_white = ''
whole_black = ' '
down_black = ''
up_black = ''
dual_flag = False
last_line = []
output = '\n'
for i in range(module_count + 2):
output += up_black
output += '\n'
i = y_flag
while i < height - y_flag:
if dual_flag:
output += whole_white
t = 0
j = x_flag
while j < width - x_flag:
total = 0
for k in range(planes):
px = lines[i][j * planes + k]
total += px
avg = total / planes
black = avg < threshold
if dual_flag:
last_black = last_line[t]
if black and last_black:
output += whole_black
elif black and not last_black:
output += down_black
elif not black and last_black:
output += up_black
elif not black and not last_black:
output += whole_white
else:
last_line[t:t+1] = [black]
t = t + 1
j += scale
if dual_flag:
output += whole_white + '\n'
dual_flag = not dual_flag
i += scale
output += whole_white
for i in range(module_count):
output += up_black if last_line[i] else whole_white
output += whole_white + '\n'
print(output, flush=True)

View File

@@ -77,3 +77,18 @@ class Request(HTMLSession):
url = url.replace(HOST_URL_OF_12306, cdn)
return self.request(method, url, headers={'Host': HOST_URL_OF_12306}, verify=False, **kwargs)
def dump_cookies(self):
cookies = []
for _, item in self.cookies._cookies.items():
for _, urls in item.items():
for _, cookie in urls.items():
from http.cookiejar import Cookie
assert isinstance(cookie, Cookie)
if cookie.domain:
cookies.append({
'name': cookie.name,
'value': cookie.value,
'url': 'https://' + cookie.domain + cookie.path,
})
return cookies

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
import asyncio
import urllib
# from py12306.config import UserType
from pyppeteer import launch
from py12306.config import Config
from py12306.helpers.api import *
from py12306.helpers.func import *
@@ -10,6 +13,73 @@ from py12306.log.common_log import CommonLog
from py12306.log.order_log import OrderLog
class DomBounding:
def __init__(self, rect: dict) -> None:
super().__init__()
self.x = rect['x']
self.y = rect['y']
self.width = rect['width']
self.height = rect['height']
@singleton
class Browser:
def __init__(self) -> None:
super().__init__()
def request_init_slide(self, session, html):
""" 处理滑块,拿到 session_id, sig """
OrderLog.add_quick_log('正在识别滑动验证码...').flush()
return asyncio.get_event_loop_policy().new_event_loop().run_until_complete(
self.__request_init_slide(session, html))
async def __request_init_slide(self, session, html):
""" 异步获取 """
browser = await launch(headless=True, autoClose=True, handleSIGINT=False, handleSIGTERM=False,
handleSIGHUP=False)
page = await browser.newPage()
await page.setViewport({'width': 1200, 'height': 1080})
await page.setRequestInterception(True)
load_js = """() => {
__old = navigator.userAgent; navigator.__defineGetter__('userAgent', () => __old.replace('Headless', ''));
__old = navigator.appVersion; navigator.__defineGetter__('appVersion', () => __old.replace('Headless', ''));
var __newProto = navigator.__proto__; delete __newProto.webdriver; navigator.__proto__ = __newProto;
}"""
source_url = 'https://kyfw.12306.cn/otn'
html = html.replace('href="/otn', f'href="{source_url}').replace('src="/otn', f'src="{source_url}')
@page.on('framenavigated')
async def on_frame_navigated(_):
await page.evaluate(load_js)
@page.on('request')
async def on_request(req):
if req.url.startswith(API_INITDC_URL):
if req.isNavigationRequest():
await page.setCookie(*session.dump_cookies())
return await req.respond({'body': html})
return await req.continue_()
await page.goto(API_INITDC_URL, timeout=30000)
slide_btn = await page.waitForSelector('#slide_passcode .nc-lang-cnt', timeout=30000)
rect = await slide_btn.boundingBox()
pos = DomBounding(rect)
pos.x += 5
pos.y += 10
await page.mouse.move(pos.x, pos.y)
await page.mouse.down()
await page.mouse.move(pos.x + pos.width, pos.y, steps=30)
await page.mouse.up()
# 等待获取 session id
await page.evaluate(
'async () => {let i = 3 * 10; while (!csessionid && i >= 0) await new Promise(resolve => setTimeout(resolve, 100), i--);}')
ret = await page.evaluate('JSON.stringify({session_id: csessionid, sig: sig})')
await page.close()
await browser.close()
return json.loads(ret)
class Order:
"""
处理下单
@@ -41,6 +111,7 @@ class Order:
assert isinstance(user, UserJob)
self.query_ins = query
self.user_ins = user
self.is_slide = False
self.make_passenger_ticket_str()
@@ -63,9 +134,20 @@ class Order:
return self.order_did_success()
elif not order_request_res:
return
if not self.user_ins.request_init_dc_page():
init_res, self.is_slide, init_html = self.user_ins.request_init_dc_page()
if not init_res:
return
if not self.check_order_info():
slide_info = {}
if self.is_slide:
try:
slide_info = Browser().request_init_slide(self.session, init_html)
if not slide_info.get('session_id') or not slide_info.get('sig'):
raise Exception()
except Exception:
OrderLog.add_quick_log('滑动验证码识别失败').flush()
return
OrderLog.add_quick_log('滑动验证码识别成功').flush()
if not self.check_order_info(slide_info):
return
if not self.get_queue_count():
return
@@ -89,7 +171,8 @@ class Order:
# num = 0 # 通知次数
# sustain_time = self.notification_sustain_time
info_message = OrderLog.get_order_success_notification_info(self.query_ins)
normal_message = OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_OF_EMAIL_CONTENT.format(self.order_id, self.user_ins.user_name)
normal_message = OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_OF_EMAIL_CONTENT.format(self.order_id,
self.user_ins.user_name)
if Config().EMAIL_ENABLED: # 邮件通知
Notification.send_email(Config().EMAIL_RECEIVER, OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_TITLE,
normal_message + info_message)
@@ -104,7 +187,7 @@ class Order:
Notification.push_bear(Config().PUSHBEAR_KEY, OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_TITLE,
normal_message + info_message)
if Config().BARK_ENABLED:
Notification.push_bark(normal_message+info_message)
Notification.push_bark(normal_message + info_message)
if Config().NOTIFICATION_BY_VOICE_CODE: # 语音通知
if Config().NOTIFICATION_VOICE_CODE_TYPE == 'dingxin':
@@ -142,7 +225,7 @@ class Order:
}
response = self.session.post(API_SUBMIT_ORDER_REQUEST, data)
result = response.json()
if result.get('data') == 'N':
if result.get('data') == '0':
OrderLog.add_quick_log(OrderLog.MESSAGE_SUBMIT_ORDER_REQUEST_SUCCESS).flush()
return True
else:
@@ -156,7 +239,7 @@ class Order:
result.get('messages', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR))).flush()
return False
def check_order_info(self):
def check_order_info(self, slide_info=None):
"""
cancel_flag=2
bed_level_order_num=000000000000000000000000000000
@@ -179,6 +262,12 @@ class Order:
'_json_att': '',
'REPEAT_SUBMIT_TOKEN': self.user_ins.global_repeat_submit_token
}
if self.is_slide:
data.update({
'sessionId': slide_info['session_id'],
'sig': slide_info['sig'],
'scene': 'nc_login',
})
response = self.session.post(API_CHECK_ORDER_INFO, data)
result = response.json()
if result.get('data.submitStatus'): # 成功
@@ -255,7 +344,7 @@ class Order:
if ticket_number != '充足' and int(ticket_number) <= 0:
if self.query_ins.current_seat == SeatType.NO_SEAT: # 允许无座
ticket_number = ticket[1]
if not int(ticket_number): # 跳过无座
if not int(ticket_number): # 跳过无座
OrderLog.add_quick_log(OrderLog.MESSAGE_GET_QUEUE_INFO_NO_SEAT).flush()
return False

View File

@@ -1,4 +1,3 @@
import sys
from datetime import timedelta
from datetime import datetime
@@ -154,14 +153,14 @@ class Job:
QueryLog.add_quick_log(msg).flush(publish=False)
raise RuntimeError(msg)
else:
pass
return date_query.strftime("%Y-%m-%d")
def query_by_date(self, date):
"""
通过日期进行查询
:return:
"""
self.judge_date_legal(date)
date = self.judge_date_legal(date)
from py12306.helpers.cdn import Cdn
QueryLog.add_log(('\n' if not is_main_thread() else '') + QueryLog.MESSAGE_QUERY_START_BY_DATE.format(date,
self.left_station,

View File

@@ -1,3 +1,4 @@
from base64 import b64decode
from py12306.config import Config
from py12306.cluster.cluster import Cluster
from py12306.app import app_available_check
@@ -5,7 +6,7 @@ from py12306.helpers.func import *
from py12306.helpers.request import Request
from py12306.log.query_log import QueryLog
from py12306.query.job import Job
from py12306.helpers.api import API_QUERY_INIT_PAGE
from py12306.helpers.api import API_QUERY_INIT_PAGE, API_GET_BROWSER_DEVICE_ID
@singleton
@@ -29,6 +30,7 @@ class Query:
def __init__(self):
self.session = Request()
self.request_device_id()
self.cluster = Cluster()
self.update_query_interval()
self.update_query_jobs()
@@ -117,6 +119,32 @@ class Query:
self.jobs.append(job)
return job
def request_device_id(self):
"""
获取加密后的浏览器特征 ID
:return:
"""
response = self.session.get(API_GET_BROWSER_DEVICE_ID)
if response.status_code == 200:
try:
result = json.loads(response.text)
response = self.session.get(b64decode(result['id']).decode())
if response.text.find('callbackFunction') >= 0:
result = response.text[18:-2]
result = json.loads(result)
if not Config().is_cache_rail_id_enabled():
self.session.cookies.update({
'RAIL_EXPIRATION': result.get('exp'),
'RAIL_DEVICEID': result.get('dfp'),
})
else:
self.session.cookies.update({
'RAIL_EXPIRATION': Config().RAIL_EXPIRATION,
'RAIL_DEVICEID': Config().RAIL_DEVICEID,
})
except:
return False
@classmethod
def wait_for_ready(cls):
self = cls()
@@ -154,6 +182,9 @@ class Query:
self.api_type = res.group(1)
except IndexError:
pass
if not self.api_type:
QueryLog.add_quick_log('查询地址获取失败, 正在重新获取...').flush()
sleep(1)
return cls.get_query_api_type()
# def get_jobs_from_cluster(self):

View File

@@ -1,4 +1,4 @@
import json
import base64
import pickle
import re
from os import path
@@ -11,6 +11,7 @@ from py12306.helpers.event import Event
from py12306.helpers.func import *
from py12306.helpers.request import Request
from py12306.helpers.type import UserType
from py12306.helpers.qrcode import print_qrcode
from py12306.log.order_log import OrderLog
from py12306.log.user_log import UserLog
from py12306.log.common_log import CommonLog
@@ -23,6 +24,7 @@ class UserJob:
key = None
user_name = ''
password = ''
type = 'qr'
user = None
info = {} # 用户信息
last_heartbeat = None
@@ -30,6 +32,7 @@ class UserJob:
user_loaded = False # 用户是否已加载成功
passengers = []
retry_time = 3
retry_count = 0
login_num = 0 # 尝试登录次数
# Init page
@@ -51,6 +54,7 @@ class UserJob:
self.key = str(info.get('key'))
self.user_name = info.get('user_name')
self.password = info.get('password')
self.type = info.get('type')
def update_user(self):
from py12306.user.user import User
@@ -111,7 +115,10 @@ class UserJob:
if expire: UserLog.print_user_expired()
self.is_ready = False
UserLog.print_start_login(user=self)
return self.login()
if self.type == 'qr':
return self.qr_login()
else:
return self.login()
def login(self):
"""
@@ -150,6 +157,82 @@ class UserJob:
return False
def qr_login(self):
self.request_device_id()
image_uuid, png_path = self.download_code()
while True:
data = {
'RAIL_DEVICEID': self.session.cookies.get('RAIL_DEVICEID'),
'RAIL_EXPIRATION': self.session.cookies.get('RAIL_EXPIRATION'),
'uuid': image_uuid,
'appid': 'otn'
}
response = self.session.post(API_AUTH_QRCODE_CHECK.get('url'), data)
result = response.json()
result_code = int(result.get('result_code'))
if result_code == 0:
time.sleep(2)
elif result_code == 1:
UserLog.add_quick_log('请确认登录').flush()
time.sleep(2)
elif result_code == 2:
break
elif result_code == 3:
try:
os.remove(png_path)
except Exception as e:
UserLog.add_quick_log('无法删除文件: {}'.format(e)).flush()
image_uuid = self.download_code()
try:
os.remove(png_path)
except Exception as e:
UserLog.add_quick_log('无法删除文件: {}'.format(e)).flush()
self.session.get(API_USER_LOGIN, allow_redirects=True)
new_tk = self.auth_uamtk()
user_name = self.auth_uamauthclient(new_tk)
self.update_user_info({'user_name': user_name})
self.session.get(API_USER_LOGIN, allow_redirects=True)
self.login_did_success()
return True
def download_code(self):
try:
UserLog.add_quick_log(UserLog.MESSAGE_QRCODE_DOWNLOADING).flush()
response = self.session.post(API_AUTH_QRCODE_BASE64_DOWNLOAD.get('url'), data={'appid': 'otn'})
result = response.json()
if result.get('result_code') == '0':
img_bytes = base64.b64decode(result.get('image'))
try:
os.mkdir(Config().USER_DATA_DIR + '/qrcode')
except FileExistsError:
pass
png_path = path.normpath(Config().USER_DATA_DIR + '/qrcode/%d.png' % time.time())
with open(png_path, 'wb') as file:
file.write(img_bytes)
file.close()
if os.name == 'nt':
os.startfile(png_path)
else:
print_qrcode(png_path)
UserLog.add_log(UserLog.MESSAGE_QRCODE_DOWNLOADED.format(png_path)).flush()
Notification.send_email_with_qrcode(Config().EMAIL_RECEIVER, '你有新的登录二维码啦!', png_path)
self.retry_count = 0
return result.get('uuid'), png_path
raise KeyError('获取二维码失败: {}'.format(result.get('result_message')))
except Exception as e:
UserLog.add_quick_log(
UserLog.MESSAGE_QRCODE_FAIL.format(e, self.retry_time)).flush()
self.retry_count = self.retry_count + 1
if self.retry_count == 20:
self.retry_count = 0
try:
os.remove(self.get_cookie_path())
except:
pass
time.sleep(self.retry_time)
return self.download_code()
def check_user_is_login(self):
response = self.session.get(API_USER_LOGIN_CHECK)
is_login = response.json().get('data.is_login', False) == 'Y'
@@ -189,18 +272,23 @@ class UserJob:
try:
result = json.loads(response.text)
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36"
}
from base64 import b64decode
self.session.headers.update(headers)
response = self.session.get(b64decode(result['id']).decode())
response = self.session.get(base64.b64decode(result['id']).decode())
if response.text.find('callbackFunction') >= 0:
result = response.text[18:-2]
result = json.loads(result)
self.session.cookies.update({
'RAIL_EXPIRATION': result.get('exp'),
'RAIL_DEVICEID': result.get('dfp'),
})
if not Config().is_cache_rail_id_enabled():
self.session.cookies.update({
'RAIL_EXPIRATION': result.get('exp'),
'RAIL_DEVICEID': result.get('dfp'),
})
else:
self.session.cookies.update({
'RAIL_EXPIRATION': Config().RAIL_EXPIRATION,
'RAIL_DEVICEID': Config().RAIL_DEVICEID,
})
except:
return False
@@ -411,12 +499,16 @@ class UserJob:
# 系统忙,请稍后重试
if html.find('系统忙,请稍后重试') != -1:
OrderLog.add_quick_log(OrderLog.MESSAGE_REQUEST_INIT_DC_PAGE_FAIL).flush() # 重试无用,直接跳过
return False
return False, False, html
try:
self.global_repeat_submit_token = token.groups()[0]
self.ticket_info_for_passenger_form = json.loads(form.groups()[0].replace("'", '"'))
self.order_request_dto = json.loads(order.groups()[0].replace("'", '"'))
except:
return False # TODO Error
return False, False, html # TODO Error
return True
slide_val = re.search(r"var if_check_slide_passcode.*='(\d?)'", html)
is_slide = False
if slide_val:
is_slide = int(slide_val[1]) == 1
return True, is_slide, html

View File

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

View File

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