こんにちは おそらく、私たちはかつてあなたが緊急にどこかに立ち去る必要があるという状況に直面したかもしれませんが、すべての列車のチケットはすでに売り切れています。 この記事では、リリースされたUkrzaliznytsiaチケットを追跡して購入するためのTelegramボットの作成方法について説明します。
仕組み
ウクライナで鉄道チケットを購入するために、Ukrzaliznytsiaはリソースhttp://booking.uz.gov.ua/を立ち上げました。 リソースは、チケット自体を受け取るためにチケットオフィスにアクセスする必要がないという点で便利です。 スマートフォンの画面で搭乗券のQRコードを指揮者に表示するか、プリンターに印刷するだけで十分です。
問題は、人気のあるフライトでは場所が非常に早く終了し、時にはチケットを購入するのが非常に問題になることです。 ただし、多くの人はチケットを購入せずに予約します。 予約は24時間のみ有効で、その後、チケット売り場で購入しない場合、チケットは無料プールに返されます。 したがって、チケットが予約または再購入される前に購入できるようになった瞬間をキャッチする必要があります。
1分に1回、目的の列車の無料チケットをチェックし、可能な場合は15分間予約するスクリプトを使用して、この問題を解決することにしました。 その後、ユーザーはWebブラウザーを介して支払い手順を完了する必要があります。
Telegramは私にとって新しいプラットフォームであり、少し対処したかったため、インターフェイスとして選択されました。 おまけとして、プッシュ通知やメールを考えずに、モバイルですぐに通知を受け取ります。
Pythonがプログラミング言語として選択されました。
インターフェース
それでも、これはユーザーの観点からどのように機能しますか?
ボットは次のコマンドを認識します。
-
/help
サポートされているコマンドのリストを返します -
/trains 2016-06-12 Kyiv Lviv
-2016年6月12日に出発するキエフからリヴィウまでの列車のリストを返します -
/scan Ivanov Ivan 2016-06-12 Kyiv Lviv 743K
キエフリヴィウ743K列車のチケットの監視を開始します。 このスキャンのIDを返します。 -
/status_1234
-ID 1234のスキャンステータスを返します -
/abort_1234
-ID 1234のスキャンを停止します
チケットが正常に予約されると、ユーザーはセッションIDを含むメッセージを受け取ります。 次に、このIDをブラウザCookieに手動で入力し、チケットの購入を完了する必要があります。
UZ API
まず、ポータルで使用されるAPI形式を見てみましょう。 これは大したことではありません。ブラウザで開発者のコンソールを開き、チケット検索ページでスクリプトが実行するリクエストを確認してください。
APIはPOSTリクエストのみを使用します。 サードパーティの開発者によるAPIの使用を防ぐために、ほとんどすべての呼び出しで、トークンが本文に含まれています。 トークンなしでは、ステーションのみを検索できます。
また、日付を扱う際の微妙な違いにも注目する価値があります。 まず、日付形式はAPIの現在のロケールに応じて変わります。 たとえば、ロケールen
形式はmm.dd.yyyy
ます。 一方、 ua
とru
については、 dd.mm.yyyy
おなじみです。 第二に、一部のリクエストでは、日付はタイムスタンプの形式で表示されますが、夏/冬の状態に依存します。 したがって、これらのスタンプのシリアル化/非シリアル化に煩わされることなく、APIが返す形式で使用することにしました。
受信トークン
Webサイトで接続されたスクリプトを調べた結果、そのような部分を簡単に見つけることができます。
var ajax = $v.ajax(url).header({ 'GV-Ajax': 1, 'GV-Referer': encodeURI(GV.site.htcur_url + GV.site.requestUri), 'GV-Screen': screen.width + 'x' + screen.height, 'GV-Token': localStorage.getItem('gv-token') || '' });
ここで、APIを呼び出すときに、localStorageブラウザーからトークンが読み取られることがわかります。 彼がそこに書いている場所を見つけることは残っています。
htmlとjsでの単純な検索が見つからなかったため、この部分が最も興味深いものでした。 Googleで数時間過ごした後、著者がUZ Webサイトでチケットを監視するという同じ問題を解決する記事に出会いました。 そのため、この記事では、 JJEncodeを使用して難読化されたコードによってトークンが生成されることを詳しく説明しました 。 数分後に、pythonでの難読化解除機能の実装が見つかります。これは将来使用されます。
簡単なAPIリファレンス
APIメソッドを呼び出すには、次のヘッダーを含める必要があります。
GV-Ajax: 1 GV-Referer: http://booking.uz.gov.ua/en/ GV-Token: <token>
駅検索
たとえば、ステーションの自動補完のヒントを生成するには、 http://booking.uz.gov.ua/en/purchase/station/ky/
で空のボディを使用してリクエストを行います。ここで、 ky
はステーション選択テキストボックスに入力したものです。
応答として、サーバーは次のようなJSONを送信します。
{ "value": [ { "title": "Kyiv", "station_id": "2200001" }, { "title": "Kyivska Rusanivka", "station_id": "2201180" }, { "title": "Kyj", "station_id": "2031278" }, { "title": "Kykshor", "station_id": "2011189" } ], "error": null, "data": { "req_text": [ "ky", "" ] }, "captcha": null }
電車を検索
列車を検索するには、 http://booking.uz.gov.ua/en/purchase/search/
のリクエストを次の本文で実行する必要があります。
station_id_from=2200001 # ID station_id_till=2218000 # ID date_dep=06.12.2016 # mm.dd.yyyy time_dep=00:00 time_dep_till= another_ec=0 search=
応答として、指定されたルートに沿った列車のリストを受け取ります。 また、回答には、各タイプの車の空席数に関する情報(ルクス、クーペ、指定席など)が含まれます。
{ "value": [ { "num": "743", "model": 1, "category": 1, "travel_time": "5:01", "from": { "station_id": 2200001, "station": "Darnytsya", "date": 1465741200, "src_date": "2016-06-12 17:20:00" }, "till": { "station_id": 2218000, "station": "Lviv", "date": 1465759260, "src_date": "2016-06-12 22:21:00" }, "types": [ { "title": "Seating first class", "letter": "1", "places": 117 }, { "title": "Seating second class", "letter": "2", "places": 176 } ], "reserve_error": "reserve_24h" }, { "num": "091", "model": 0, "category": 0, "travel_time": "7:25", "from": { "station_id": 2200001, "station": "Kyiv-Pasazhyrsky", "date": 1465760460, "src_date": "2016-06-12 22:41:00" }, "till": { "station_id": 2218000, "station": "Lviv", "date": 1465787160, "src_date": "2016-06-13 06:06:00" }, "types": [ { "title": "Suite / first-class sleeper", "letter": "", "places": 11 }, { "title": "Coupe / coach with compartments", "letter": "", "places": 50 } ], "reserve_error": "reserve_24h" } ], "error": null, "data": null, "captcha": null }
ワゴンを見る
次の本文を使用してhttp://booking.uz.gov.ua/en/purchase/coaches/
でリクエストを完了すると、車のリストと利用可能な座席の数を表示できます。
station_id_from=2200001 station_id_till=2218000 date_dep=1462976400 train=743 # model=3 # coach_type=2 # (, , . .) round_trip=0 another_ec=0
これに応じて、このタイプの車のリストと無料席の数と価格を取得します。
{ "coach_type_id": 10, "coaches": [ { "num": 1, "type": "", "allow_bonus": false, "places_cnt": 21, "has_bedding": false, "reserve_price": 1700, "services": [], "prices": { "": 35831 }, "coach_type_id": 10, "coach_class": "2" }, { "num": 3, "type": "", "allow_bonus": false, "places_cnt": 21, "has_bedding": false, "reserve_price": 1700, "services": [], "prices": { "": 35831 }, "coach_type_id": 9, "coach_class": "2" } ], "places_allowed": 8, "places_max": 8 }
空いている席を見る
選択した車のhttp://booking.uz.gov.ua/en/purchase/coach/
ている席を表示するには、 http://booking.uz.gov.ua/en/purchase/coach/
のリクエストを本文で実行する必要があります。
station_id_from=2200001 station_id_till=2218000 train=743 coach_num=1 coach_class=2 coach_type_id=19 date_dep=1462976400 change_scheme=1
応答として、空き場所のリストを取得します。
{ "value": { "places": { "": [ "8", "12", "16", "18", "22", "27", "28", "32", "33", "34", "36", "37", "38", "39", "42", "43", "47", "48", "49", "55", "56" ] } }, "error": null, "data": null, "captcha": null }
バスケットで作業する
チケットをバスケットに入れ、それによって支払いのために15分間予約した場合、 http://booking.uz.gov.ua/en/cart/add/
のリクエストを本文で処理する必要があります。
code_station_from:2200007 code_station_to:2218000 train:743 date:1463580000 round_trip:0 places[0][ord]:0 places[0][coach_num]:5 places[0][coach_class]:2 places[0][coach_type_id]:22 places[0][place_num]:37 places[0][firstname]:Name places[0][lastname]:Surname places[0][bedding]:0 places[0][child]: places[0][stud]: places[0][transp]:0 places[0][reserve]:0
モニタリング
それで、ここで最も興味深い部分、つまり無料チケットを監視することになりました。 この問題を解決するために、いくつかのメソッドを持つUZScanner
クラスが実装されました。
- 監視する列車を追加する
- 監視から列車を削除
- 監視開始
- 監視停止
監視クラスは、他の非テレグラム、ボット、またはWebサイトなど、ユーザーインターフェイスを簡単に接続できるように実装されます。
監視は非同期プロセスであり、コルーチンとして実行されます。 チケットの予約が成功した場合、モニタリングはコールバックを実行し、結果をユーザーに通知します。 これを行うには、コールバック関数がクラスコンストラクターに渡されます。
class UZScanner(object): def __init__(self, success_cb, delay=60): self.success_cb = success_cb self.loop = asyncio.get_event_loop() self.delay = delay self.session = aiohttp.ClientSession() self.client = UZClient(self.session) self.__state = dict() self.__running = False
呼び出しコードがどの特定のユーザーに対してコールバックが発生したかを区別するために、トレイン自体に関するデータに加えて、コールバックIDも送信されます。
def add_item(self, success_cb_id, firstname, lastname, date, source, destination, train_num, ct_letter=None): scan_id = uuid4().hex self.__state[scan_id] = dict( success_cb_id=success_cb_id, firstname=firstname, lastname=lastname, date=date, source=source, destination=destination, train_num=train_num, ct_letter=ct_letter, lock=asyncio.Lock(), attempts=0, error=None) return scan_id
主な監視機能は、列車ごとに座席を確認する機能を起動するサイクルです。
async def run(self): self.__running = True while self.__running: for scan_id, data in self.__state.items(): asyncio.ensure_future(self.scan(scan_id, data)) await reliable_async_sleep(self.delay)
監視機能自体は、このアルゴリズムに従って機能します。
- 特定のルートの特定の日付の列車のリストを取得する
- 電車があるか確認する
- すべての車(または指定されたタイプのみ)については、空室状況を確認してください
- 最初の空席を予約してください
- 成功した場合、コールバック、監視から列車を削除
async def scan(self, scan_id, data): if data['lock'].locked(): return async with data['lock']: data['attempts'] += 1 train = await self.client.fetch_train( data['date'], data['source'], data['destination'], data['train_num']) if train is None: return self.handle_error( scan_id, data, 'Train {} not found'.format(data['train_num'])) if data['ct_letter']: coach_type = self.find_coach_type(train, data['ct_letter']) if coach_type is None: return self.handle_error( scan_id, data, 'Coach type {} not found'.format(data['ct_letter'])) coach_types = [coach_type] else: coach_types = train.coach_types session_id = await self.book(train, coach_types, data['firstname'], data['lastname']) if session_id is None: return self.handle_error(scan_id, data, 'No available seats') await self.success_cb(data['success_cb_id'], session_id) self.abort(scan_id) @staticmethod async def book(train, coach_types, firstname, lastname): with UZClient() as client: for coach_type in coach_types: for coach in await client.list_coaches(train, coach_type): try: seats = await client.list_seats(train, coach) except ResponseError: continue for seat in seats: try: await client.book_seat(train, coach, seat, firstname, lastname) except ResponseError: continue return client.get_session_id()
おわりに
http://booking.uz.gov.uaポータルで使用されるAPIを把握し、チケット予約スクリプトを実装しました。 コードはGitHubで入手できます 。 DockerイメージはDockerHubで入手できます。 Telegramボット@uz_ticket_botも利用できます。
UPD UZがIPボットをブロックしました。 ボットは一時的に無効になっています。