Покупаем билеты на поезд в Новый год

    Конечно, этот новый год, все из вас хотели бы провести дома. Не будем спорить о том, что такое дом, у каждого свое представление об этом, но лично у меня дом ассоциируется с семьей, родителями. Наверное самый доступный способ оказаться дома в новый год на территории России (помимо метро или нескольких часов пробок) — это конечно же поезд от всеми любимой компании РЖД.



    Но спрос явно превышает предложение. Особенно на плацкарт, который, прямо скажем, самый выгодный. Так что же делать? Если интересно, то можно пройти под кат. Но, конечно, все может быть не так драматично, а просто вам надо куда-то уехать, в любое время года, а цены на люкс от РЖД вас не устраивают. Все мы знаем про бронь которая снимается и билеты которые могут возвращать, их то мы и будем ловить :)



    Начну с того, что мы будем просто посылать запросы, по крону, например раз в пять минут. Затем читать ответ, и если условия нас устроят, то отправим себе смс, что можно брать. Но если все так просто, то зачем статья на хабр, спросите вы? А затем, что это просто готовое решение, которым я могу поделится, с тем кто не может или не хочет тратить свое время + на сайте РЖД есть довольно любопытный механизм по защите от таких левых запросов, который я обошел, и сейчас расскажу в чем он заключается и как его обойти.

    Запрос на получение мест на конкретную дату:

    curl 'http://pass.rzd.ru/timetable/public/ru?STRUCTURE_ID=735&layer_id=5371&dir=0&tfl=3&checkSeats=1&st0={from}&code0=2004000&dt0={date}&st1={to}&code1=2060600&dt1={date}&rid=729493435&SESSION_ID=2' -H 'Cookie: JSESSIONID=00006mwFi5RKtF-z0R16OGSMJtS:17obqce3m;'

    Жирным я выделил параметры, которые нас интересуют:

    • from — город отправления, выбираем сами
    • to — город куда мы хотим попасть, выбираем сами
    • date — дата отправления, выбираем сами
    • JSESSIONID — ид сессии, с ним нам ничего делать не нужно, просто будем использовать куки в curl
    • rid — загадочный ид номер 1
    • SESSION_ID — загадочный ид номер 2


    После небольшого изучения становится понятно, что при каждом новом запросе rid изменяется внешне хаотично, а SESSION_ID просто увеличивается на один. Чтоже делать, как их узнать? Если присмотримся немного к логам, то увидим еще один запрос, который всегда идет перед этим.

    Вот он:

    curl 'http://pass.rzd.ru/timetable/public/ru?STRUCTURE_ID=735&layer_id=5371&dir=0&tfl=3&checkSeats=1&st0={from}&code0=2004000&dt0={date}&st1={to}&code1=2060600&dt1={date}' -H 'Cookie: JSESSIONID=00006mwFi5RKtF-z0R16OGSMJtS:17obqce3m;'

    И вот, что он нам вернет:

    {«result»:«RID»,"SESSION_ID":2,"rid":729493435,«discounts»:{}}

    Нас в этом интересует понятно что :)

    Все ясно скажете вы, делаем подряд два запроса, из первого получаем недостающие параметры, подставляем во второй и бинго! Но нет. Это еще не все. Я если честно, дальше впал в ступор, почему же оно отказывается работать? Сравнивал другие заголовки, искал скрытые переменные, искал немного магии. Но нет. Из консоли работает, с сайта работает, из скрипта — ошибка.

    Даже проникся уважением к защите, которую недооценил с первого взгляда. И конечно потом меня осенило, задержка! Между запросами должна быть пауза. sleep(2); Вот и все решение. Сложно сказать с чем это связано, действительно ли задержка там для защиты, или просто данные попадают куда нужно не так быстро, но так или иначе ее необходимость была для меня не совсем очевидна.

    Вот и все. Данные есть, как бесплатно отослать смс? Можно конечно почтой, но если важна оперативность, то смс будет предпочтительнее. Тут все на ваш вкус, а я просто использовал sms.ru, там рассылка на один телефонный номер бесплатна, а больше мне и не надо. Для каждого вашего конкретного города нужны буду уникальные числовые коды, которые как видим мы передаем на вход нашего метода:

    $rzd->request([
        'Санкт-Петербург',
        '2004000',
        'Киров',
        '2060600',
        '28.12.2013',
    ]);


    Чтобы их узнать, спасибо mafet, можно например сделать запрос к http://pass.rzd.ru/suggester?lang=ru&stationNamePart=Са, подставив две первые буквы города и найти там нужный город и вокзал, и оттуда достать нужный нам ид.

    Если кому он нужен, то сам скрипт:

    Скрипт
    <?php
     
    class rzd {
     
        private $urlData = 'http://pass.rzd.ru/timetable/public/ru?STRUCTURE_ID=735&layer_id=5371&dir=0&tfl=3&checkSeats=1&st0={{from}}&code0={{code_from}}&dt0={{date}}&st1={{to}}&code1={{code_to}}&dt1={{date}}';
        private $data;
        private $replace = [
            '{{from}}',
            '{{code_from}}',
            '{{to}}',
            '{{code_to}}',
            '{{date}}',
        ];
        private $secure = '&rid={{rid}}&SESSION_ID={{session_id}}';
        private $replaceSecure = [
            '{{rid}}',
            '{{session_id}}',
        ];
        private $cookie = 'cookie';
     
        public function request($data) {
            $this->data = $data;
            $this->urlData = str_replace($this->replace, $this->data, $this->urlData);
            $ch = curl_init($this->urlData);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($ch, CURLOPT_COOKIEJAR, $this->cookie);
            curl_setopt($ch, CURLOPT_COOKIEFILE, $this->cookie);
            $result = json_decode(curl_exec($ch), true);
            $this->urlData .= str_replace($this->replaceSecure, [$result['rid'], $result['SESSION_ID']], $this->secure);
            sleep(2);
            $ch = curl_init($this->urlData);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($ch, CURLOPT_COOKIEJAR, $this->cookie);
            curl_setopt($ch, CURLOPT_COOKIEFILE, $this->cookie);
            $result = json_decode(curl_exec($ch), true);
            $result = reset($result['tp']);
            $result = $result['list'];
            foreach ($result as $train) {
                if (isset($train['cars']) && is_array($train['cars']))
                    foreach ($train['cars'] as $ticket) {
                        # здесь можно написать условие, например если цена меньше 4000р то делаем все что ниже и высылаем смс
                        $resultExec = 'На '.$data[4].' - '.$train['number']." - ".$ticket['type'].' за '.$ticket['tariff'].' - '.$ticket['freeSeats'].' м';
                        $ch = curl_init("sms.ru/sms/send");
                        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
                        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
                        curl_setopt($ch, CURLOPT_POSTFIELDS, array(
                            "api_id" => 'id sms.ru',
                            "to"      => 'ваш телефон',
                            "text"   => $resultExec,
                        ));
                        sleep(2);
                        $body = curl_exec($ch);
                        curl_close($ch);
                    }
            }
        }
    }
     
     
     
    $rzd = new rzd();
    $rzd->request([
        'Санкт-Петербург',
        '2004000',
        'Киров',
        '2060600',
        '27.12.2013',
    ]);
     
     
    $rzd = new rzd();
    $rzd->request([
        'Санкт-Петербург',
        '2004000',
        'Киров',
        '2060600',
        '28.12.2013',
    ]);



    Не забываем поставить все это в кронтаб и ждать улова. Удачных поездок.

    P.S. php 5.5, но что изменить для 5.4 и меньше, думаю всем понятно, и дада, тут нет никакого ООП, нет паттернов и нет продуманного дизайна кода, это просто скрипт, который пока что работает (пока ржд не изменят алгоритм)
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 66
    • +6
      Если желающих появится много, введут капчу (
      • +32
        Я бы на их месте просто бы открыл api. Я люблю api. И не люблю капчи.
        • +2
          А api можно сказать есть, просто документации нет открытой. У них сейчас GET запрос и json ответ который очень удобно парсить.
          • +2
            Да, конечно можно и так сказать. Просто, факт того что официально оно не открыто, не дает понять как они отнесутся к этим автоматическим нагрузкам на свои сервера.
        • +1
          … поставят проводника с флажком.
          • 0
            Так и не понял, что конкретно упрощает скрипт из топика.
          • +21
            Могу ответить, почему ответ приходит на запрос не сразу. Я сам написал скрипт обработки данных с сайта РЖД полгода назад. Точней написал я его больше года назад, но пол года назад один существенно изменили движок. Раньше приходилось парсить html, а теперь json обмен, что очень удобно.
            Так вот.

            В первый запрос вы никогда не получите полный ответ по билетам, т.к. база у них работает не быстро и вообще вся логика построена на очереди на запрос со стороны клиента. В первом запросе вы получаете номер сессии [rid] (можно сравнить с талончиком на получение информации) и cookie, которое нужно формально. В следующий раз вы должны прийти с этим номером сессии и cookie и спрашивать — получена ли информация. Спрашивать до упора (нужно заложить свой таймаут, хотя можно покопаться в скриптах сайта и понять, какой у них зашит, но это не принципиально. у меня лично ответ должен прийти за 25 запросов. Каждый запрос не более 5 секунд) и не важно с каким интервалом. У меня лично опрос после первого запроса — раз в секунду и никаких проблем, там никакой защиты в этом плане нет.

            Кстати рекомендую делать обработку типа билетов по полю «type», хоть оно и на русском языке, что не очень удобно парсить. Раньше парсил по itype (или ittype), но номера itype могут отличаться в зависимости от направления. Не знаю, зачем оно нужно.

            И кстати насчёт session_id — я лично забил на увеличение на 1 каждый запрос. Тупо шлю то, что мне в первый раз ответили.

            ps. Очень рекомендую веб-дебаггер Charles — любая стандартная веб-активность там просто на лицо. Из его дебага сразу становится понятно, как работать с сайтом РЖД
            • +2
              Я бы порекомендовал BurpSuite вместо Charles. Помощнее утилита.
              • 0
                Уточняйте цену пожалуйста, когда советуете. $299 в год не стоит рядом с $50 чарлеса. Хотя банальный фаербаг удобнее для анализа
                • 0
                  У них есть Free версия, которая умеет куда больше этого Charles
            • +2
              И кстати, я что-то не понял, почему у вас code0 и code1 фиксированные. Я конечно сейчас не очень уверен, но по моему они важны для сайта ржд. Я лично забираю полный список станций с ссылки pass.rzd.ru/suggester?lang=ru&stationNamePart= (например pass.rzd.ru/suggester?lang=ru&stationNamePart=МО ) по первым двум буквам [А-Я][А-Я]. Там так же указаны коды станций.
              • +4
                Такой у меня интерфейсик

                К сожалению не готов в паблик выкладвать, т.к. очкую что забанят мой ип от количества запросов. Да и sql инъекций там можно много сделать.
                • +4
                  А может быть вы код выложите, а желающие у себя разместят?
                • +2
                  Да, точно. Немного отредактировал исходный код своего скрипта, спасибо за замечание и столь развернутый комментарий :)
                • +4
                  Сайт РЖД это просто ад какой-то… Неужели из миллиардов не нашлось пару тысяч на разработку нормального сайта, а не этого ужаса с зойчем в логотипе?..
                  • +1
                    Сайты железных дорог они часто такие.
                    • +15
                      Ну знаете ли. Если всё делать как надо, то на очередное шубохранилище может и не хватить.
                      • +1
                        Шутки шутками, но если уж сравнивать…
                        Сайт Trainitalia до сих пор шлет мне спам. Регистрация там была сущим адом, потому что в пароле — скрытая авто-капитализация и нет фильтрования пробелов (ну и угадайте, как это обнаружить). Но даже после прохождения этих квестов я все равно не смог купить билет — сайт просто отдавал 500.
                      • +3
                        Кстати, на английском языке красивее.
                      • +1
                        я этим летом написал аналогичный скрипт чтобы взять билеты в крым. только он еще отправлял мне смс когда нужный билет появляется. ) могу скинуть кому надо, но код непричесанный (php, bash).
                        • +2
                          А как смс слал? В смысле, использовал какой-то сторонний сервис или своё что-то?
                          • +4
                            sms.ru — отправка смс на свой номер бесплатно
                            • 0
                              какойто выбрал зарубежный сервис с простым апи и там штук 15 первых смс на аккаунте бесплатно — для билетов мне хватило, на первый раз не успел до вокзала доехать, второй раз один взял и через день еще один билет.
                              • 0
                                clickatell.com
                            • +1
                              А разве сайты типа tutu.ru не так делают? Или они платят, а им дают доступ к api?
                              • 0
                                Доступ в базу у таких агенств есть
                              • +3
                                Программисты компаний по продаже билетов уже реализуют ваш хак. Через пару дней свободных билетов не будет, а через неделю РЖД закроет дыру. Получение преимуществ за чужой счет/в обход очереди — неэффективный путь. Хотя и веселый.
                                • +12
                                  Программисты компаний по продаже билетов уже реализуют ваш хак.
                                  А то они вместо этого в очередях прозябали, отлавливая билетики.
                                  • 0
                                    с августа этим пользовался. ниче не закрыли. если есть ajax-интерфейс закрыть так уж прям совсем не так просто.
                                  • +6
                                    В студенчестве наездился в старых плацкартах. Всё-таки 48 часов из Петербурга в Пятигорск — очень утомительно. Позже перешёл на купейные вагоны. А сейчас, купе почти сравнялся по цене с самолётом, так что проблема выбора почти отпала. Летаю:) Тем более, очень жалко двух суток в дороге.
                                    • +2
                                      Да, собственно, скоро и плацкарт станет стоить дороже самолета. Нужно больше шуб золота!
                                    • +1
                                      public function request($data) { ... }
                                      

                                      $rzd->request([...]);
                                      

                                      Ну почему не так?
                                      public function request($from, $codeFrom, $to, $codeTo, $date) { ... }
                                      
                                      • 0
                                        А если еще аргументы надо будет ввести? Масштабируемо же.
                                        • 0
                                          Преждевременная оптимизация однако
                                          • 0
                                            Да тут копеечная оптимизация, всего 2 скобки лишние, почему бы сразу не ее сделать.
                                            • +2
                                              Потому что без документации непонятно сколько элементов должно быть в переданном массиве и их тип. Это даже не оптимизация, так делать нельзя. Плюс непонятно как делать анализ такого кода.
                                              Если методу надо 5 строк и одно число, он должен принимать 5 строк и одно число, а не массив. Если в будущем понадобится еще аргумент, добавьте его и сделайте необязательным, чтобы ничего не сломать.
                                              • 0
                                                Вы правы в том, что «непонятно сколько элементов должно быть в переданном массиве и их тип». Для этого в самой функции обязательно надо массив переданных параметров мержить с массивом параметров по умолчанию. Такая методика вполне себе отлично работает. Широко применяется к примеру в jQuery-плагинах.

                                                Если в будущем понадобится еще аргумент, добавьте его и сделайте необязательным, чтобы ничего не сломать.

                                                Только потом будет у нас какой-то уродец, а не функция some_function($a, $b, $c, $d, $e = null, $f = 0, $g = 'all', $h = true). Извините, но это говнокод.
                                                • +4
                                                  Почему это говнокод? Если some_function может работать с четырьмя параметрами и с семью, то три последних должны быть необязательными, это нормальное поведение функции.
                                                  • –4
                                                    Говнокод, потому что без той же самой документации не поймешь в каком порядке надо ставить параметры. А если дальше у вас не нужны будут первые 4 параметра $a, $b, $c, $d, зато надо будет определить пятый $e? Тогда функция плавно превращается в some_function($a = 0, $b = 1, $c = false, $d = null, $e = null, $f = 0, $g = 'all', $h = true) и будете писать some_function(0, 1, false, null, $foobar). Вообще, больше 4 параметров, на мой взгляд, это уже многовато — повод задуматься над декомпозицией.
                                                    • +3
                                                      1. Для того, чтобы знать какие параметры в каком порядке ставить достаточно именовать параметры не $a и $b, а $dateFrom и $dateTo, а также писать PHPDoc.
                                                      2. Если первые 4 параметра будут не нужны, ваш метод (с массивом) всё равно сломается и придется его переписывать. С нормальными аргументами это сделать будет проще и не надо будет изобретать ту чушь, которую вы привели как пример.
                                                      3. Если вам понадобится добавить еще аргументы, то либо они необязательные (значит все вызовы метода работают и без них и вам не надо будет их править), либо без них ничего не работает и вам в любом случае придется переписывать код.
                                                      • –5
                                                        1. Это не отменяет необходимости запоминания порядка аргументов. А если человек в блокноте решил пару фиксов сделать?
                                                        2. Мой метод с массивом не поломается, так как достаточно будет внести в массив аргументов (который мы мержим с переданным в функции) значения по умолчанию для этих 4 параметров. И я не понял как вы собираетесь бороться с some_function(0, 1, false, null, $foobar) без переписывания всего кода, где встречается вызов этого метода.
                                                        3. Собственно пункт №2 показывает, что в моем случае все отлично работает и ничего переписывать не надо

                                                        Посмотрите на любой jQuery-плагин, как он работает. Добавление новых параметров происходит совершенно без болезненно, и ничего не ломается.
                                                        • +4
                                                          1. Продолжайте писать в блокноте.
                                                          2. То есть вместо some_function(0, 1, false, null, $foobar) вы будете передавать some_function([0, 1, false, null, $foobar])? Даа, это очень круто )

                                                          И зачем вы сравниваете PHP и jQuery?
                                                          В общем, я не собираюсь вас переубеждать. Продолжайте говнокодить.
                                                          • –2
                                                            1. Это не имеет значения
                                                            2. Я буду передавать some_function(['foobar' => $foobar]). Что здесь непонятного?

                                                            Говнокодите из нас только вы. Ни в одной приличной библиотеке не видел функций вида some_function($a, $b, $c, $d, $e = null, $f = 0, $g = 'all', $h = true)
                                                            • +1
                                                              Видимо разработчики php где-то ошиблись. Зачем вообще методу несколько аргументов, если можно принимать на входе один массив. Видимо это фатальный недостаток языка.
                                                              • 0
                                                                Безусловно это не везде надо, но отсутствие именованных аргументов — да — 1 из недостатков PHP.
                                                                  • –1
                                                                    Давайте не будем разводить срач теперь и про PHP vs Ruby/Python/C#/whatever, просто я хотел донести мысль, что

                                                                    some_function([
                                                                        'param1' => $value1,
                                                                        'param2' => $value2,
                                                                        'param3' => $value3,
                                                                        'param4' => $value4,
                                                                        'param5' => $value5,
                                                                        'param6' => $value6,
                                                                        'param7' => $value7,
                                                                        'param8' => $value8
                                                                    ])
                                                                    


                                                                    гораздо выразительнее, чем

                                                                    some_function($a = null, $b = 1, $c = 0, $d = false, $e = true. $f = 'all', $g = 100, $h = $foobar)
                                                                    
                                                • 0
                                                  Довольно холиварная тема, на самом деле. Мне нравится способ «обязательные раздельно, необязательные потом массивом». В одном из выпусков JavaScript Jabber он был упомянут.
                                                  • 0
                                                    Такой вариант тоже неплох, только надо объективно подойти к выбору обязательных, а то потом выясняется, что обязательные вовсе и не такие уж и обязательные.
                                          • 0
                                            Если интересно, то, как вы можете догадаться, то что выше не первый вариант скрипта, он менялся и так было проще в процессе, особенно для использования str_replace, как минишаблонизатора. А еще я в конце статьи специально написал постскриптум :)
                                          • +3
                                            Вот этой штукой пользовался, довольно сносно работает: www.watchmyticket.com/
                                            • 0
                                              Спасибо. Поменял билеты на 30-е число, сэкономил 1,5к благодаря вам.
                                              • 0
                                                Ахаха, дисклеймер «Банду Геть!» доставил )))
                                              • +1
                                                Да. РЖД вообще странная компания. Впервые за 33 года моей жизни сокращают количество поездов до 4 в неделю (раньше было 14, т.е. два раза в день, потом стало 7). При этом как не попробуешь билет брать, так мест нет. Или у туалета или верхние. На сайте минимум свободных мест. При этом сокращение поездов мотивировали низким спросом. Типа сколько из областного бюджета заплатите, столько и будет поездов хранить. И это с тем учётом, что билеты на автобус дешевле плацкарта. Разве что не все готовы в автобусе 6 часов в пути проводить, особенно если с детьми.
                                                • +2
                                                  Тут где-то была статья про датацентр РЖД, там прямым текстом сказали, что пассажирский трафик РЖД вообще не уперся.
                                                  • +1
                                                    Ну собственно они и выдавливают пассажиров повышением тарифов. Кто может, тот на машине или автобусе рейсовом едет.
                                                • 0
                                                  Как минимум уже один раз на хабре была подобная вещица, весной вроде бы.
                                                  • 0
                                                    Такого способа, с json-ом, и двумя запросами, единственно актуальными сейчас я не нашел.
                                                  • +1
                                                    Помнится весной хотел купить билет на поезд — их нет. Через час дёрнуло меня ещё раз посмотреть — опа, билет есть. Пока тыкал в кнопки — его купили/заблокировали. В 5 минут следующего часа смотрю — снова 1 билет — успел купить.

                                                    Также летом покупал билет. Выбрал вагон, в нём 50/50 свободных мест, дошёл до выбора места — решил вернуться обратно, пощёлкал другие поезда, возвращаюсь заново к своему поезду и вагону, а там уже 49/50 свободных мест. Тут я вспомнил ситуацию с весной и решил подождать до начала следующего часа, ради интереса. И действительно, в начале следующего часа снова стало 50/50 свободных мест.

                                                    К чему я это всё писал: на определённом этапе оформления билета он блокируется для продажи. А дальше пользователь может купить этот билет, а может отказаться. Сброс блокировки некупленных билетов происходит раз в час (по крайней мере так было весной и летом), поэтому обновлять по крону раз в 5 минут никакого смысла нет.
                                                    • 0
                                                      В интерфейсе при переходе к покупке билета написано, что операцию необходимо выполнить в течение 10 (насколько я помню) минут – именно на это время бронируется место, при неоплате бронь снимается. К началу часа это отношения не имеет.
                                                    • 0
                                                      офтоп — но может кому-то поможет
                                                      хотел предупредить граждан других стран (не РФ)
                                                      у них там косяк сейчас с оформлением билетов (уже месяц как — наверное в связи с борьбой со шпионами)
                                                      при вводе серии паспорта латинскими буквами возникает ошибка — потому что система проверяет только буквы из кириллицы.
                                                      например, если серии типа PP (латинские)- то водите РР (русские)
                                                      • –1
                                                        Нормальный интерфейс сайта? Разработчики?
                                                        Этот человек Вам искренне улыбается
                                                        image
                                                        • 0
                                                          Вот тут и выясняется, что у каждого путешествующего программиста есть скрипт в загашнике )

                                                          Спасибо за скрипт! Вы молодец, что докопались. Я с наскока не смог обойти защиту, стал мониторить более простым способом: скриптом с GET-запросом на Яндекс.Расписания:
                                                          rasp.yandex.ru/buy/options/?number=154А&thread=154AB_tis&t_type=train&station_to=9602494&station_from=2006004&date=2014-01-01
                                                          Запрос возвращает либо «retrieving», либо ответ в формате JSON.
                                                          Выяснил также, что сервис Яндекса обновляет данные из базы РЖД с некоторым интервалом, из-за этого билет на Яндексе может появиться на несколько минут позже. Забыл уже, где-то около 2-х минут разница, но этих пару минут хватает, чтобы билет уже кто-то увел.

                                                          Плюс Яндекса в том, что можно мониторить билеты на любой транспорт.
                                                        • 0
                                                          Спасибо за статью, сам об этом доумал, но не было времения заняться.

                                                          Если в скором появится много скриптов и сервисов по отловле билетов — будет важно то, кто первее получит информацию и кто быстрее сможет ею воспользоваться.
                                                          Второй этап тоже можно автоматизировать по максимуму — например приложение в телефоне, которое хранит даынне карточки и может и имеет досту к смс (для чтения кода от банка).
                                                          — Жду следующую статью — высокоскоростная выкупка освободившихся билетов РЖД.
                                                          • 0
                                                            Спасибо, отличный скрипт, все работает.

                                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.