Add query train ticket
This commit is contained in:
@@ -1,13 +1,34 @@
|
||||
import re
|
||||
from typing import List
|
||||
|
||||
from py12306.app.app import Logger
|
||||
from py12306.lib.api import API_QUERY_INIT_PAGE
|
||||
from py12306.lib.api import API_QUERY_INIT_PAGE, API_LEFT_TICKETS
|
||||
from py12306.lib.exceptions import RetryException
|
||||
from py12306.lib.func import retry
|
||||
from py12306.lib.helper import ShareInstance
|
||||
from py12306.lib.helper import DataHelper
|
||||
from py12306.lib.request import Request
|
||||
|
||||
|
||||
class QueryTicketData(DataHelper):
|
||||
left_date: str
|
||||
left_station: str
|
||||
arrive_station: str
|
||||
|
||||
|
||||
class TicketData(DataHelper):
|
||||
left_date: str = 'key:13'
|
||||
ticket_num: str = 'key:11'
|
||||
train_number: str = 'key:3'
|
||||
train_no: str = 'key:2'
|
||||
train_no: str = 'key:2'
|
||||
left_station: str = 'key:6'
|
||||
arrive_station: str = 'key:7'
|
||||
order_text: str = 'key:1'
|
||||
secret_str: str = 'key:0'
|
||||
left_time: str = 'key:8'
|
||||
arrive_time: str = 'key:9'
|
||||
|
||||
|
||||
class Query:
|
||||
@classmethod
|
||||
def task_train_ticket(cls, task: dict):
|
||||
@@ -19,6 +40,8 @@ class QueryTicket:
|
||||
车票查询
|
||||
"""
|
||||
api_type: str = None
|
||||
time_out: int = 5
|
||||
session: Request
|
||||
|
||||
def __init__(self):
|
||||
self.session = Request()
|
||||
@@ -28,6 +51,10 @@ class QueryTicket:
|
||||
|
||||
@retry()
|
||||
def get_query_api_type(self) -> str:
|
||||
"""
|
||||
动态获取查询的接口, 如 leftTicket/query
|
||||
:return:
|
||||
"""
|
||||
if QueryTicket.api_type:
|
||||
return QueryTicket.api_type
|
||||
response = self.session.get(API_QUERY_INIT_PAGE)
|
||||
@@ -38,3 +65,26 @@ class QueryTicket:
|
||||
except IndexError:
|
||||
raise RetryException('获取车票查询地址失败')
|
||||
return self.get_query_api_type()
|
||||
|
||||
@retry()
|
||||
def get_ticket(self, data: dict):
|
||||
data = QueryTicketData(data)
|
||||
url = API_LEFT_TICKETS.format(left_date=data.left_date, left_station=data.left_station,
|
||||
arrive_station=data.arrive_station, type=self.get_query_api_type())
|
||||
resp = self.session.get(url, timeout=self.time_out, allow_redirects=False)
|
||||
result = resp.json().get('data.result')
|
||||
if not result:
|
||||
Logger.error('车票查询失败, %s' % resp.reason)
|
||||
tickets = QueryParser.parse_ticket(result)
|
||||
|
||||
|
||||
class QueryParser:
|
||||
@classmethod
|
||||
def parse_ticket(cls, items: dict) -> List[TicketData]:
|
||||
res = []
|
||||
for item in items:
|
||||
info = item.split('|')
|
||||
info = {i: info[i] for i in range(0, len(info))} # conver to dict
|
||||
res.append(TicketData(info))
|
||||
|
||||
return res
|
||||
|
||||
@@ -7,3 +7,5 @@ BASE_API = 'https://' + HOST_API
|
||||
|
||||
|
||||
API_QUERY_INIT_PAGE = BASE_API + '/otn/leftTicket/init'
|
||||
API_LEFT_TICKETS = BASE_API + '/otn/{type}?leftTicketDTO.train_date={left_date}&leftTicketDTO.from_station={' \
|
||||
'left_station}&leftTicketDTO.to_station={arrive_station}&purpose_codes=ADULT'
|
||||
|
||||
@@ -20,3 +20,41 @@ class Dict(dict):
|
||||
return value
|
||||
except KeyError:
|
||||
return self.dict_to_dict(default)
|
||||
|
||||
def __getitem__(self, k):
|
||||
return self.dict_to_dict(super().__getitem__(k))
|
||||
|
||||
@staticmethod
|
||||
def dict_to_dict(value):
|
||||
return Dict(value) if isinstance(value, dict) else value
|
||||
|
||||
|
||||
class DataHelper:
|
||||
__origin: dict
|
||||
__mappers: dict = {}
|
||||
|
||||
def __init__(self, data: dict):
|
||||
self.__generate_mappers()
|
||||
self.__origin = data
|
||||
for key, val in data.items():
|
||||
if str(key) in self.__mappers:
|
||||
self.__dict__[self.__mappers[str(key)]] = val
|
||||
elif key in self.__annotations__:
|
||||
self.__dict__[key] = val
|
||||
|
||||
def __generate_mappers(self):
|
||||
for key, val in self.__annotations__.items():
|
||||
try:
|
||||
val = self.__getattribute__(key)
|
||||
if isinstance(val, str) and val.startswith('key:'):
|
||||
tags = val.split(';')
|
||||
self.__dict__[key] = None
|
||||
for tag in tags:
|
||||
tag_info = tag.split(':')
|
||||
if tag_info[0] == 'key':
|
||||
self.__mappers[tag_info[1]] = key
|
||||
elif tag_info[0] == 'default':
|
||||
self.__dict__[key] = tag_info[1]
|
||||
|
||||
except (KeyError, AttributeError):
|
||||
pass
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from tests.helper import BaseTest
|
||||
from py12306.app.query import QueryTicket
|
||||
from py12306.app.query import QueryTicket, QueryParser
|
||||
|
||||
|
||||
class TestQueryTicket(BaseTest):
|
||||
@@ -10,3 +12,29 @@ class TestQueryTicket(BaseTest):
|
||||
def test_get_query_api_type(self):
|
||||
res = QueryTicket().get_query_api_type()
|
||||
self.assertEqual('leftTicket/query', res)
|
||||
|
||||
def test_get_ticket(self):
|
||||
data = {
|
||||
'left_date': '2019-05-18',
|
||||
'left_station': 'BJP',
|
||||
'arrive_station': 'LZJ'
|
||||
}
|
||||
res = QueryTicket().get_ticket(data)
|
||||
|
||||
|
||||
class TestQueryParser(TestCase):
|
||||
tickets = [
|
||||
'iV6uPpzX3CcwqhHe4yzrJHp9hFVCouaXtS01wlUB8f%2BuA%2BKD%2FTV5KLu37w1aKHO2zlAlwMDa%2FDYY%0A2xykUxU964zvkfI3qZZ6uGEKWi0tXCT8fhkQTVvnRI43%2FAinVozab2W1Cq%2FMzJtGBv3D1Q3CscAj%0ANA1XmfNzd6Carglhzvyyy63MkbLIRvxrngx9F01W9jhKXnQupQNTOM3Pw4UIxbesBWkmQfYNj%2Fj%2F%0A3mU33kluoI5vbGVsm115Ec%2BS29KPeaM%2B4%2F2h2UZsiCb%2F5hKfew8Hijodr2VuFftbkge1meSTRRvz%0A|预订|240000G42704|G427|BXP|LAJ|BXP|LAJ|06:21|13:44|07:23|Y|nhsXhn1BbGmb4MI%2BEto43zoslKFQlIY8c356nXAHAEk9Zb2G|20190518|3|P3|01|05|0|0|||||||||||有|无|5||O090M0|O9M|0|0|null',
|
||||
'UY8SmgFA1grdsKcN7%2B4133%2FSWTQqk8wVKcdLNsk6EAiuPIaE5aPPzUr9f%2FepLG0hLchNAKjlOl71%0AbMcW3HypGxckM8L3Hz1rg3ds77qPxXXDFxMITHRQfZzSoM8uqSKPdVwT4mEs6ynZ2Niw7M3iAHbq%0A0qjpuj%2FaAc5yiWsvHxAGc3UQPqchrXjcabyp9%2Bnmf7z84Ep74XirfcRmAmZopq%2B9ySctz9lnwule%0A%2FaSdcAWypluKLPobkAdSpxwndKk8bV2U%2Bq%2BbGPaNEzy2i9ixRdaBBLkg3OAqHaCBetr9gHFEiYXu%0A|预订|240000G4290C|G429|BXP|LAJ|BXP|LAJ|10:45|19:45|09:00|Y|4lti%2FihSxlRgd8xN4SFzPvGmpcT90cvJFfy4V5IGfmyCNl6r|20190518|3|P4|01|16|0|0|||||||||||有|有|5||O0M090|OM9|0|0|null',
|
||||
'VUb2s9O%2BqvddST8Tk%2BT8PzHNjzrMsp301eZv9ukz3jw55DHXLMpQ3ZK92ystqCe9atpD7DFlHiHD%0AB9q%2F4EoAaoU3OwacLHAEMtr9fX%2FYXwuCMhHmQHw%2BL8eejS9QgR5ZQM8oV6%2FeaJ5x5KqCIutwZBtz%0AgzuRZ%2FpOHSGdg03WWOXdWHVpJrBUleLGpQZ%2BQJMz0YGrl1Md%2BpNu5ypNdyKg6AyYjmZs4fRz6Slj%0AwCbQlhkclS2mvxpAE5gSJZ3nY8IjFelQTAqt6XTEHPsZ7Rd%2FNHwOM7UtlbQy7NyBCHTLgIAjuB58%0ADEpzVw%3D%3D|预订|240000T1750J|T175|BXP|XNO|BXP|LZJ|13:05|07:44|18:39|Y|g%2F3wSCFH0UvzDmFPO8NuyXGeIMI26cl93Qzex2RLyufZ8M5i2%2FvdylS8zKM%3D|20190518|3|P4|01|13|0|0||||无|||有||有|有|||||10401030|1413|0|0|null',
|
||||
'dPVMZOEQT2rtEi5BNTY2h1nNhp2H%2BA%2BKZaZINqEQ2RUbeKK%2BFeC1y%2Bm8NdO%2BlS4Ag8r6hsWfWHdL%0AX7DrJJMRMuEXnCwqcc%2Bnwd%2BfvdaeozWFuGE08OFZzJbGnnL%2F54VMSdUnapJ4jWVvsYLG2RUqopiX%0AjDavL7dBULGrfNZN4EMTBFqUz%2BzqnmDGvf3RaXr7EHrztAJSNEQc09PqlGHs65B3VaFhN%2Fa0%2BgVQ%0AXCIAP1YysdgqDXMndNNq4nkMX21Jruvi8ToQWsGnYCf%2F7OIzS5HwOu3PElDZ9bMfempLAFk%3D|预订|2400000Z550M|Z55|BXP|LZJ|BXP|LZJ|14:58|07:30|16:32|Y|nNj9EIzgMtJaVlhUo0gt3HKZi820vP3HktntoPUe%2FFW%2BDfiv|20190518|3|P4|01|06|0|0||11||有|||||有||||||604030|643|0|0|null',
|
||||
'V%2F6N%2BhZhuqxSnfkLwZHsgPQBDsGMcJkhZXyuWQLCKlzv7T%2BMvJerzW2u2TBoM8aRbqVkjywXT99K%0AdGcCUHmNOqXqngnHnvg1yj0jvsfQHPRHKIPa6hl0QeX%2BgM%2F%2Ffyj8opU919pW4YT3HViE0hQ8vNRT%0AUQOdJmbSFo1b3xI05cuzh4j21RuP9sdgaA%2BnheYMTvyMoYiEUvN1%2BClGrlrbXnhHgSWFUMxu88sG%0AQGpnqTLtVx27AAe58c9qy5oq35lOnf5OV6%2BUebB7n4YYy7ZpugZ1gyPndGGhvQdg8j58HFo%2FY%2BC1%0AzbdwhA%3D%3D|预订|2400000Z7508|Z75|BXP|LZJ|BXP|LZJ|15:57|10:30|18:33|Y|bIdN7uqCXyxKnuLinwN1naNDcYioI7Xuk535Xl1xm6Wn8CRtk2knPYx5MW4%3D|20190518|3|P4|01|07|0|0||||有|||有||有|有|||||10401030|1413|0|0|null',
|
||||
'2WQo8Fm2OT6Y016qIB5vRQNikHMVarIhB9YUu7sDFKMTC3RFxmi7Y%2BE9S%2BjdYxUoEfUiqhj%2F8ZX1%0A1GpE8Vikd5urQLbp5%2FjkES9798ohE3dQwZ0ffKHX%2FQiIl4maKmdVKebWTyV8IMgTThm5C1l%2B8csY%0ApM0kaFEsQtERyf8Mh9FH9vQDxn2Vtb%2FoOPY2UvNS%2F8Tf%2BNWni21Dh8tRZ0ZL9CBYl6%2BRbNphYSZy%0AhQASZ9fG%2BjJe96bZL%2FsuMvFa%2FTNG51k07G8mggtoqgREp0zP0cdBHjkOm%2BTmMuK7uqLS9gUodYds%0ApEj%2B%2Bg%3D%3D|预订|240000Z1510B|Z151|BXP|XNO|BXP|LZJ|16:03|11:19|19:16|Y|TM4VyMprWgU1m%2ByEJxolE3Hutch%2FGYoyOMLhudWSaubKi5OeWcwS8XZJvkU%3D|20190518|3|P2|01|09|0|0||||无|||有||有|有|||||10401030|1413|0|0|null',
|
||||
'i4IZi6FeuPVecIlQ7MMdptQ77XQ6DEH14WRtbCN1%2FJViWj7liJ3qUEJ9ml3aC9%2B8cBPbKsVHycxa%0ApoLgwMxEcxJ8LdFDeWHSJ%2FbRPyw0Ygs3tYGz%2FTYv4Ys03Oc6NGJsXlt76XQ6Lmm6fDVs%2FKnsWARg%0Ar2NxqMn0ecRGgiDAcVRF4CApE3cdE2GW0%2Blt7xcbPTDc3R2vawIAk8zKlMWKaMReXfqgeeln%2BAIV%0Aa9KSTBxgR9pC85I%2BVMJe4mYVLeUaSa%2FI7fYrXfJyVu%2BDDdiQaWEwLsTlmh7cxkGZlosHLtJh14Ym%0A6xjrqg%3D%3D|预订|2400000Z210D|Z21|BXP|LSO|BXP|LZJ|20:00|12:17|16:17|Y|et1f50q%2B5c5I%2B9WjMAG7QRRd%2F5lr5LzzS%2Bijw0HrPjMGTPoFY0BytCT68Ho%3D|20190518|3|P2|01|05|0|0||||无|||有||无|有|||||10401030|1413|0|0|null',
|
||||
'XyBvey3WmmF82TTYlMRIMGTG9tntgOjqf7d9Y7YgdZTP16T3Ts1loq5oqe9XUOrKNJxRGUmv4Q9h%0AkbnGYxvHA4LgWlDsyqO%2B%2B6SoX%2BW%2BtCH%2BC5JXvabJcaN%2BfZjQa8aBYvHHNx4li28D4tlCfrKnkB%2BU%0AzHfTSG6ekFF5K53clwbEyaljpJDdCi6uSQqMPUkslA1RQ4KAQPnXEbDbz4oC9IdjGiiTTPuC7QJU%0A0r2VW5TnKXvJr6toDWogGW8icGjeuDVcNKn%2B5OltBdNJD5bheKSE4hjzv8HauF8H%2Bb3c77jzHqSk%0ANtV%2Bgw%3D%3D|预订|250000K8880P|K885|TJP|LAJ|BJP|LZJ|23:43|05:07|29:24|Y|A1VIUe1w8dwrEhQacQ1O8SQntd7wRO0M%2Bck0TjWwuZgB%2Fi%2BVT2cxShZVqzQ%3D|20190518|3|P4|03|15|0|0||||无|||无||无|有|||||10403010|1431|1|0|null'
|
||||
]
|
||||
|
||||
def test_parse_ticket(self):
|
||||
res = QueryParser.parse_ticket(self.tickets)
|
||||
self.assertEqual(res[0].left_station, 'BXP')
|
||||
self.assertEqual(res[0].train_number, 'G427')
|
||||
|
||||
Reference in New Issue
Block a user