Пишем скрипт для мониторинга билетов на поезд (часть 1)

biletfhotoОднажды летним вечером я пытался купить билеты на поезд через официальный сайт «Укрзалізниці». Сам сайт очень даже просто сделан и на нём удобно искать билеты. Для меня это намного удобней, чем стоять на вонючем вокзале с бомжами и попрошайками. Но оставалась одна проблема. Хорошие места (подальше от туалетов, не боковые) всегда занимаются первыми и остается ждать, когда начнут продавать следующий вагон.

Сидеть и караулить новый вагон у меня времени не было, поэтому я решил написать себе небольшой скрипт на PHP. Задача скрипта зайти на веб-сайт «Укрзалізниці», произвести поиск по нужным параметрам (дата, №поезда, направление), посчитать количество мест в поезде и если мест стало больше, чем было прошлый раз, то скрипт присылает СМС оповещение и/или мейл.

В этой статье я попытаюсь рассказать как всё это было. И буду очень рад, если для кого то это будет полезно. Должно быть полезно для тех, кто начал изучать PHP, cURL, JSON запросы.

Актуальная версия:

Нажмите, что бы перейти на GitHub

Принцип работы выглядит так:

  1. Задаем параметры для скрипта (POST данные)
  2. Заходим на главную страницу УЗ («Укрзалізниці»)
  3. Парсим все прелести (Токен, Куки)
  4. Формируем POST запрос для получения списка поездов.
  5. Отправляем POST запрос и обрабатываем результаты.
  6. Циклом по списку поездов ищем нужный номер поезда
  7. Формируем, отправляем, обрабатываем запрос для получения вагонов.
  8. Циклом перебираем все вагоны и ищем нужные нам места.
  9. Все записываем и выводим на экран.
  10. Отправляем смс, если соблюдены условия.

Итак, начнем:

Сначала нужно объявить все классы, которые пригодятся нам в будущем. Я записал их в uz_fn.php файле.

// Объявление классов
// Настройки поезда и вагона. Какой поезд? Откуда и куда? Что ищем, какие места?
class train_set {
public // Настройки поиска поездов по датам отправления
$station_id_from,
$station_id_till,
$station_from,
$station_till,
$date_dep,
$time_dep,
$time_dep_till,
$another_ec,
$search,
// Настройки поиска нужного поезда по готовым результатам поиска
// А так же Какой тип мест нужен?
$train, // Номер поезда. Например 148К
$coach_type, // Тип места П - Плацкарт Л - Люкс К - Купе (Все потом в урл кодируется)
$model,
$date_dep_java,
$round_trip;
}

// Класс для Параметров UZ
// Тут настройки самого сайта. Его параметры сессии, код html загруженый и т д
class uz_class {
public $html,
$cookie_gv_sessid,
$cookie_gv_server_n,
$token;
}

Теперь создадим файл index.php и первым делом подгрузим библиотеку smsclient.class.php (о ней чуть позже) и наш уже созданный uz_fn.php

Вторым делом нужно создать два объекта $uz и $tr. Эти объекты из уже ранее записанных классов uz_class и train_set

// Загружаем библиотеки
require_once('smsclient.class.php');
require_once('uz_fn.php');

// Создание объекта
$uz=new uz_class();
$tr=new train_set();

Далее, нужно установить к объекту $tr нужные параметры для поиска.
Все эти переменные будут передаваться в POST запросе. Если вам интересно какие поставить себе. Зайдите на сайт УЗ включите консоль веб-разработки и просмотрите передаваемые пакеты. Хотя если вы уже дочитали до этого места, то рассказывать вам про firebug будет даже глупо.

// Установка параметров и настроек
$tr->station_id_from = '2218217';
$tr->station_id_till = '2200001';
$tr->station_from = urlencode('Ворохта');
$tr->station_till = urlencode('Киев');
$tr->date_dep = '17.09.2015';
$tr->time_dep = urlencode('00:00');
$tr->time_dep_till = '';
$tr->another_ec = '0';
$tr->search = '';

// Настройки поиска нужного поезда по готовым результатам поиска
// А так же Какой тип мест нужен?
$tr->train = urlencode('358Л'); // Номер поезда. Например 148К
$tr->coach_type = urlencode('П'); // Тип места П - Плацкарт Л - Люкс К - Купе (Все потом в урл кодируется)
$tr->model = '0';
$tr->date_dep_java = '';
$tr->round_trip = '0';

//Если вы заметили, тут я использовал функцию
urlencode();

//Все параметры нужно передавать закодированном в url формат.

Получаем главную страницу УЗ. И сразу пишу вам функцию curl_get_uz которую я записал в файле uz_fn.php

$uz->html = curl_get_uz('http://booking.uz.gov.ua/ru/',"cookie.txt") ;

// Общая функция Get
// Получаем в ответ заголовки с куками и html страницу
function curl_get_uz($url, $cookiefilename) {
// Курлом получаем заголовки сервера с куками и html с главной страницы
if( $curl = curl_init() ) {
curl_setopt($curl,CURLOPT_URL,$url);
curl_setopt($curl,CURLOPT_RETURNTRANSFER,true);
//curl_setopt($curl,CURLOPT_COOKIEFILE, $cookiefilename);
//curl_setopt($curl,CURLOPT_COOKIEJAR,$cookiefilename);
curl_setopt($curl,CURLOPT_NOBODY,false);
curl_setopt($curl,CURLOPT_HEADER,true);
return curl_exec($curl);
curl_close($curl);
}

}

//Курл вернет нам заголовки от сервера вместе с куками. А мы парсим их)))
// Парсинг Куки сессии
preg_match('~Set-Cookie:..gv.sessid=(.*?);.path=~' , $uz->html, $str );
$uz->cookie_gv_sessid = $str[1];

// Парсинг номера сервера
preg_match('~Set-Cookie: HTTPSERVERID=(.*?);.path=~' , $uz->html, $str );
$uz->cookie_gv_server_n = $str[1];

А теперь самое интересное. УЗ очень смешно решило скрыть GV-TOKEN от ботов или Школоло-кодеров.
Получилось, что фраза из скрипта, который записывает GV-TOKEN в LocalStorage имеет вид нечто, что-то:

javascriptobfusДля меня это было новенькое, пришлось потратить 20-30 минут на разбор этой радости. Понял, что не зря называют этот алгоритм обфускации «Кровотечение из мозга» или «Brainfuck».

Спасибо блогу IntSystem.Org за то, что не пришлось писать велосипед для деобфускации jjencode в php.

Привожу вам парсинг токена и класс JJDecode для декодирование самого токена (как она работает, напишу позже)

// Парсинг GV-токена
preg_match('~gaq.push....trackPageview...;(.*?).function .. .var~' , $uz->html , $gvstr );
$jj=new JJDecode();
$str = $jj->Decode($gvstr[1]);
preg_match('~,."(.*?)..;~' , $str , $token );
$uz->token = $token[1];

// Класс для декодирования токена
class JJDecode{
protected $glob_var=null;

function Decode($str){
while(preg_match('#=~[];s*(.+?)s*={#is', $str, $mth)){
$this->glob_var=$mth[1];

$str=$this->ParseJs($str);
}

return $str;
}

/** Парсинг всех участков зашифрованных JJEncode
*
* @param string $str
* @return string
*/
protected function ParseJs($str){
$preg='#'.preg_quote($this->glob_var, '#').'=~[];s*'.
''.preg_quote($this->glob_var, '#').'={s*([s|S]+?)s*};s*'.
'[s|S]+?'.
's*'.preg_quote($this->glob_var).'.$('.preg_quote($this->glob_var, '#').'.$(([s|S]+?))())();#is';

$newstr=preg_replace_callback($preg, array($this, 'ParseStr'), $str);

return $newstr;
}

/** Функция колбека для ParseJs($str)
*
* @param array $mathes
* @return string
*/
private function ParseStr($mathes){

$obufstr=$mathes[2];
$alpha=$mathes[1];

//Выделяем начальный алфавит
$alphabet=$this->ParseAlphabet($alpha);

if(!is_array($alphabet))return '';

$alphabet=array_merge($alphabet, $this->ParseAlphabetAdd());

//Деобусфицируем строку
$newstr=$this->ParseObufStr($obufstr, $alphabet);

//Приводим строку к нормальному виду (без escape последовательностей)
$newstr=$this->ParseDecodeStr($newstr);

return $newstr;
}

/** Очищаем строку от escape-последовательностей
*
* @param string $str деобфусцированная строка
* @return string
*/
protected function ParseDecodeStr($str){
$str=preg_replace_callback('#\\([0-7]{1,3})#i', array($this, 'ParseDecodeStrCallback'), $str);

return $str;
}

/** Колбэк для ParseDecodeStr. Преобразование каждой escape - последовательности с учетом восьмиричной системы
*
* @param array $mathes
* @return string
*/
private function ParseDecodeStrCallback($mathes){
$int=$mathes[1];
$add='';

while(($dec=octdec($int))>255){
$add=substr($int, -1);
$int=substr($int, 0, -1);

if(strlen($int)<1)break;
}

return chr(octdec($int)).$add;
}

/** Приведение обфусцированной строки к упрощенному виду для облегчения разделения на "слагаемые"
*
* @param string $str обфусцированная строка
* @return string упрощенный вид
*/
private function ParseObufStrRaw($str){
$nstr='';
$incnt=0;
$quote=false;
for($i=0, $x=strlen($str); $i<$x; $i++){ $char=$str{$i}; if($char!='+'){ if($char=='"'){ $quote=!$quote; $nstr.='x'; continue; } if($quote){ if($char=='\'){ $i++; $nstr.='xx'; continue; }else{ $nstr.='x'; continue; } } if(in_array($char, array('{', '[', '('))){ $incnt++; }elseif(in_array($char, array('}', ']', ')'))){ $incnt--; } $nstr.='x'; }else{ if($quote){ $nstr.='x'; }else{ if($incnt==0){ $nstr.='+'; }else{ $nstr.='x'; } } } } $arr=array(); $words=explode('+', $nstr); $pos=0; foreach($words as $word){ $len=strlen($word); $arr[]=substr($str, $pos, $len); $pos+=($len+1); } return $arr; } /** Деобфусцирование строк * * @param string $str обфусцированная строка * @param array $alphabet алфавит * @return string */ protected function ParseObufStr($str, $alphabet){ $array=$this->ParseObufStrRaw($str);

$nstr='';
$unk=array();
foreach($array as $val){
$val=trim($val);
if(empty($val))continue;

if(preg_match('#^'.preg_quote($this->glob_var).'.([_$]{1,4})$#i', $val, $mth)){

if(array_key_exists($mth[1], $alphabet)){
$nstr.=$alphabet[$mth[1]];
}else{
$unk[]=$val;
$nstr.='?';
}
}elseif(preg_match('#^"(.*)"$#i', $val, $mth)){
$nstr.=str_replace('"', '"', stripslashes($mth[1]));
}elseif(preg_match('#((.+))['.preg_quote($this->glob_var).'.([_$]{1,4})]#i', $val, $mth)){
if(array_key_exists($mth[2], $alphabet)){
if(strpos($mth[1], '![]+""')!==false){
$tmp='false';
$nstr.=$tmp{$alphabet[$mth[2]]};
}else{
$unk[]=$val;
$nstr.='?';
}
}else{
$unk[]=$val;
$nstr.='?';
}
}else{
$unk[]=$val;
$nstr.='?';
}
}

if(count($unk)>0){

}

if(preg_match('#returns*"(.+)"#i', $nstr, $mth)){
$nstr=$mth[1];
return $nstr;
}else{
return false;
}
}

protected function ParseAlphabetAdd(){
return array(
'$_'=>'constructor',
'$$'=>'return',
'$'=>'function Function() { [native code] }',
'__'=>'t',
'_$'=>'o',
'_'=>'u',
);
}

/** Парсинг участка с алфавитом
*
* @param string $str участок строки с алфавитом
* @return array
*/
protected function ParseAlphabet($str){

if(!preg_match_all('#([_|$]{2,4}):(.+?),#i', $str.',', $mth)){

return false;
}

$newarr=array();
$val_o=0;

for($i=0, $x=count($mth[0]); $i<$x; $i++){ $key=$mth[1][$i]; $val=$mth[2][$i]; if($val=='++'.$this->glob_var.''){
$newarr[$key]=$val_o;
$val_o++;
}elseif(strpos($val, '(![]+"")')!==false){
$tmp='false';
$newarr[$key]=$tmp{($val_o-1)};
}elseif(strpos($val, '({}+"")')!==false){
$tmp='[object Object]';
$newarr[$key]=$tmp{($val_o-1)};
}elseif(strpos($val, '('.$this->glob_var.'['.$this->glob_var.']+"")')!==false){
$tmp='undefined';
$newarr[$key]=$tmp{($val_o-1)};
}elseif(strpos($val, '(!""+"")')!==false){
$tmp='true';
$newarr[$key]=$tmp{($val_o-1)};
}
}

if(count($newarr)!==16){
return false;
}

return $newarr;
}
}

Продолжение во второй части. Там мы рассмотрим, что же нам делать с полученным токеном.

А как вы думаете, есть ли более удобный способ получить и обработать информацию?

8 комментариев

  • Алексей Ответить

    Спасибо интересно. А как через curl вы заголовки получили?

    • GD Ответить

      Получить заголовки через cURL очень просто.
      Нужно установить опцию CURLOPT_HEADER равной true.

      Например:
      curl_setopt($curl,CURLOPT_HEADER,true);

  • Михаил Ответить

    Зачем вы запихнули в целые классы всего лишь по пару переменных? не проще ли в массив запихнуть?

    • Good Developers Ответить

      Что бы в будущем было удобнее добавлять разные дополнения и функции. С массивом сложнее работать было бы. Хотя тут кому как.

  • InSys Ответить

    Спасибо блогу IntSystem.Org за то, что не пришлось писать велосипед для деобфускации jjencode в php.

    И вам спасибо за то что ставите обратные ссылки и указываете авторство 🙂

  • Kirk Ответить

    Скажите, а что содержит файл cookie.txt?
    // curl_get_uz(‘http://booking.uz.gov.ua/ru/’,»cookie.txt»)

    • Good Developers Ответить

      В Curl можно использовать эту опцию для записи и чтения Cookie при запросах. Тут она как бы не нужна. Так как пришлось куки получать из самих заголовков сервера.
      //curl_setopt($curl,CURLOPT_COOKIEFILE, $cookiefilename);
      //curl_setopt($curl,CURLOPT_COOKIEJAR,$cookiefilename);
      Тут можно указать в $cookiefilename файл для записи в него самих кук. Но пока что закомментированы и не нужны.

  • Вася Ответить

    Я поборол расшифровку токена следующим образом: записал в файл а его скормил nodejs, а потом из месаги об ошибке вытянул токен

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *