diff --git a/py12306/config.py b/py12306/config.py index 5c88a51..4e390bb 100644 --- a/py12306/config.py +++ b/py12306/config.py @@ -7,6 +7,9 @@ USER_ACCOUNTS = [] # 查询任务 QUERY_JOBS = [] +# 查询间隔 +QUERY_INTERVAL = 1 + SEAT_TYPES = { '商务座': 32, diff --git a/py12306/helpers/func.py b/py12306/helpers/func.py index 103dd9e..64aff76 100644 --- a/py12306/helpers/func.py +++ b/py12306/helpers/func.py @@ -1,3 +1,6 @@ +import random +from time import sleep + from py12306 import config import functools @@ -36,4 +39,25 @@ def get_seat_name_by_number(number): return [k for k, v in config.SEAT_TYPES.items() if v == number].pop() +# 初始化间隔 +def init_interval_by_number(number): + if isinstance(number, dict): + min = float(number.get('min')) + max = float(number.get('max')) + else: + min = number / 2 + max = number + return { + 'min': min, + 'max': max + } + + +def get_interval_num(interval, decimal=2): + return round(random.uniform(interval.get('min'), interval.get('max')), decimal) + + +def stay_second(second): + sleep(second) + # def test: diff --git a/py12306/log/base.py b/py12306/log/base.py index 20cb175..a16cb17 100644 --- a/py12306/log/base.py +++ b/py12306/log/base.py @@ -3,6 +3,7 @@ from py12306.helpers.func import * class BaseLog: logs = [] + quick_log = [] @classmethod def add_log(cls, content): @@ -11,8 +12,19 @@ class BaseLog: return self @classmethod - def flush(cls): + def flush(cls, sep='\n', end='\n', file=None): self = cls() - for i in self.logs: - print(i) + logs = self.quick_log if self.quick_log else self.logs + # for i in logs: + print(*logs, sep=sep, end=end, file=file) + if self.quick_log: + self.quick_log = [] + else: + self.logs = [] # print(self.logs) + + @classmethod + def add_quick_log(cls, content): + self = cls() + self.quick_log.append(content) + return self diff --git a/py12306/log/query_log.py b/py12306/log/query_log.py index 6b27c6b..25f57ed 100644 --- a/py12306/log/query_log.py +++ b/py12306/log/query_log.py @@ -1,3 +1,4 @@ +import datetime import json from os import path from py12306.log.base import BaseLog @@ -7,12 +8,17 @@ from py12306.helpers.func import * @singleton class QueryLog(BaseLog): data = { - 'query_count': 0, + 'query_count': 1, 'last_time': '', } + data_path = config.QUERY_DATA_DIR + '/status.json' LOG_INIT_JOBS = '' + MESSAGE_GIVE_UP_CHANCE_CAUSE_TICKET_NUM_LESS_THAN_SPECIFIED = '余票数小于乘车人数,放弃此次提交机会' + MESSAGE_QUERY_LOG_OF_EVERY_TRAIN = '{}-{}' + MESSAGE_QUERY_START_BY_DATE = '出发日期 {}: {} - {}' + def __init__(self): super().__init__() self.init_data() @@ -20,12 +26,12 @@ class QueryLog(BaseLog): def init_data(self): # 获取上次记录 print('Query Log 初始化') - file_path = config.QUERY_DATA_DIR + '/status.json' - if path.exists(file_path): - result = open(file_path, encoding='utf-8').read() + if path.exists(self.data_path): + result = open(self.data_path, encoding='utf-8').read() if result: result = json.loads(result) self.data = {**self.data, **result} + self.print_data_restored() @classmethod def print_init_jobs(cls, jobs): @@ -34,7 +40,7 @@ class QueryLog(BaseLog): 输出初始化信息 :return: """ - self.add_log('# 发现任务 {} 条 #'.format(len(jobs))) + self.add_log('# 发现 {} 个任务 #'.format(len(jobs))) index = 1 for job in jobs: self.add_log('================== 任务 {} =================='.format(index)) @@ -45,5 +51,69 @@ class QueryLog(BaseLog): self.add_log('筛选车次:{}'.format(','.join(job.allow_train_numbers))) # 乘车日期:['2019-01-24', '2019-01-25', '2019-01-26', '2019-01-27'] index += 1 - + self.add_log('') + self.flush() return self + + @classmethod + def print_ticket_num_less_than_specified(cls, rest_num, job): + self = cls() + self.add_quick_log( + '余票数小于乘车人数,当前余票数: {rest_num}, 实际人数 {actual_num}, 删减人车人数到: {take_num}'.format(rest_num=rest_num, + actual_num=job.member_num, + take=job.member_num_take)) + self.flush() + return self + + @classmethod + def print_ticket_seat_available(cls, left_date, train_number, seat_type, rest_num): + self = cls() + self.add_quick_log( + '查询到座位可用 出发时间 {left_date} 车次 {train_number} 座位类型 {seat_type} 余票数量 {rest_num}'.format(left_date=left_date, + train_number=train_number, + seat_type=seat_type, + rest_num=rest_num)) + self.flush() + return self + + @classmethod + def print_query_error(cls, reason, code=None): + self = cls() + self.add_quick_log('查询余票请求失败') + if code: + self.add_quick_log('状态码{} '.format(code)) + if reason: + self.add_quick_log('错误原因{} '.format(reason)) + self.flush(sep='\t') + return self + + @classmethod + def print_job_start(cls): + self = cls() + self.add_quick_log('=== 正在进行第 {query_count} 次查询 === {time}'.format(query_count=self.data.get('query_count'), time=datetime.datetime.now())) + self.refresh_data() + self.flush() + return self + + @classmethod + def add_stay_log(cls, second): + self = cls() + self.add_log('安全停留 {}'.format(second)) + return self + + def print_data_restored(self): + self.add_quick_log('============================================================') + self.add_quick_log('|=== 查询记录恢复成功 上次查询 {last_date} ===|'.format(last_date=self.data.get('last_time'))) + self.add_quick_log('============================================================') + self.add_log('') + self.flush() + return self + + def refresh_data(self): + self.data['query_count'] += 1 + self.data['last_time'] = str(datetime.datetime.now()) + self.save_data() + + def save_data(self): + with open(self.data_path, 'w') as file: + file.write(json.dumps(self.data)) diff --git a/py12306/query/job.py b/py12306/query/job.py index 579e7cd..630ffe4 100644 --- a/py12306/query/job.py +++ b/py12306/query/job.py @@ -1,4 +1,7 @@ +from py12306.helpers.api import LEFT_TICKETS from py12306.helpers.station import Station +from py12306.log.query_log import QueryLog +from py12306.helpers.func import * class Job: @@ -15,10 +18,19 @@ class Job: allow_seats = [] allow_train_numbers = [] members = [] - member_num = [] + member_num = 0 + member_num_take = 0 # 最终提交的人数 + allow_less_member = False + + interval = {} + + query = None + INDEX_TICKET_NUM = 11 + INDEX_TRAIN_NUMBER = 3 + INDEX_LEFT_DATE = 13 - def __init__(self, info): + def __init__(self, info, query): self.left_dates = info.get('left_dates') self.left_station = info.get('stations').get('left') self.arrive_station = info.get('stations').get('arrive') @@ -29,3 +41,110 @@ class Job: 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 run(self): + self.start() + + def start(self): + """ + 处理单个任务 + 根据日期循环查询 + + 展示处理时间 + :param job: + :return: + """ + QueryLog.print_job_start() + for date in self.left_dates: + response = self.query_by_date(date) + self.handle_response(response) + self.safe_stay() + QueryLog.flush(sep='\t\t') + QueryLog.add_quick_log('').flush() + + def query_by_date(self, date): + """ + 通过日期进行查询 + :return: + """ + QueryLog.add_log(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='leftTicket/queryZ') + + return self.query.session.get(url) + + def handle_response(self, response): + """ + 错误判断 + 余票判断 + 小黑屋判断 + 座位判断 + 乘车人判断 + :param result: + :return: + """ + results = self.get_results(response) + if not results: + return False + for result in results: + ticket_info = result.split('|') + if not self.is_trains_number_valid(ticket_info): # 车次是否有效 + continue + QueryLog.add_log(QueryLog.MESSAGE_QUERY_LOG_OF_EVERY_TRAIN.format(ticket_info[self.INDEX_TRAIN_NUMBER], ticket_info[self.INDEX_TICKET_NUM])) + if not self.is_has_ticket(ticket_info): + continue + allow_seats = self.allow_seats if self.allow_seats else list(config.SEAT_TYPES.values()) # 未设置 则所有可用 + for seat in allow_seats: # 检查座位是否有票 + ticket_of_seat = ticket_info[get_seat_number_by_name(seat)] + if not self.is_has_ticket_by_seat(ticket_of_seat): # 座位是否有效 + continue + QueryLog.print_ticket_seat_available(left_date=ticket_info[self.INDEX_LEFT_DATE], train_number=ticket_info[self.INDEX_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 + # 检查完成 开始提交订单 + print('检查完成 开始提交订单') + + def get_results(self, response): + """ + 解析查询返回结果 + :param response: + :return: + """ + if response.status_code != 200: + QueryLog.print_query_error(response.reason, response.status_code) + try: + result_data = response.json().get('data', {}) + result = result_data.get('result', []) + except: + pass # TODO + return result if result else False + + def is_has_ticket(self, ticket_info): + return ticket_info[11] == 'Y' and ticket_info[1] == '预订' + + def is_has_ticket_by_seat(self, seat): + return seat != '' and seat != '无' and seat != '*' + + def is_trains_number_valid(self, ticket_info): + if self.allow_train_numbers: + return ticket_info[3] in self.allow_train_numbers + return True + + def is_member_number_valid(self, seat): + return seat == '有' or self.member_num <= int(seat) + + + def safe_stay(self): + interval = get_interval_num(self.interval) + QueryLog.add_stay_log(interval) + stay_second(interval) diff --git a/py12306/query/query.py b/py12306/query/query.py index 29e3436..0b41a05 100644 --- a/py12306/query/query.py +++ b/py12306/query/query.py @@ -1,9 +1,6 @@ from requests_html import HTMLSession -import py12306.config as config -from py12306.helpers.api import LEFT_TICKETS from py12306.helpers.func import * -from py12306.helpers.station import Station from py12306.log.query_log import QueryLog from py12306.query.job import Job @@ -16,7 +13,11 @@ class Query: jobs = [] session = {} + # 查询间隔 + interval = {} + def __init__(self): + self.interval = init_interval_by_number(config.QUERY_INTERVAL) self.session = HTMLSession() @classmethod @@ -27,52 +28,13 @@ class Query: def start(self): self.init_jobs() - QueryLog.print_init_jobs(jobs=self.jobs).flush() - for job in self.jobs: - self.handle_single_job(job) + QueryLog.print_init_jobs(jobs=self.jobs) + while True: + for job in self.jobs: + job.run() def init_jobs(self): jobs = config.QUERY_JOBS for job in jobs: - job = Job(info=job) + job = Job(info=job, query=self) self.jobs.append(job) - - def handle_single_job(self, job): - """ - 处理单个任务 - 根据日期循环查询 - - 展示处理时间 - :param job: - :return: - """ - for date in job.left_dates: - result = self.query_by_job_and_date(job, date) - self.handle_single_result(result) - - station = Station.get_station_by_name('广州') - print(station) - pass - - def query_by_job_and_date(self, job, date): - """ - 通过日期进行查询 - :return: - """ - QueryLog.add_log('正在查询 {}, {} - {}'.format(date, job.left_station, job.arrive_station)) - url = LEFT_TICKETS.get('url').format(left_date=date, left_station=job.left_station_code, - arrive_station=job.arrive_station_code, type='leftTicket/queryZ') - - return self.session.get(url) - - def handle_single_result(self, result): - """ - 错误判断 - 余票判断 - 小黑屋判断 - 座位判断 - 乘车人判断 - :param result: - :return: - """ - pass diff --git a/py12306/user/user.py b/py12306/user/user.py new file mode 100644 index 0000000..4b15616 --- /dev/null +++ b/py12306/user/user.py @@ -0,0 +1,22 @@ +from py12306.helpers.func import * + + +@singleton +class User: + + def __init__(self): + """ + 初始化用户 + 恢复 + 登录 + """ + pass + + @classmethod + def run(cls): + self = cls() + self.start() + pass + + def start(self): + pass