def __init__(self, info, query): self.cluster = Cluster() self.left_dates = info.get('left_dates') # 多车站已放在下面处理 # self.left_station = info.get('stations').get('left') # self.arrive_station = info.get('stations').get('arrive') # self.left_station_code = Station.get_station_key_by_name(self.left_station) # self.arrive_station_code = Station.get_station_key_by_name(self.arrive_station) self.stations = info.get('stations') self.stations = [self.stations] if isinstance(self.stations, dict) else self.stations self.job_name = info.get( 'job_name', '{} -> {}'.format(self.stations[0]['left'], self.stations[0]['arrive'])) self.account_key = str(info.get('account_key')) self.allow_seats = info.get('seats') self.allow_train_numbers = info.get('train_numbers') self.members = info.get('members') self.member_num = len(self.members) self.member_num_take = self.member_num self.allow_less_member = bool(info.get('allow_less_member')) self.interval = query.interval self.query = query
def print_configs(cls): # 打印配置 self = cls() enable = '已开启' disable = '未开启' self.add_quick_log('**** 当前配置 ****') self.add_quick_log('多线程查询: {}'.format( get_true_false_text(Config().QUERY_JOB_THREAD_ENABLED, enable, disable))) self.add_quick_log('语音验证码: {}'.format( get_true_false_text(Config().NOTIFICATION_BY_VOICE_CODE, enable, disable))) self.add_quick_log('查询间隔: {} 秒'.format(Config().QUERY_INTERVAL)) self.add_quick_log('用户心跳检测间隔: {} 秒'.format( Config().USER_HEARTBEAT_INTERVAL)) if Config().is_cluster_enabled(): from py12306.cluster.cluster import Cluster self.add_quick_log('分布式查询: {}'.format( get_true_false_text(Config().is_cluster_enabled(), enable, enable))) self.add_quick_log('节点名称: {}'.format(Cluster().node_name)) self.add_quick_log('节点是否主节点: {}'.format( get_true_false_text(Config().is_master(), '是', '否'))) self.add_quick_log('子节点提升为主节点: {}'.format( get_true_false_text(Config().NODE_SLAVE_CAN_BE_MASTER, enable, disable))) self.add_quick_log() self.flush() return self
def print_configs(cls): # 打印配置 self = cls() enable = '已开启' disable = '未开启' self.add_quick_log('**** 当前配置 ****') self.add_quick_log('多线程查询: {}'.format(get_true_false_text(Config().QUERY_JOB_THREAD_ENABLED, enable, disable))) self.add_quick_log('CDN 状态: {}'.format(get_true_false_text(Config().CDN_ENABLED, enable, disable))).flush() self.add_quick_log('通知状态:') self.add_quick_log( '语音验证码: {}'.format(get_true_false_text(Config().NOTIFICATION_BY_VOICE_CODE, enable, disable))) self.add_quick_log('邮件通知: {}'.format(get_true_false_text(Config().EMAIL_ENABLED, enable, disable))) self.add_quick_log('钉钉通知: {}'.format(get_true_false_text(Config().DINGTALK_ENABLED, enable, disable))) self.add_quick_log('Telegram通知: {}'.format(get_true_false_text(Config().TELEGRAM_ENABLED, enable, disable))) self.add_quick_log('ServerChan通知: {}'.format(get_true_false_text(Config().SERVERCHAN_ENABLED, enable, disable))) self.add_quick_log('Bark通知: {}'.format(get_true_false_text(Config().BARK_ENABLED, enable, disable))) self.add_quick_log( 'PushBear通知: {}'.format(get_true_false_text(Config().PUSHBEAR_ENABLED, enable, disable))).flush(sep='\t\t') self.add_quick_log('查询间隔: {} 秒'.format(Config().QUERY_INTERVAL)) self.add_quick_log('用户心跳检测间隔: {} 秒'.format(Config().USER_HEARTBEAT_INTERVAL)) self.add_quick_log('WEB 管理页面: {}'.format(get_true_false_text(Config().WEB_ENABLE, enable, disable))) if Config().is_cluster_enabled(): from py12306.cluster.cluster import Cluster self.add_quick_log('分布式查询: {}'.format(get_true_false_text(Config().is_cluster_enabled(), enable, enable))) self.add_quick_log('节点名称: {}'.format(Cluster().node_name)) self.add_quick_log('节点是否主节点: {}'.format(get_true_false_text(Config().is_master(), '是', '否'))) self.add_quick_log( '子节点提升为主节点: {}'.format(get_true_false_text(Config().NODE_SLAVE_CAN_BE_MASTER, enable, disable))) self.add_quick_log() self.flush() return self
def __init__(self): self.session = Request() self.request_device_id() self.cluster = Cluster() self.update_query_interval() self.update_query_jobs() self.get_query_api_type()
class Event(): """ 处理事件 """ # 事件 KEY_JOB_DESTROY = 'job_destroy' KEY_USER_JOB_DESTROY = 'user_job_destroy' KEY_USER_LOADED = 'user_loaded' cluster = None def __init__(self): from py12306.cluster.cluster import Cluster self.cluster = Cluster() def job_destroy(self, data={}, callback=False): # 停止查询任务 from py12306.query.query import Query if Config().is_cluster_enabled() and not callback: return self.cluster.publish_event(self.KEY_JOB_DESTROY, data) # 通知其它节点退出 job = Query.job_by_name(data.get('name')) if job: job.destroy() def user_loaded(self, data={}, callback=False): # 用户初始化完成 if Config().is_cluster_enabled() and not callback: return self.cluster.publish_event(self.KEY_USER_LOADED, data) # 通知其它节点退出 from py12306.query.query import Query if not Config().is_cluster_enabled() or Config().is_master(): query = Query.wait_for_ready() for job in query.jobs: if job.account_key == data.get('key'): create_thread_and_run(job, 'check_passengers', Const.IS_TEST) # 检查乘客信息 防止提交订单时才检查 stay_second(1) def user_job_destroy(self, data={}, callback=False): from py12306.user.user import User if Config().is_cluster_enabled() and not callback: return self.cluster.publish_event(self.KEY_JOB_DESTROY, data) # 通知其它节点退出 user = User.get_user(data.get('key')) if user: user.destroy()
def clusters(): """ 节点统计 节点数量,主节点,子节点列表 :return: """ from py12306.cluster.cluster import Cluster nodes = Cluster().nodes count = len(nodes) node_lists = list(nodes) master = [key for key, val in nodes.items() if int(val) == Cluster.KEY_MASTER] master = master[0] if master else '' return jsonify({ 'master': master, 'count': count, 'node_lists': ', '.join(node_lists) })
def flush(cls, sep='\n', end='\n', file=None, exit=False, publish=True): from py12306.cluster.cluster import Cluster self = cls() logs = self.get_logs() # 输出到文件 if file == None and Config().OUT_PUT_LOG_TO_FILE_ENABLED and not Const.IS_TEST: # TODO 文件无法写入友好提示 file = open(Config().OUT_PUT_LOG_TO_FILE_PATH, 'a', encoding='utf-8') if not file: file = None # 输出日志到各个节点 if publish and self.quick_log and Config().is_cluster_enabled() and Cluster().is_ready: # f = io.StringIO() with redirect_stdout(f): print(*logs, sep=sep, end='' if end == '\n' else end) out = f.getvalue() Cluster().publish_log_message(out) else: print(*logs, sep=sep, end=end, file=file) self.empty_logs(logs) if exit: sys.exit()
def handler_exit(self, *args, **kwargs): """ 程序退出 :param args: :param kwargs: :return: """ if Config.is_cluster_enabled(): from py12306.cluster.cluster import Cluster Cluster().left_cluster() sys.exit()
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'), # 'RAIL_EXPIRATION': '1554474881858', # 'RAIL_DEVICEID': 'AuT-Gn6_zBqrgut3m5pj-OtRGGHDAKXfCDKI3SlDT98JBD-XzxPH08tjaplcAW5aKb8nyX90r92psp5QpwRIGTn6XeIwiQxvuwEnqseza6mPSAu_gmrGCLVpFvCbDUky4EB-UTjDH-ozAHx1oaz5KkvGgakW0Jou', }) 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_len_div = int(data_str_len / 2) if data_str_len % 2 == 0: data_str = data_str[data_str_len_div:data_str_len] + data_str[ 0:data_str_len_div] else: data_str = data_str[data_str_len_div + 1:data_str_len] + \ data_str[data_str_len_div] + data_str[0:data_str_len_div] data_str = self._encode_data_str(data_str) 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_string(data_str_tmp[::-1]) 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_f + data_str[ 2 * data_str_len_tmp:data_str_len] + data_str_e 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
def __init__(self): self.cluster = Cluster() create_thread_and_run(self, 'watch_cdn', False)
def init_class(self): from py12306.cluster.cluster import Cluster if Config.is_cluster_enabled(): Cluster().run()
def is_master(): # 是不是 主 from py12306.cluster.cluster import Cluster return Config().CLUSTER_ENABLED and (Config().NODE_IS_MASTER or Cluster().is_master)
class Job: """ 查询任务 """ id = 0 is_alive = True job_name = None left_dates = [] left_date = None stations = [] left_station = '' arrive_station = '' left_station_code = '' arrive_station_code = '' from_time = timedelta(hours=0) to_time = timedelta(hours=24) account_key = 0 allow_seats = [] current_seat = None current_seat_name = '' current_order_seat = None allow_train_numbers = [] except_train_numbers = [] members = [] member_num = 0 member_num_take = 0 # 最终提交的人数 passengers = [] allow_less_member = False retry_time = 3 interval = {} interval_additional = 0 interval_additional_max = 5 query = None cluster = None ticket_info = {} is_cdn = False query_time_out = 3 INDEX_TICKET_NUM = 11 INDEX_TRAIN_NUMBER = 3 INDEX_TRAIN_NO = 2 INDEX_LEFT_DATE = 13 INDEX_LEFT_STATION = 6 # 4 5 始发 终点 INDEX_ARRIVE_STATION = 7 INDEX_ORDER_TEXT = 1 # 下单文字 INDEX_SECRET_STR = 0 INDEX_LEFT_TIME = 8 INDEX_ARRIVE_TIME = 9 def __init__(self, info, query): self.cluster = Cluster() self.query = query self.init_data(info) self.update_interval() def init_data(self, info): self.id = md5(info) self.left_dates = info.get('left_dates') self.stations = info.get('stations') self.stations = [self.stations] if isinstance(self.stations, dict) else self.stations if not self.job_name: # name 不能被修改 self.job_name = info.get( 'job_name', '{} -> {}'.format(self.stations[0]['left'], self.stations[0]['arrive'])) self.account_key = str(info.get('account_key')) self.allow_seats = info.get('seats') self.allow_train_numbers = info.get('train_numbers') self.except_train_numbers = info.get('except_train_numbers') self.members = list(map(str, info.get('members'))) self.member_num = len(self.members) self.member_num_take = self.member_num self.allow_less_member = bool(info.get('allow_less_member')) period = info.get('period') if isinstance(period, dict): if 'from' in period: parts = period['from'].split(':') if len(parts) == 2: self.from_time = timedelta(hours=int(parts[0]), seconds=int(parts[1])) if 'to' in period: parts = period['to'].split(':') if len(parts) == 2: self.to_time = timedelta(hours=int(parts[0]), seconds=int(parts[1])) def update_interval(self): self.interval = self.query.interval def run(self): self.start() def start(self): """ 处理单个任务 根据日期循环查询, 展示处理时间 :param job: :return: """ while True and self.is_alive: app_available_check() QueryLog.print_job_start(self.job_name) for station in self.stations: self.refresh_station(station) for date in self.left_dates: self.left_date = date response = self.query_by_date(date) self.handle_response(response) QueryLog.add_query_time_log( time=response.elapsed.total_seconds(), is_cdn=self.is_cdn) if not self.is_alive: return self.safe_stay() if is_main_thread(): QueryLog.flush(sep='\t\t', publish=False) if not Config().QUERY_JOB_THREAD_ENABLED: QueryLog.add_quick_log('').flush(publish=False) break else: QueryLog.add_log('\n').flush(sep='\t\t', publish=False) if Const.IS_TEST: return def query_by_date(self, date): """ 通过日期进行查询 :return: """ 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, self.arrive_station)) url = LEFT_TICKETS.get('url').format( left_date=date, left_station=self.left_station_code, arrive_station=self.arrive_station_code, type=self.query.api_type) if Config.is_cdn_enabled() and Cdn().is_ready: self.is_cdn = True return self.query.session.cdn_request(url, timeout=self.query_time_out, allow_redirects=False) self.is_cdn = False return self.query.session.get(url, timeout=self.query_time_out, allow_redirects=False) def handle_response(self, response): """ 错误判断 余票判断 小黑屋判断 座位判断 乘车人判断 :param result: :return: """ results = self.get_results(response) if not results: return False for result in results: self.ticket_info = ticket_info = result.split('|') if not self.is_trains_number_valid(): # 车次是否有效 continue QueryLog.add_log( QueryLog.MESSAGE_QUERY_LOG_OF_EVERY_TRAIN.format( self.get_info_of_train_number())) if not self.is_has_ticket(ticket_info): continue allow_seats = self.allow_seats if self.allow_seats else list( Config.SEAT_TYPES.values()) # 未设置 则所有可用 TODO 合法检测 self.handle_seats(allow_seats, ticket_info) if not self.is_alive: return def handle_seats(self, allow_seats, ticket_info): for seat in allow_seats: # 检查座位是否有票 self.set_seat(seat) ticket_of_seat = ticket_info[self.current_seat] if not self.is_has_ticket_by_seat(ticket_of_seat): # 座位是否有效 continue QueryLog.print_ticket_seat_available( left_date=self.get_info_of_left_date(), train_number=self.get_info_of_train_number(), seat_type=seat, rest_num=ticket_of_seat) if not self.is_member_number_valid(ticket_of_seat): # 乘车人数是否有效 if self.allow_less_member: self.member_num_take = int(ticket_of_seat) QueryLog.print_ticket_num_less_than_specified( ticket_of_seat, self) else: QueryLog.add_quick_log( QueryLog. MESSAGE_GIVE_UP_CHANCE_CAUSE_TICKET_NUM_LESS_THAN_SPECIFIED ).flush() continue if Const.IS_TEST: return # 检查完成 开始提交订单 QueryLog.print_ticket_available( left_date=self.get_info_of_left_date(), train_number=self.get_info_of_train_number(), rest_num=ticket_of_seat) if User.is_empty(): QueryLog.add_quick_log( QueryLog.MESSAGE_USER_IS_EMPTY_WHEN_DO_ORDER.format( self.retry_time)) return stay_second(self.retry_time) order_result = False user = self.get_user() if not user: QueryLog.add_quick_log( QueryLog.MESSAGE_ORDER_USER_IS_EMPTY.format( self.retry_time)) return stay_second(self.retry_time) lock_id = Cluster.KEY_LOCK_DO_ORDER + '_' + user.key if Config().is_cluster_enabled(): if self.cluster.get_lock( lock_id, Cluster.lock_do_order_time, {'node': self.cluster.node_name}): # 获得下单锁 order_result = self.do_order(user) if not order_result: # 下单失败,解锁 self.cluster.release_lock(lock_id) else: QueryLog.add_quick_log( QueryLog.MESSAGE_SKIP_ORDER.format( self.cluster.get_lock_info(lock_id).get('node'), user.user_name)) stay_second(self.retry_time) # 防止过多重复 else: order_result = self.do_order(user) # 任务已成功 通知集群停止任务 if order_result: Event().job_destroy({'name': self.job_name}) def do_order(self, user): self.check_passengers() order = Order(user=user, query=self) return order.order() def get_results(self, response): """ 解析查询返回结果 :param response: :return: """ if response.status_code != 200: QueryLog.print_query_error(response.reason, response.status_code) if self.interval_additional < self.interval_additional_max: self.interval_additional += self.interval.get('min') else: self.interval_additional = 0 result = response.json().get('data.result') return result if result else False def is_has_ticket(self, ticket_info): return self.get_info_of_ticket_num( ) == 'Y' and self.get_info_of_order_text() == '预订' def is_has_ticket_by_seat(self, seat): return seat != '' and seat != '无' and seat != '*' def is_trains_number_valid(self): train_left_time = self.get_info_of_train_left_time() time_parts = train_left_time.split(':') left_time = timedelta(hours=int(time_parts[0]), seconds=int(time_parts[1])) if left_time < self.from_time or left_time > self.to_time: return False if self.except_train_numbers: return self.get_info_of_train_number().upper() not in map( str.upper, self.except_train_numbers) if self.allow_train_numbers: return self.get_info_of_train_number().upper() in map( str.upper, self.allow_train_numbers) return True def is_member_number_valid(self, seat): return seat == '有' or self.member_num <= int(seat) def destroy(self): """ 退出任务 :return: """ from py12306.query.query import Query self.is_alive = False QueryLog.add_quick_log( QueryLog.MESSAGE_QUERY_JOB_BEING_DESTROY.format( self.job_name)).flush() # sys.exit(1) # 无法退出线程... # 手动移出jobs 防止单线程死循环 index = Query().jobs.index(self) Query().jobs.pop(index) def safe_stay(self): origin_interval = get_interval_num(self.interval) interval = origin_interval + self.interval_additional QueryLog.add_stay_log('%s + %s' % (origin_interval, self.interval_additional) if self.interval_additional else origin_interval) stay_second(interval) def set_passengers(self, passengers): UserLog.print_user_passenger_init_success(passengers) self.passengers = passengers def set_seat(self, seat): self.current_seat_name = seat self.current_seat = SeatType.dicts.get(seat) self.current_order_seat = OrderSeatType.dicts.get(seat) def get_user(self): user = User.get_user(self.account_key) # if not user.check_is_ready(): # 这里不需要检测了,后面获取乘客时已经检测过 # # # pass return user def check_passengers(self): if not self.passengers: QueryLog.add_quick_log( QueryLog.MESSAGE_CHECK_PASSENGERS.format( self.job_name)).flush() passengers = User.get_passenger_for_members( self.members, self.account_key) if passengers: self.set_passengers(passengers) else: # 退出当前查询任务 self.destroy() return True def refresh_station(self, station): self.left_station = station.get('left') self.arrive_station = station.get('arrive') self.left_station_code = Station.get_station_key_by_name( self.left_station) self.arrive_station_code = Station.get_station_key_by_name( self.arrive_station) # 提供一些便利方法 def get_info_of_left_date(self): return self.ticket_info[self.INDEX_LEFT_DATE] def get_info_of_ticket_num(self): return self.ticket_info[self.INDEX_TICKET_NUM] def get_info_of_train_number(self): return self.ticket_info[self.INDEX_TRAIN_NUMBER] def get_info_of_train_no(self): return self.ticket_info[self.INDEX_TRAIN_NO] def get_info_of_left_station(self): return Station.get_station_name_by_key( self.ticket_info[self.INDEX_LEFT_STATION]) def get_info_of_arrive_station(self): return Station.get_station_name_by_key( self.ticket_info[self.INDEX_ARRIVE_STATION]) def get_info_of_order_text(self): return self.ticket_info[self.INDEX_ORDER_TEXT] def get_info_of_secret_str(self): return self.ticket_info[self.INDEX_SECRET_STR] def get_info_of_train_left_time(self): return self.ticket_info[self.INDEX_LEFT_TIME] def get_info_of_train_arrive_time(self): return self.ticket_info[self.INDEX_ARRIVE_TIME]
def __init__(self): super().__init__() self.data_path = Config().QUERY_DATA_DIR + 'status.json' self.cluster = Cluster()
def __init__(self): self.session = Request() self.cluster = Cluster() self.update_query_interval() self.update_query_jobs()
def __init__(self): self.cluster = Cluster() self.heartbeat = Config().USER_HEARTBEAT_INTERVAL self.update_interval() self.update_user_accounts()
def __init__(self, info): self.cluster = Cluster() self.init_data(info)
def get_remote_config(self): if not self.is_cluster_enabled(): return from py12306.cluster.cluster import Cluster return Cluster().session.get_pickle(Cluster().KEY_CONFIGS, {})
def __init__(self): from py12306.cluster.cluster import Cluster self.cluster = Cluster()
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() print(result) 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.init_my_12306_api() 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 init_my_12306_api(self): response = self.session.post(API_AUTH_INITMy12306Api.get('url')) result = response.json() print(result) print(result.get('data')['user_name']) # if result.get('data')['user_name']: # return result.get('data')['user_name'] # return False def request_device_id(self): """ 获取加密后的浏览器特征 ID :return: """ response = self.session.get(API_GET_BROWSER_DEVICE_ID) if response.status_code == 200: try: result = json.loads(response.text) self.session.cookies.update(result) self.session.cookies.update({ 'RAIL_EXPIRATION': '1569235953446', 'RAIL_DEVICEID': 'AZbdkCY6rk-b3J_W98mcINAH-YjQBFH1V3Km1ksO793jzhyDkQS4_u25heGonaHws5pml1x0ZB8L2pmcUct6rztKmJV0FNmDDAJD4Fe4Y_azJhhfS4JNuvBiEO_3tvYRKgicqV75RdzYSClHbZh0AyDo4iD8V5Bf', }) except: return False 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: '成人', enc_str: 'aaaaaa' }] """ 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'))), 'enc_str': passenger.get('allEncStr') } 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: return False # TODO Error return True
def __init__(self, info, query): self.cluster = Cluster() self.query = query self.init_data(info) self.update_interval()
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: """ """ TODO 通过程序进行加密,目前不清楚固定参数能使用多久 encrypt js address: https://kyfw.12306.cn/otn/HttpZF/GetJS line:2716 """ params = { "algID": "ozy7Gbfya4", "hashCode": "SPPnxwJaxslzp6PLz38mV_078n44WjSOp7vTgvQpGxA", "FMQw": "0", "q4f3": "zh-CN", "VySQ": "FGGlO7IzXMj0sfYT705RPKxtnYIHB5MI", "VPIf": "1", "custID": "133", "VEek": "unknown", "dzuS": "0", "yD16": "0", "EOQP": "c227b88b01f5c513710d4b9f16a5ce52", "lEnu": "167838050", "jp76": "52d67b2a5aa5e031084733d5006cc664", "hAqN": "MacIntel", "platform": "WEB", "ks0Q": "d22ca0b81584fbea62237b14bd04c866", "TeRS": "878x1440", "tOHY": "24xx900x1440", "Fvje": "i1l1o1s1", "q5aJ": "-8", "wNLf": "99115dfb07133750ba677d055874de87", "0aew": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36", "E3gR": "8b9855470c623dc9bd6e495a1417f6f8", "timestamp": int(time.time() * 1000) } 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 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
def save_to_remote(self): if not self.is_master(): return from py12306.cluster.cluster import Cluster Cluster().session.set_pickle(Cluster().KEY_CONFIGS, self.envs)
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.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() # if not Const.IS_TEST: 测试模块下也可以从文件中加载用户 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() pass 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(): self.is_ready = False if not self.handle_login(): return self.set_last_heartbeat() self.is_ready = True self.user_did_load() message = UserLog.MESSAGE_USER_HEARTBEAT_NORMAL.format( self.get_name(), Config().USER_HEARTBEAT_INTERVAL) if not Config.is_cluster_enabled(): UserLog.add_quick_log(message).flush() else: self.cluster.publish_log_message(message) # self.set_last_heartbeat() 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): if Config().is_cluster_enabled(): return self.cluster.session.set( Cluster.KEY_USER_LAST_HEARTBEAT, time if time != None else time_int()) self.last_heartbeat = time_int() # 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): UserLog.print_start_login(user=self) return self.login() def login(self): """ 获取验证码结果 :return 权限校验码 """ data = { 'username': self.user_name, 'password': self.password, 'appid': 'otn' } 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', '-')))).flush() return False pass def check_user_is_login(self): response = self.session.get(API_USER_CHECK.get('url')) is_login = response.json().get('data.flag', False) if is_login: self.save_user() self.set_last_heartbeat() # self.get_user_info() # 检测应该是不会维持状态,这里再请求下个人中心看有没有用,01-10 看来应该是没用 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 login_did_success(self): """ 用户登录成功 :return: """ self.login_num += 1 self.welcome_user() self.save_user() self.get_user_info() pass 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() self.is_ready = True 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: """ 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 None 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: 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 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') return self.passengers else: UserLog.add_quick_log( UserLog.MESSAGE_GET_USER_PASSENGERS_FAIL.format( result.get('messages', '-'), self.retry_time)).flush() 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: child_check = array_dict_find_by_key_value(results, 'name', member) if 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: 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
def __init__(self): self.cluster = Cluster() self.update_interval() self.update_user_accounts()
class UserJob: # heartbeat = 60 * 2 # 心跳保持时长 is_alive = True check_interval = 5 key = None user_name = '' password = '' type = 'qr' user = None info = {} # 用户信息 last_heartbeat = None is_ready = False user_loaded = False # 用户是否已加载成功 passengers = [] retry_time = 3 retry_count = 0 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') self.type = info.get('type') 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) if self.type == 'qr': return self.qr_login() else: return self.login() def login(self): """ 获取验证码结果 :return 权限校验码 """ data = { 'username': self.user_name, 'password': self.password, 'appid': 'otn' } answer = AuthCode.get_auth_code(self.session) data['answer'] = answer self.request_device_id() response = self.session.post(API_BASE_LOGIN.get('url'), data) result = response.json() if result.get('result_code') == 0: # 登录成功 """ 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 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' 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'}, headers={ 'Referer': 'https://kyfw.12306.cn/otn/passport?redirect=/otn/login/userLogin', 'Origin': 'https://kyfw.12306.cn' }) result = response.json() if result.get('newapptk'): return result.get('newapptk') # 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: """ response = self.session.get(API_GET_BROWSER_DEVICE_ID) if response.status_code == 200: try: result = json.loads(response.text) headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36" } self.session.headers.update(headers) response = self.session.get(base64.b64decode(result['id']).decode()) if response.text.find('callbackFunction') >= 0: result = response.text[18:-2] result = json.loads(result) if not Config().is_cache_rail_id_enabled(): self.session.cookies.update({ 'RAIL_EXPIRATION': result.get('exp'), 'RAIL_DEVICEID': result.get('dfp'), }) else: self.session.cookies.update({ 'RAIL_EXPIRATION': Config().RAIL_EXPIRATION, 'RAIL_DEVICEID': Config().RAIL_DEVICEID, }) except: return False def login_did_success(self): """ 用户登录成功 :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: '成人', enc_str: 'aaaaaa' }] """ self.get_user_passengers() results = [] for member in members: is_member_code = is_number(member) if not is_member_code: if member[0] == "*": audlt = 1 member = member[1:] else: audlt = 0 child_check = array_dict_find_by_key_value(results, 'name', member) if not is_member_code and child_check: new_member = child_check.copy() 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 audlt: passenger['passenger_type'] = UserType.ADULT if not passenger: UserLog.add_quick_log( UserLog.MESSAGE_USER_PASSENGERS_IS_INVALID.format(self.user_name, member)).flush() 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'))), 'enc_str': passenger.get('allEncStr') } 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, 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, False, html # TODO Error 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