From 3446bfc9c26c0e1092282970d1871c22ec229216 Mon Sep 17 00:00:00 2001 From: Jalin Date: Sun, 6 Jan 2019 22:43:30 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=94=A8=E6=88=B7=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 6 +- py12306/config.py | 14 ++- py12306/helpers/OCR.py | 71 +++++++++++++++ py12306/helpers/api.py | 16 +++- py12306/helpers/auth_code.py | 69 +++++++++++++++ py12306/helpers/func.py | 5 ++ py12306/helpers/request.py | 20 +++++ py12306/log/common_log.py | 20 +++++ py12306/log/query_log.py | 2 +- py12306/log/user_log.py | 24 ++++- py12306/query/query.py | 3 +- py12306/user/job.py | 154 ++++++++++++++++++++++++++++----- py12306/user/user.py | 10 ++- py12306/vender/ruokuai/main.py | 54 ++++++++++++ 14 files changed, 434 insertions(+), 34 deletions(-) create mode 100644 py12306/helpers/OCR.py create mode 100644 py12306/helpers/auth_code.py create mode 100644 py12306/helpers/request.py create mode 100644 py12306/log/common_log.py create mode 100755 py12306/vender/ruokuai/main.py diff --git a/main.py b/main.py index d411963..be540e4 100644 --- a/main.py +++ b/main.py @@ -4,12 +4,14 @@ from threading import Thread from py12306.log.query_log import QueryLog from py12306.query.query import Query +from py12306.user.user import User def main(): # Thread(target=Query.run).start() # 余票查询 - QueryLog.add_log('init') - Query.run() + # QueryLog.add_log('init') + # Query.run() + User.run() pass diff --git a/py12306/config.py b/py12306/config.py index 0136d0b..be00cdd 100644 --- a/py12306/config.py +++ b/py12306/config.py @@ -17,6 +17,12 @@ USER_HEARTBEAT_INTERVAL = 120 # 多线程查询 QUERY_JOB_THREAD_ENABLED = 0 +# 打码平台账号 +AUTO_CODE_ACCOUNT = { + 'user': '', + 'pwd': '' +} + SEAT_TYPES = { '商务座': 32, '一等座': 31, @@ -28,10 +34,12 @@ SEAT_TYPES = { '无座': 26, } -# Query -QUERY_DATA_DIR = 'runtime/query' -USER_DATA_DIR = 'runtime/user' +PROJECT_DIR = path.dirname(path.dirname(path.abspath(__file__))) + '/' +# Query +RUNTIME_DIR = PROJECT_DIR + 'runtime/' +QUERY_DATA_DIR = RUNTIME_DIR + 'query/' +USER_DATA_DIR = RUNTIME_DIR + 'user/' STATION_FILE = 'data/stations.txt' CONFIG_FILE = 'env.py' diff --git a/py12306/helpers/OCR.py b/py12306/helpers/OCR.py new file mode 100644 index 0000000..1d250da --- /dev/null +++ b/py12306/helpers/OCR.py @@ -0,0 +1,71 @@ +from py12306 import config +from py12306.log.common_log import CommonLog +from py12306.vender.ruokuai.main import RKClient + + +class OCR: + """ + 图片识别 + """ + + @classmethod + def get_img_position(cls, img_path): + """ + 获取图像坐标 + :param img_path: + :return: + """ + self = cls() + return self.get_img_position_by_ruokuai(img_path) + pass + + def get_img_position_by_ruokuai(self, img_path): + ruokuai_account = config.AUTO_CODE_ACCOUNT + soft_id = '119671' + soft_key = '6839cbaca1f942f58d2760baba5ed987' + rc = RKClient(ruokuai_account.get('user'), ruokuai_account.get('pwd'), soft_id, soft_key) + im = open(img_path, 'rb').read() + result = rc.rk_create(im, 6113) + if "Result" in result: + return self.get_image_position_by_offset(list(result['Result'])) + CommonLog.print_auto_code_fail(result.get("Error", '-')) + return None + + def get_image_position_by_offset(self, offsets): + positions = [] + for offset in offsets: + if offset == '1': + y = 46 + x = 42 + elif offset == '2': + y = 46 + x = 105 + elif offset == '3': + y = 45 + x = 184 + elif offset == '4': + y = 48 + x = 256 + elif offset == '5': + y = 36 + x = 117 + elif offset == '6': + y = 112 + x = 115 + elif offset == '7': + y = 114 + x = 181 + elif offset == '8': + y = 111 + x = 252 + else: + pass + positions.append(x) + positions.append(y) + return positions + + + +if __name__ == '__main__': + pass + # code_result = AuthCode.get_auth_code() diff --git a/py12306/helpers/api.py b/py12306/helpers/api.py index e87a9b8..43a54d4 100644 --- a/py12306/helpers/api.py +++ b/py12306/helpers/api.py @@ -26,8 +26,22 @@ API_USER_CHECK = { "is_cdn": True, } +API_AUTH_CODE_DOWNLOAD = { + 'url': BASE_URL_OF_12306 + '/passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&_={random}' +} +API_AUTH_CODE_CHECK = { + 'url': BASE_URL_OF_12306 + '/passport/captcha/captcha-check?answer={answer}&rand=sjrand&login_site=E&_={random}' +} +API_AUTH_UAMTK = { + 'url': BASE_URL_OF_12306 + '/passport/web/auth/uamtk' +} +API_AUTH_UAMAUTHCLIENT = { + 'url': BASE_URL_OF_12306 + '/otn/uamauthclient' +} - +API_USER_INFO = { + 'url': BASE_URL_OF_12306 + '/otn/modifyUser/initQueryUserInfoApi' +} urls = { "auth": { # 登录接口 diff --git a/py12306/helpers/auth_code.py b/py12306/helpers/auth_code.py new file mode 100644 index 0000000..cf7151d --- /dev/null +++ b/py12306/helpers/auth_code.py @@ -0,0 +1,69 @@ +import random +import time + +from requests.exceptions import SSLError + +from py12306.helpers.OCR import OCR +from py12306.helpers.api import API_AUTH_CODE_DOWNLOAD, API_AUTH_CODE_CHECK +from py12306.helpers.request import Request +from py12306.helpers.func import * +from py12306.log.common_log import CommonLog +from py12306.log.user_log import UserLog + + +class AuthCode: + """ + 验证码类 + """ + session = None + data_path = config.RUNTIME_DIR + retry_time = 1 + + def __init__(self, session): + self.session = session + + @classmethod + def get_auth_code(cls, session): + self = cls(session) + img_path = self.download_code() + position = OCR.get_img_position(img_path) + answer = ','.join(map(str, position)) + if not self.check_code(answer): + time.sleep(self.retry_time) + return self.get_auth_code(session) + return position + + def download_code(self): + url = API_AUTH_CODE_DOWNLOAD.get('url').format(random=random.random()) + code_path = self.data_path + 'code.png' + try: + UserLog.add_quick_log(UserLog.MESSAGE_DOWNLAODING_THE_CODE).flush() + response = self.session.save_to_file(url, code_path) # TODO 返回错误情况 + except SSLError as e: + UserLog.add_quick_log( + UserLog.MESSAGE_DOWNLAOD_AUTH_CODE_FAIL.format(e, self.retry_time)).flush() + time.sleep(self.retry_time) + return self.download_code() + return code_path + + def check_code(self, answer): + """ + 校验验证码 + :return: + """ + url = API_AUTH_CODE_CHECK.get('url').format(answer=answer, random=random.random()) + response = self.session.get(url) + result = response.json() + if result.get('result_code') == '4': + UserLog.add_quick_log(UserLog.MESSAGE_CODE_AUTH_SUCCESS).flush() + return True + else: + UserLog.add_quick_log( + UserLog.MESSAGE_CODE_AUTH_FAIL.format(result.get('result_message'), self.retry_time)).flush() + self.session.cookies.clear_session_cookies() + + return False + + +if __name__ == '__main__': + code_result = AuthCode.get_auth_code() diff --git a/py12306/helpers/func.py b/py12306/helpers/func.py index a9e5d9e..60bd4ab 100644 --- a/py12306/helpers/func.py +++ b/py12306/helpers/func.py @@ -1,3 +1,4 @@ +import datetime import random import threading from time import sleep @@ -65,7 +66,11 @@ def stay_second(second): def is_main_thread(): return threading.current_thread() == threading.main_thread() + def current_thread_id(): return threading.current_thread().ident + +def time_now(): + return datetime.datetime.now() # def test: diff --git a/py12306/helpers/request.py b/py12306/helpers/request.py new file mode 100644 index 0000000..762432a --- /dev/null +++ b/py12306/helpers/request.py @@ -0,0 +1,20 @@ +from requests_html import HTMLSession + + +class Request(HTMLSession): + """ + 请求处理类 + """ + # session = {} + + # def __init__(self, mock_browser=True, session=None): + # super().__init__(mock_browser=mock_browser) + # self.session = session if session else HTMLSession() + pass + + def save_to_file(self, url, path): + response = self.get(url, stream=True) + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=1024): + f.write(chunk) + return response diff --git a/py12306/log/common_log.py b/py12306/log/common_log.py new file mode 100644 index 0000000..de0868e --- /dev/null +++ b/py12306/log/common_log.py @@ -0,0 +1,20 @@ +from py12306.log.base import BaseLog +from py12306.helpers.func import * + + +@singleton +class CommonLog(BaseLog): + + def __init__(self): + super().__init__() + self.init_data() + + def init_data(self): + print('Common Log 初始化') + + @classmethod + def print_auto_code_fail(cls, reason): + self = cls() + self.add_quick_log('打码失败: 错误原因 {reason}'.format(reason=reason)) + self.flush() + return self diff --git a/py12306/log/query_log.py b/py12306/log/query_log.py index f5a39b2..c10346f 100644 --- a/py12306/log/query_log.py +++ b/py12306/log/query_log.py @@ -12,7 +12,7 @@ class QueryLog(BaseLog): 'query_count': 1, 'last_time': '', } - data_path = config.QUERY_DATA_DIR + '/status.json' + data_path = config.QUERY_DATA_DIR + 'status.json' LOG_INIT_JOBS = '' diff --git a/py12306/log/user_log.py b/py12306/log/user_log.py index 6b5b6c9..22af8af 100644 --- a/py12306/log/user_log.py +++ b/py12306/log/user_log.py @@ -4,6 +4,15 @@ from py12306.helpers.func import * @singleton class UserLog(BaseLog): + MESSAGE_DOWNLAOD_AUTH_CODE_FAIL = '验证码下载失败 错误原因: {} {} 秒后重试' + MESSAGE_DOWNLAODING_THE_CODE = '正在下载验证码...' + MESSAGE_CODE_AUTH_FAIL = '验证码验证失败 错误原因: {} {} 秒后重试' + MESSAGE_CODE_AUTH_SUCCESS = '验证码验证成功 开始登录...' + MESSAGE_LOGIN_FAIL = '登录失败 错误原因: {}' + MESSAGE_LOADED_USER = '正在尝试恢复用户: {}' + MESSAGE_LOADED_USER_SUCCESS = '用户恢复成功: {}' + MESSAGE_LOADED_USER_BUT_EXPIRED = '用户状态已过期,正在重新登录' + MESSAGE_USER_HEARTBEAT_NORMAL = '用户 {} 心跳正常,下次检测 {} 秒后' def __init__(self): super().__init__() @@ -20,6 +29,19 @@ class UserLog(BaseLog): """ self = cls() self.add_log('================== 发现 {} 个用户 =================='.format(len(users))) - self.add_log('') + self.flush() + return self + + @classmethod + def print_welcome_user(cls, user): + self = cls() + self.add_log('# 欢迎回来,{} #'.format(user.get_name())) + self.flush() + return self + + @classmethod + def print_start_login(cls, user): + self = cls() + self.add_log('正在登录用户 {}'.format(user.user_name)) self.flush() return self diff --git a/py12306/query/query.py b/py12306/query/query.py index 819ba7c..4942b08 100644 --- a/py12306/query/query.py +++ b/py12306/query/query.py @@ -38,8 +38,7 @@ class Query: thread = threading.Thread(target=job.run) thread.start() threads.append(thread) - for thread in threads: - thread.join() + for thread in threads: thread.join() else: for job in self.jobs: job.run() diff --git a/py12306/user/job.py b/py12306/user/job.py index fd3caf5..a6251bb 100644 --- a/py12306/user/job.py +++ b/py12306/user/job.py @@ -1,68 +1,178 @@ +import pickle from os import path -from requests_html import HTMLSession - -from py12306.helpers.api import API_USER_CHECK, API_BASE_LOGIN +from py12306.helpers.api import API_USER_CHECK, API_BASE_LOGIN, API_AUTH_UAMTK, API_AUTH_UAMAUTHCLIENT, API_USER_INFO +from py12306.helpers.auth_code import AuthCode from py12306.helpers.func import * +from py12306.helpers.request import Request +from py12306.log.user_log import UserLog class UserJob: - heartbeat = 60 * 2 + heartbeat = 60 * 2 # 心跳保持时长 + heartbeat_interval = 5 key = None - user_name: '' - password: '' - user: None + user_name = '' + password = '' + user = None + info = {} # 用户信息 + last_heartbeat = None def __init__(self, info, user): - self.session = HTMLSession() - # cookie TODO + self.session = Request() self.heartbeat = user.heartbeat self.key = info.get('key') self.user_name = info.get('user_name') self.password = info.get('password') self.user = user + # load user + self.load_user() def run(self): self.start() def start(self): - self.check_heartbeat() + """ + 检测心跳 + :return: + """ + while True: + self.check_heartbeat() + sleep(self.heartbeat_interval) def check_heartbeat(self): + # 心跳检测 + if self.last_heartbeat and (time_now() - self.last_heartbeat).seconds < self.heartbeat: + return True if self.is_first_time() or not self.check_user_is_login(): self.handle_login() - pass + + UserLog.add_quick_log(UserLog.MESSAGE_USER_HEARTBEAT_NORMAL.format(self.get_name(), self.heartbeat)).flush() + self.last_heartbeat = time_now() # def init_cookies def is_first_time(self): - return not self.get_user_cookie() + return not path.exists(self.get_cookie_path()) def handle_login(self): - self.base_login() + UserLog.print_start_login(user=self) + self.login() - def base_login(self): + def login(self): """ 获取验证码结果 - :return: + :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() + elif result.get('result_code') == 2: # 账号之内错误 + # 登录失败,用户名或密码为空 + # 密码输入错误 + UserLog.add_quick_log(UserLog.MESSAGE_LOGIN_FAIL.format(result.get('result_message'))) + else: + UserLog.add_quick_log( + UserLog.MESSAGE_LOGIN_FAIL.format(result.get('result_message', result.get('message', '-')))) + + return False + pass def check_user_is_login(self): response = self.session.get(API_USER_CHECK.get('url')) - is_login = response.json().get('status') + is_login = response.json().get('data').get('flag', False) + if is_login: + self.save_user() + return is_login - def get_user_cookie(self): - path = self.get_cookie_path() - if path.exists(path): - return open(path, encoding='utf-8').read() - return None + 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.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' + 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): + 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)) + if self.check_user_is_login(): + UserLog.add_quick_log(UserLog.MESSAGE_LOADED_USER_SUCCESS.format(self.user_name)) + self.get_user_info() + UserLog.print_welcome_user(self) + else: + UserLog.add_quick_log(UserLog.MESSAGE_LOADED_USER_BUT_EXPIRED) + + def get_user_info(self): + response = self.session.get(API_USER_INFO.get('url')) + result = response.json() + user_data = result.get('data') + if user_data.get('userDTO') and user_data['userDTO'].get('loginUserDTO'): + user_data = user_data['userDTO']['loginUserDTO'] + self.update_user_info({**user_data, **{'user_name': user_data['name']}}) + return True + return None + + def load_user(self): + cookie_path = self.get_cookie_path() + if path.exists(cookie_path): + with open(self.get_cookie_path(), 'rb') as f: + self.session.cookies.update(pickle.load(f)) + self.did_loaded_user() + return True + return None diff --git a/py12306/user/user.py b/py12306/user/user.py index 22ced68..2e5f945 100644 --- a/py12306/user/user.py +++ b/py12306/user/user.py @@ -19,10 +19,16 @@ class User: def start(self): self.init_users() - UserLog.print_init_users(jobs=self.users) + UserLog.print_init_users(users=self.users) while True: + # 多线程维护用户 + threads = [] for user in self.users: - user.run() + thread = threading.Thread(target=user.run) + thread.start() + threads.append(thread) + # user.run() + for thread in threads: thread.join() def init_users(self): accounts = config.USER_ACCOUNTS diff --git a/py12306/vender/ruokuai/main.py b/py12306/vender/ruokuai/main.py new file mode 100755 index 0000000..8ff2cde --- /dev/null +++ b/py12306/vender/ruokuai/main.py @@ -0,0 +1,54 @@ +import requests +from hashlib import md5 + + +class RKClient(object): + + def __init__(self, username, password, soft_id, soft_key): + self.username = username + self.password = md5(password.encode('utf-8')).hexdigest() + self.soft_id = soft_id + self.soft_key = soft_key + self.base_params = { + 'username': self.username, + 'password': self.password, + 'softid': self.soft_id, + 'softkey': self.soft_key, + } + self.headers = { + 'Connection': 'Keep-Alive', + 'Expect': '100-continue', + 'User-Agent': 'ben', + } + + def rk_create(self, im, im_type, timeout=60): + """ + im: 图片字节 + im_type: 题目类型 + """ + params = { + 'typeid': im_type, + 'timeout': timeout, + } + params.update(self.base_params) + files = {'image': ('a.jpg', im)} + r = requests.post('http://api.ruokuai.com/create.json', data=params, files=files, headers=self.headers) + return r.json() + + def rk_report_error(self, im_id): + """ + im_id:报错题目的ID + """ + params = { + 'id': im_id, + } + params.update(self.base_params) + r = requests.post('http://api.ruokuai.com/reporterror.json', data=params, headers=self.headers) + return r.json() + + +if __name__ == '__main__': + rc = RKClient('username', 'password', 'soft_id', 'soft_key') + im = open('a.jpg', 'rb').read() + # print rc.rk_create(im, 3040) +