Exemple #1
0
class TicketFilter:
    def __init__(self):
        # Type mask to filter ticket types
        self.enabled_types = BitFlags(TicketType.ALL)
        # Price filter. Tickets with prices outside
        # this range will be ignored. (ValueRange)
        self.price_range = ValueRange()
        # Whether to ignore trains that aren't selling tickets yet
        self.filter_not_yet_sold = False
        # Whether to ignore trains that are completely sold out
        self.filter_sold_out = False

    def check(self, ticket):
        ticket_status = ticket.status
        if ticket_status == TicketStatus.NOT_APPLICABLE:
            return False
        if self.filter_sold_out and ticket_status == TicketStatus.SOLD_OUT:
            return False
        if self.filter_not_yet_sold and ticket_status == TicketStatus.NOT_YET_SOLD:
            return False
        if not self.enabled_types[ticket.type]:
            return False
        if not self.price_range.check(lambda: ticket.price):
            return False
        return True

    def filter(self, tickets):
        return [ticket for ticket in tickets if self.check(ticket)]
Exemple #2
0
 def __init__(self, path_selector):
     # This is a callback function that takes a list of
     # trains and returns a selected train path
     self.path_selector = path_selector
     # The type of ticket pricing -- normal ("adult") or student
     self.pricing = TicketPricing.NORMAL
     # Whether to allow "fuzzy searching" for the overall
     # departure and destination stations.
     self.exact_departure_station = False
     self.exact_destination_station = False
     # Whether to require that all train transfers take place
     # in the same station -- that is, you will not have to travel
     # between train stations to transfer. Setting this to true
     # is HIGHLY RECOMMENDED unless you know what you are doing!
     self.exact_substations = True
     # If true, when searching for stations from, say, A->C,
     # a train that can go from A->C and A->B will only show
     # up once with the path A->C.
     self.only_show_longest_path = True
     # The time range (ValueRange<datetime.timedelta>) between successive trains.
     # Set a minimum that is long enough to allow you to get from one
     # train to the other, and a maximum to prevent waiting for too long.
     # Note that THIS VALUE IS REQUIRED, and is NOT a filter!
     self.transfer_time_range = ValueRange()
     # A list of stations to avoid
     self.station_blacklist = FlagSet()
     # (Optional) An instance of the TrainFilter class that
     # filters out unwanted trains. This filter is applied
     # BEFORE the "longest path only" filter, which can be
     # very useful in removing unbuyable trains.
     self.train_filter = None
Exemple #3
0
 def __init__(self):
     # Filter to ignore certain train types
     self.enabled_types = BitFlags(TrainType.ALL)
     # If any trains are added to this set, they will be returned,
     # regardless of whether they meet other criteria. To ONLY allow trains
     # in this list to appear, simply set enabled_types to TrainType.NONE.
     self.whitelist = FlagSet()
     # Add train names (e.g. "T110") to this list to ignore them
     self.blacklist = FlagSet()
     # Departure and arrival time filters. Trains that depart/arrive
     # outside this time range will be ignored. -- ValueRange<datetime.time>
     self.departure_time_range = ValueRange()
     self.arrival_time_range = ValueRange()
     # Train duration filter. Trains that have a travel time outside this
     # range will be ignored. -- ValueRange<datetime.timedelta>
     self.duration_range = ValueRange()
     self.ticket_filter = TicketFilter()
Exemple #4
0
 def __init__(self):
     # Type mask to filter ticket types
     self.enabled_types = BitFlags(TicketType.ALL)
     # Price filter. Tickets with prices outside
     # this range will be ignored. (ValueRange)
     self.price_range = ValueRange()
     # Whether to ignore trains that aren't selling tickets yet
     self.filter_not_yet_sold = False
     # Whether to ignore trains that are completely sold out
     self.filter_sold_out = False
Exemple #5
0
class TrainFilter:
    def __init__(self):
        # Filter to ignore certain train types
        self.enabled_types = BitFlags(TrainType.ALL)
        # If any trains are added to this set, they will be returned,
        # regardless of whether they meet other criteria. To ONLY allow trains
        # in this list to appear, simply set enabled_types to TrainType.NONE.
        self.whitelist = FlagSet()
        # Add train names (e.g. "T110") to this list to ignore them
        self.blacklist = FlagSet()
        # Departure and arrival time filters. Trains that depart/arrive
        # outside this time range will be ignored. -- ValueRange<datetime.time>
        self.departure_time_range = ValueRange()
        self.arrival_time_range = ValueRange()
        # Train duration filter. Trains that have a travel time outside this
        # range will be ignored. -- ValueRange<datetime.timedelta>
        self.duration_range = ValueRange()
        self.ticket_filter = TicketFilter()

    def check(self, train):
        if self.whitelist[train.name]:
            return True
        if self.blacklist[train.name]:
            return False
        if not self.enabled_types[train.type]:
            return False
        if not self.departure_time_range.check(train.departure_time.time()):
            return False
        if not self.arrival_time_range.check(train.arrival_time.time()):
            return False
        if not self.duration_range.check(train.duration):
            return False
        if len(self.ticket_filter.filter(train.tickets)) == 0:
            return False
        return True

    def filter(self, trains):
        return [train for train in trains if self.check(train)]
Exemple #6
0
class PathFinder:
    def __init__(self, path_selector):
        # This is a callback function that takes a list of
        # trains and returns a selected train path
        self.path_selector = path_selector
        # The type of ticket pricing -- normal ("adult") or student
        self.pricing = TicketPricing.NORMAL
        # Whether to allow "fuzzy searching" for the overall
        # departure and destination stations.
        self.exact_departure_station = False
        self.exact_destination_station = False
        # Whether to require that all train transfers take place
        # in the same station -- that is, you will not have to travel
        # between train stations to transfer. Setting this to true
        # is HIGHLY RECOMMENDED unless you know what you are doing!
        self.exact_substations = True
        # If true, when searching for stations from, say, A->C,
        # a train that can go from A->C and A->B will only show
        # up once with the path A->C.
        self.only_show_longest_path = True
        # The time range (ValueRange<datetime.timedelta>) between successive trains.
        # Set a minimum that is long enough to allow you to get from one
        # train to the other, and a maximum to prevent waiting for too long.
        # Note that THIS VALUE IS REQUIRED, and is NOT a filter!
        self.transfer_time_range = ValueRange()
        # A list of stations to avoid
        self.station_blacklist = FlagSet()
        # (Optional) An instance of the TrainFilter class that
        # filters out unwanted trains. This filter is applied
        # BEFORE the "longest path only" filter, which can be
        # very useful in removing unbuyable trains.
        self.train_filter = None

    @staticmethod
    def __get_train_data_query_params(train):
        return [
            ("train_no", train.id),
            ("from_station_telecode", train.departure_station.id),
            ("to_station_telecode", train.destination_station.id),
            # Apparently you're not supposed to use the actual
            # train date here, not the date returned in the
            # train query data. And yes, they can be different.
            ("depart_date", timeconverter.date_to_str(train.data["alt_date"]).strftime("%Y%m%d"))
        ]

    @staticmethod
    def __get_dates_between(date_start, date_end):
        if isinstance(date_start, datetime):
            date_start = date_start.date()
        if isinstance(date_end, datetime):
            date_end = date_end.date()
        for i in range((date_end - date_start).days + 1):
            yield date_start + timedelta(days=i)

    def __get_substations(self, train):
        # Gets stations in (train.departure, train.destination]
        url = "https://kyfw.12306.cn/otn/czxx/queryByTrainNo"
        params = self.__get_train_data_query_params(train)
        json = webrequest.get_json(url, params=params)
        json_station_list = json["data"]["data"]
        logger.debug("Fetched station data for train " + train.name)
        istart = None
        iend = len(json_station_list)
        for i in range(len(json_station_list)):
            is_in_path = json_station_list[i]["isEnabled"]
            if is_in_path and istart is None:
                istart = i
            elif not is_in_path and istart is not None:
                iend = i
                break
        assert istart is not None
        assert json_station_list[istart]["station_name"] == train.departure_station.name
        assert json_station_list[iend-1]["station_name"] == train.destination_station.name
        available_stations = []
        station_list = StationList.instance()
        for i in range(istart+1, iend-1):
            station_name = json_station_list[i]["station_name"]
            if self.station_blacklist[station_name]:
                continue
            station = station_list.get_by_name(station_name)
            available_stations.append(station)
        available_stations.append(train.destination_station)
        return available_stations

    def __get_path_recursive(self, train_list, visited_stations, query, is_first):
        # Note that the "example" train is passed in as the first
        # item in the train list. After our path has been built,
        # we have to remove this item from the list.
        prev_train = train_list[-1]
        last_station = visited_stations[-1]

        if is_first:
            query.exact_departure_station = self.exact_departure_station
            departure_station = prev_train.departure_station
            date_range = [prev_train.departure_time.date()]
        else:
            query.exact_departure_station = self.exact_substations
            departure_station = prev_train.destination_station
            date_range_begin = prev_train.arrival_time + self.transfer_time_range.lower
            date_range_end = prev_train.arrival_time + self.transfer_time_range.upper
            date_range = list(self.__get_dates_between(date_range_begin, date_range_end))

        query.departure_station = departure_station

        # Need to maintain a dict of train destination stations.
        # These destinations are the QUERIED stations, NOT the ones
        # returned by search queries, which can be fuzzy. This is
        # used to determine what stations remain in the trip.
        # Use an ordered dictionary to preserve station order,
        # which might be useful if the client doesn't sort the
        # results themselves.
        next_train_dict = OrderedDict()

        for next_station in visited_stations:
            # Make sure the destination fuzzy search option is set
            # correctly based on which station we're traveling to
            if next_station == last_station:
                query.exact_destination_station = self.exact_destination_station
            else:
                query.exact_destination_station = self.exact_substations

            query.destination_station = next_station

            # Our inter-train transfer period can span multiple days.
            # Generally, this is very rare, but it can happen anyways.
            # For example, if our train arrives at 11:30PM, and our
            # transfer time range is [10, 120] minutes, we want to
            # search for tickets on both the current and the next day.
            for date in date_range:
                query.date = date
                for next_train in query.execute():
                    transfer_time = next_train.departure_time - prev_train.arrival_time
                    # For the first train there's obviously no previous train to check
                    if not is_first and not self.transfer_time_range.check(transfer_time):
                        continue
                    if self.train_filter is not None and self.train_filter.check(next_train):
                        continue
                    if self.only_show_longest_path:
                        for train in next_train_dict:
                            if train.id == next_train.id and train.departure_time == next_train.departure_time:
                                # Found a clash, replace the old train.
                                # Technically, we should compare the station indices,
                                # but since we are looping in order from closest to
                                # furthest station, we can just replace the previous train.
                                # Since we don't iterate over anything after this,
                                # we can safely modify the collection in the loop.
                                next_train_dict.pop(train)
                                break
                    next_train_dict[next_train] = next_station

        # Oh no, there is no way to get from our current station to
        # the target destination station!
        if len(next_train_dict) == 0:
            logger.debug("No trains found in sub-path from {0} to {1}".format(
                departure_station.name, last_station.name))
            # Let the client handle no-result cases
            # return None

        while True:
            # Call the user-defined train selector function with the train list
            selected_train = self.path_selector(list(next_train_dict.keys()))

            # Accept None as a sentinel value to "undo" to the higher level
            # To prematurely exit the search, simply raise StopPathSearch
            # in the path selector function; get_path() will catch this
            # exception and return None to the caller.
            if selected_train is None:
                return None

            train_list.append(selected_train)

            # This is not the actual station that we are at, but the
            # one that was in the original train's station path, which
            # is obviously guaranteed to be in the station list.
            curr_station = next_train_dict[selected_train]

            # "Slice" off the stations that we have already passed
            remaining_stations = visited_stations[visited_stations.index(curr_station)+1:]

            # If there are no stations left, we are at our destination!
            if len(remaining_stations) == 0:
                return MultiTrainPath(train_list[1:])

            # Otherwise, we have to search in the remaining section of the trip
            next_search = self.__get_path_recursive(train_list, remaining_stations, query, False)

            # Note that at this point the query object has probably been
            # modified, so don't rely on it to store information across calls

            if next_search is not None:
                return next_search

            logger.debug("Undoing from {0} to {1}".format(
                query.departure_station.name, departure_station.name))

    def get_path(self, train):
        query = TrainQuery()
        query.pricing = self.pricing
        substations = self.__get_substations(train)
        try:
            return self.__get_path_recursive([train], substations, query, True)
        except StopPathSearch:
            return None