Files
py12306/old_py12306/user/job.py
2019-05-14 13:21:25 +08:00

522 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import pickle
import re
from os import path
from py12306.cluster.cluster import Cluster
from py12306.helpers.api import *
from py12306.app import *
from py12306.helpers.auth_code import AuthCode
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.log.order_log import OrderLog
from py12306.log.user_log import UserLog
from py12306.log.common_log import CommonLog
class UserJob:
# heartbeat = 60 * 2 # 心跳保持时长
is_alive = True
check_interval = 5
key = None
user_name = ''
password = ''
user = None
info = {} # 用户信息
last_heartbeat = None
is_ready = False
user_loaded = False # 用户是否已加载成功
passengers = []
retry_time = 3
login_num = 0 # 尝试登录次数
# Init page
global_repeat_submit_token = None
ticket_info_for_passenger_form = None
order_request_dto = None
cluster = None
lock_init_user_time = 3 * 60
cookie = False
def __init__(self, info):
self.cluster = Cluster()
self.init_data(info)
def init_data(self, info):
self.session = Request()
self.session.add_response_hook(self.response_login_check)
self.key = str(info.get('key'))
self.user_name = info.get('user_name')
self.password = info.get('password')
def update_user(self):
from py12306.user.user import User
self.user = User()
self.load_user()
def run(self):
# load user
self.update_user()
self.start()
def start(self):
"""
检测心跳
:return:
"""
while True and self.is_alive:
app_available_check()
if Config().is_slave():
self.load_user_from_remote()
else:
if Config().is_master() and not self.cookie: self.load_user_from_remote() # 主节点加载一次 Cookie
self.check_heartbeat()
if Const.IS_TEST: return
stay_second(self.check_interval)
def check_heartbeat(self):
# 心跳检测
if self.get_last_heartbeat() and (time_int() - self.get_last_heartbeat()) < Config().USER_HEARTBEAT_INTERVAL:
return True
# 只有主节点才能走到这
if self.is_first_time() or not self.check_user_is_login():
if not self.handle_login(): return
self.user_did_load()
message = UserLog.MESSAGE_USER_HEARTBEAT_NORMAL.format(self.get_name(), Config().USER_HEARTBEAT_INTERVAL)
UserLog.add_quick_log(message).flush()
def get_last_heartbeat(self):
if Config().is_cluster_enabled():
return int(self.cluster.session.get(Cluster.KEY_USER_LAST_HEARTBEAT, 0))
return self.last_heartbeat
def set_last_heartbeat(self, time=None):
time = time if time != None else time_int()
if Config().is_cluster_enabled():
self.cluster.session.set(Cluster.KEY_USER_LAST_HEARTBEAT, time)
self.last_heartbeat = time
# def init_cookies
def is_first_time(self):
if Config().is_cluster_enabled():
return not self.cluster.get_user_cookie(self.key)
return not path.exists(self.get_cookie_path())
def handle_login(self, expire=False):
if expire: UserLog.print_user_expired()
self.is_ready = False
UserLog.print_start_login(user=self)
return self.login()
def login(self):
"""
获取验证码结果
:return 权限校验码
"""
data = {
'username': self.user_name,
'password': self.password,
'appid': 'otn'
}
self.request_device_id()
answer = AuthCode.get_auth_code(self.session)
data['answer'] = answer
response = self.session.post(API_BASE_LOGIN.get('url'), data)
result = response.json()
if result.get('result_code') == 0: # 登录成功
"""
login 获得 cookie uamtk
auth/uamtk 不请求,会返回 uamtk票据内容为空
/otn/uamauthclient 能拿到用户名
"""
new_tk = self.auth_uamtk()
user_name = self.auth_uamauthclient(new_tk)
self.update_user_info({'user_name': user_name})
self.login_did_success()
return True
elif result.get('result_code') == 2: # 账号之内错误
# 登录失败,用户名或密码为空
# 密码输入错误
UserLog.add_quick_log(UserLog.MESSAGE_LOGIN_FAIL.format(result.get('result_message'))).flush()
else:
UserLog.add_quick_log(
UserLog.MESSAGE_LOGIN_FAIL.format(result.get('result_message', result.get('message',
CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR)))).flush()
return False
def check_user_is_login(self):
response = self.session.get(API_USER_LOGIN_CHECK)
is_login = response.json().get('data.is_login', False) == 'Y'
if is_login:
self.save_user()
self.set_last_heartbeat()
return self.get_user_info() # 检测应该是不会维持状态这里再请求下个人中心看有没有用01-10 看来应该是没用 01-22 有时拿到的状态 是已失效的再加上试试
return is_login
def auth_uamtk(self):
response = self.session.post(API_AUTH_UAMTK.get('url'), {'appid': 'otn'})
result = response.json()
if result.get('newapptk'):
return result.get('newapptk')
# TODO 处理获取失败情况
return False
def auth_uamauthclient(self, tk):
response = self.session.post(API_AUTH_UAMAUTHCLIENT.get('url'), {'tk': tk})
result = response.json()
if result.get('username'):
return result.get('username')
# TODO 处理获取失败情况
return False
def request_device_id(self):
"""
获取加密后的浏览器特征 ID
:return:
"""
params = {"algID": self.request_alg_id(), "timestamp": int(time.time() * 1000)}
params = dict(params, **self._get_hash_code_params())
response = self.session.get(API_GET_BROWSER_DEVICE_ID, params=params)
if response.text.find('callbackFunction') >= 0:
result = response.text[18:-2]
try:
result = json.loads(result)
self.session.cookies.update({
'RAIL_EXPIRATION': result.get('exp'),
'RAIL_DEVICEID': result.get('dfp'),
})
except:
return False
def request_alg_id(self):
response = self.session.get("https://kyfw.12306.cn/otn/HttpZF/GetJS")
result = re.search(r'algID\\x3d(.*?)\\x26', response.text)
try:
return result.group(1)
except (IndexError, AttributeError) as e:
pass
return ""
def _get_hash_code_params(self):
from collections import OrderedDict
data = {
'adblock': '0',
'browserLanguage': 'en-US',
'cookieEnabled': '1',
'custID': '133',
'doNotTrack': 'unknown',
'flashVersion': '0',
'javaEnabled': '0',
'jsFonts': 'c227b88b01f5c513710d4b9f16a5ce52',
'localCode': '3232236206',
'mimeTypes': '52d67b2a5aa5e031084733d5006cc664',
'os': 'MacIntel',
'platform': 'WEB',
'plugins': 'd22ca0b81584fbea62237b14bd04c866',
'scrAvailSize': str(random.randint(500, 1000)) + 'x1920',
'srcScreenSize': '24xx1080x1920',
'storeDb': 'i1l1o1s1',
'timeZone': '-8',
'touchSupport': '99115dfb07133750ba677d055874de87',
'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.' + str(
random.randint(
5000, 7000)) + '.0 Safari/537.36',
'webSmartID': 'f4e3b7b14cc647e30a6267028ad54c56',
}
data_trans = {
'browserVersion': 'd435',
'touchSupport': 'wNLf',
'systemLanguage': 'e6OK',
'scrWidth': 'ssI5',
'openDatabase': 'V8vl',
'scrAvailSize': 'TeRS',
'hasLiedResolution': '3neK',
'hasLiedOs': 'ci5c',
'timeZone': 'q5aJ',
'userAgent': '0aew',
'userLanguage': 'hLzX',
'jsFonts': 'EOQP',
'scrAvailHeight': '88tV',
'browserName': '-UVA',
'cookieCode': 'VySQ',
'online': '9vyE',
'scrAvailWidth': 'E-lJ',
'flashVersion': 'dzuS',
'scrDeviceXDPI': '3jCe',
'srcScreenSize': 'tOHY',
'storeDb': 'Fvje',
'doNotTrack': 'VEek',
'mimeTypes': 'jp76',
'sessionStorage': 'HVia',
'cookieEnabled': 'VPIf',
'os': 'hAqN',
'hasLiedLanguages': 'j5po',
'hasLiedBrowser': '2xC5',
'webSmartID': 'E3gR',
'appcodeName': 'qT7b',
'javaEnabled': 'yD16',
'plugins': 'ks0Q',
'appMinorVersion': 'qBVW',
'cpuClass': 'Md7A',
'indexedDb': '3sw-',
'adblock': 'FMQw',
'localCode': 'lEnu',
'browserLanguage': 'q4f3',
'scrHeight': '5Jwy',
'localStorage': 'XM7l',
'historyList': 'kU5z',
'scrColorDepth': "qmyu"
}
data = OrderedDict(data)
data_str = ''
params = {}
for key, item in data.items():
data_str += key + item
key = data_trans[key] if key in data_trans else key
params[key] = item
data_str = self._encode_data_str(data_str)
data_str_len = len(data_str)
data_str_f = int(data_str_len / 3) if data_str_len % 3 == 0 else int(data_str_len / 3) + 1
if data_str_len >= 3:
data_str = data_str[data_str_f:2*data_str_f] + data_str[2*data_str_f:data_str_len] + data_str[0: data_str_f]
data_str = data_str[::-1]
data_str_tmp = ""
for e in range(0, len(data_str)):
data_str_code = ord(data_str[e])
data_str_tmp += chr(0) if data_str_code == 127 else chr(data_str_code + 1)
data_str = self._encode_data_str(data_str_tmp)
data_str = self._encode_string(data_str)
params['hashCode'] = data_str
return params
def _encode_data_str(self, data_str):
data_str_len = len(data_str)
data_str_len_tmp = int(data_str_len / 3) if data_str_len % 3 == 0 else int(data_str_len / 3) + 1
if data_str_len >= 3:
data_str_e = data_str[0:data_str_len_tmp]
data_str_f = data_str[data_str_len_tmp:2 * data_str_len_tmp]
return data_str[2 * data_str_len_tmp:data_str_len] + data_str_e + data_str_f
return data_str
def _encode_string(self, str):
import hashlib
import base64
result = base64.b64encode(hashlib.sha256(str.encode()).digest()).decode()
return result.replace('+', '-').replace('/', '_').replace('=', '')
def login_did_success(self):
"""
用户登录成功
:return:
"""
self.login_num += 1
self.welcome_user()
self.save_user()
self.get_user_info()
self.set_last_heartbeat()
self.is_ready = True
def welcome_user(self):
UserLog.print_welcome_user(self)
pass
def get_cookie_path(self):
return Config().USER_DATA_DIR + self.user_name + '.cookie'
def update_user_info(self, info):
self.info = {**self.info, **info}
def get_name(self):
return self.info.get('user_name', '')
def save_user(self):
if Config().is_master():
self.cluster.set_user_cookie(self.key, self.session.cookies)
self.cluster.set_user_info(self.key, self.info)
with open(self.get_cookie_path(), 'wb') as f:
pickle.dump(self.session.cookies, f)
def did_loaded_user(self):
"""
恢复用户成功
:return:
"""
UserLog.add_quick_log(UserLog.MESSAGE_LOADED_USER.format(self.user_name)).flush()
if self.check_user_is_login() and self.get_user_info():
UserLog.add_quick_log(UserLog.MESSAGE_LOADED_USER_SUCCESS.format(self.user_name)).flush()
UserLog.print_welcome_user(self)
self.user_did_load()
else:
UserLog.add_quick_log(UserLog.MESSAGE_LOADED_USER_BUT_EXPIRED).flush()
self.set_last_heartbeat(0)
def user_did_load(self):
"""
用户已经加载成功
:return:
"""
self.is_ready = True
if self.user_loaded: return
self.user_loaded = True
Event().user_loaded({'key': self.key}) # 发布通知
def get_user_info(self):
response = self.session.get(API_USER_INFO.get('url'))
result = response.json()
user_data = result.get('data.userDTO.loginUserDTO')
# 子节点访问会导致主节点登录失效 TODO 可快考虑实时同步 cookie
if user_data:
self.update_user_info({**user_data, **{'user_name': user_data.get('name')}})
self.save_user()
return True
return False
def load_user(self):
if Config().is_cluster_enabled(): return
cookie_path = self.get_cookie_path()
if path.exists(cookie_path):
with open(self.get_cookie_path(), 'rb') as f:
cookie = pickle.load(f)
self.cookie = True
self.session.cookies.update(cookie)
self.did_loaded_user()
return True
return None
def load_user_from_remote(self):
cookie = self.cluster.get_user_cookie(self.key)
info = self.cluster.get_user_info(self.key)
if Config().is_slave() and (not cookie or not info):
while True: # 子节点只能取
UserLog.add_quick_log(UserLog.MESSAGE_USER_COOKIE_NOT_FOUND_FROM_REMOTE.format(self.user_name)).flush()
stay_second(self.retry_time)
return self.load_user_from_remote()
if info: self.info = info
if cookie:
self.session.cookies.update(cookie)
if not self.cookie: # 第一次加载
self.cookie = True
if not Config().is_slave():
self.did_loaded_user()
else:
self.is_ready = True # 设置子节点用户 已准备好
UserLog.print_welcome_user(self)
return True
return False
def check_is_ready(self):
return self.is_ready
def wait_for_ready(self):
if self.is_ready: return self
UserLog.add_quick_log(UserLog.MESSAGE_WAIT_USER_INIT_COMPLETE.format(self.retry_time)).flush()
stay_second(self.retry_time)
return self.wait_for_ready()
def destroy(self):
"""
退出用户
:return:
"""
UserLog.add_quick_log(UserLog.MESSAGE_USER_BEING_DESTROY.format(self.user_name)).flush()
self.is_alive = False
def response_login_check(self, response, **kwargs):
if Config().is_master() and response.json().get('data.noLogin') == 'true': # relogin
self.handle_login(expire=True)
def get_user_passengers(self):
if self.passengers: return self.passengers
response = self.session.post(API_USER_PASSENGERS)
result = response.json()
if result.get('data.normal_passengers'):
self.passengers = result.get('data.normal_passengers')
# 将乘客写入到文件
with open(Config().USER_PASSENGERS_FILE % self.user_name, 'w', encoding='utf-8') as f:
f.write(json.dumps(self.passengers, indent=4, ensure_ascii=False))
return self.passengers
else:
UserLog.add_quick_log(
UserLog.MESSAGE_GET_USER_PASSENGERS_FAIL.format(
result.get('messages', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR), self.retry_time)).flush()
if Config().is_slave():
self.load_user_from_remote() # 加载最新 cookie
stay_second(self.retry_time)
return self.get_user_passengers()
def get_passengers_by_members(self, members):
"""
获取格式化后的乘客信息
:param members:
:return:
[{
name: '项羽',
type: 1,
id_card: 0000000000000000000,
type_text: '成人'
}]
"""
self.get_user_passengers()
results = []
for member in members:
is_member_code = is_number(member)
if not is_member_code:
child_check = array_dict_find_by_key_value(results, 'name', member)
if not is_member_code and child_check:
new_member = child_check.copy()
new_member['type'] = UserType.CHILD
new_member['type_text'] = dict_find_key_by_value(UserType.dicts, int(new_member['type']))
else:
if is_member_code:
passenger = array_dict_find_by_key_value(self.passengers, 'code', member)
else:
passenger = array_dict_find_by_key_value(self.passengers, 'passenger_name', member)
if not passenger:
UserLog.add_quick_log(
UserLog.MESSAGE_USER_PASSENGERS_IS_INVALID.format(self.user_name, member)).flush()
return False
new_member = {
'name': passenger.get('passenger_name'),
'id_card': passenger.get('passenger_id_no'),
'id_card_type': passenger.get('passenger_id_type_code'),
'mobile': passenger.get('mobile_no'),
'type': passenger.get('passenger_type'),
'type_text': dict_find_key_by_value(UserType.dicts, int(passenger.get('passenger_type')))
}
results.append(new_member)
return results
def request_init_dc_page(self):
"""
请求下单页面 拿到 token
:return:
"""
data = {'_json_att': ''}
response = self.session.post(API_INITDC_URL, data)
html = response.text
token = re.search(r'var globalRepeatSubmitToken = \'(.+?)\'', html)
form = re.search(r'var ticketInfoForPassengerForm *= *(\{.+\})', html)
order = re.search(r'var orderRequestDTO *= *(\{.+\})', html)
# 系统忙,请稍后重试
if html.find('系统忙,请稍后重试') != -1:
OrderLog.add_quick_log(OrderLog.MESSAGE_REQUEST_INIT_DC_PAGE_FAIL).flush() # 重试无用,直接跳过
return False
try:
self.global_repeat_submit_token = token.groups()[0]
self.ticket_info_for_passenger_form = json.loads(form.groups()[0].replace("'", '"'))
self.order_request_dto = json.loads(order.groups()[0].replace("'", '"'))
except:
pass # TODO Error
return True