Пример парсинга и автоматизации получения информации

Статус
В этой теме нельзя размещать новые ответы.

Darkmind

SNMP maniac
Регистрация
31 Май 2006
Сообщения
185
Реакции
82
Столкнулся с необходимостью автоматизации работы с веб-мордой базы телефонных номеров. В процессе решения задачи отловил несколько забавных моментов и, поскольку тема парсинга сайтов всегда была популярной, решил поделиться с сообществом в образовательных целях. Тема обучающая, взломом её тоже назвать нельзя. Плюс она исключительно специфическая и в "готовые решения" её тоже публиковать смысла нет. Поэтому я размещу её в общем форуме. На этом закончим с лирикой.

Суть: есть сайт Для просмотра ссылки Войди или Зарегистрируйся, представляющий собой сервис, выдающий название оператора и регистратора по запросу телефонного номера. Блоки номеров, конечно, распределены по операторам, но действующее законодательство страны позволяет переходить от одного оператора к другому, сохраняя номер телефона. При этом тарификация звонков между сетями разных операторов различается и может возникнуть необходимость проверки принадлежности номера к определенной сети. Конечно, имело бы смысл использовать родной веб-сервис, но никакого SLA этот сайт не предоставляет и гарантий актуальности данных не даёт. Да и задача на первый взгляд несложная - отправить POST и получить ответ.

Посетитель видит текстовое поле и submit. Однако... Подключаемся Dragonfly (или Firebug'ом) и отправляем пробный пост. Оказывается, форма засылает куда большее количество полей, большая часть из которых пустые. Заглянем в исходник страницы и видим 20 текстовых полей для ввода номера, пара hidden полей, одно из которых содержит стрёмный 256-битный ключ (который меняется с каждым рефрешем), а другое - не менее стрёмный хэш.
2rfc03o.png


При этом при отправке формы введённый номер телефона отправляется в двух полях. Одно поле остаётся неизменным, а второе каждый раз разное. Можно сделать промежуточный вывод - второе поле является контрольным. Поскольку мы отправляем уже сгенерированную форму, то второе поле заполняется автоматически и алгоритм его получения находится в коде страницы. Ищем onsubmit или onclick и, разумеется, находим. Вызывается некая функция r(), которая в свою очередь вызывает соседнюю функцию D(). Переводим обе функции в удобочитаемый вид и вот он алгоритм получения ID'шника второго "контрольного" поля.
2qmgaz7.png


Дело за малым - последовательно выполнить вполне понятные действия:
  1. Получаем стартовую страницу через CURL
    PHP:
    $options = array(
        CURLOPT_URL            => $config['init_url'],
        CURLOPT_HEADER         => true,
        CURLOPT_FRESH_CONNECT  => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_USERAGENT      => 'Opera/9.80 (Windows NT 6.1; WOW64; U; en) Presto/2.10.289 Version/12.02',
    );
     
    $ch = curl_init();
          curl_setopt_array($ch, $options);
     
    $ce = curl_exec( $ch );
          curl_close( $ch );
  2. Выцепляем из полученной страницы ключевые поля: хэш и 256-битный ключ
    PHP:
    preg_match('/<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="(.+)"/', $ce, $viewstate);
    preg_match('/<input name="nusa:keyField" id="nusa_keyField" type="hidden" value="(.+)"/', $ce, $keyfield);
  3. Затем выцепляем функцию получения номера контрольного поля. Это делается для того, чтобы преобразовать ее на лету в PHP-функцию. Как оказалось - движок меняет алгоритм получения номера контрольного поля каждый день.
    PHP:
    preg_match('/(function D\(A\).+return h;\})/i', $ce, $funcD);
  4. Преобразовываем функцию в PHP и делаем ей eval()
    PHP:
    $funcD = str_replace(
        array('D(A)', 'var ', 'A.length', 'A.charCodeAt(i)'),
        array('D($A)', '', 'strlen($A)', ' ord($A{$i})'),
        preg_replace(
            '/(\s|\*|\{|;)(h|i)/i',
            '$1\$$2',
            $funcD[0]
        )
    );
     
    eval($funcD);
  5. Вторая функция r() неизменна и её пока можно определить руками
    PHP:
    function r( $keyfield, $c )
    {
        if( !function_exists("D") ) {
            die("Function D() is not defined");
        }
        return D( $keyfield ) % $c;
    }
  6. Судя по изменяемому алгоритму, я предположил, что с них станется и количество текстовых полей поменять, поэтому скрипт определяет их количество. Заодно при анализе исходного кода стало понятно, что реально видимое поле может изменяться и его тоже можно определить. Одним выстрелом двух зайцев:
    PHP:
    preg_match('/<input\s+name.+type="submit".+onclick="r\( document\.getElementById\(\''.$this->txtBoxId.'(\d+)\'\),.+,.+,(\d+)\)/i', $ce, $matches);
  7. Теперь можно вычислять какое поле из 19 "фальшивых" будет контрольным
    PHP:
    $controlField = r($keyfield[1], $txtBoxes[1]);
  8. Формируем массив на отправку. Для отправки POST'а будет достаточно обычного ассоциативного массива. Код приводить не буду - простой обход по полученному количеству полей и добавление искомого номера телефона к двум нужным полям (вычислено выше)
  9. Отправляем страницу точно так же через CURL
    PHP:
    $options = array(
        CURLOPT_URL            => 'http://www.numuri.lv/default.aspx',
        CURLOPT_HEADER         => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $data,
        CURLOPT_REFERER        => 'http://www.numuri.lv/default.aspx',
        CURLOPT_USERAGENT      => 'Opera/9.80 (Windows NT 6.1; WOW64; U; en) Presto/2.10.289 Version/12.02',
    );
     
    $ch = curl_init();
          curl_setopt_array($ch, $options);
     
    $ce = curl_exec( $ch );
          curl_close( $ch );
  10. Вуаля, мы получили данные.
Эпопея на этом не заканчивается - полученные данные слегка обфусцированы. Данные приходят в виде мешанины символов и HTML-сущностей, причём у некоторых сущностей отсутствует точка с запятой.
dzbepj.png


Откровенная эксплуатация недокументированных особенностей бразуеров - они дополняют пропущенные точки с запятой и символ всё равно выводится в читаемом формате, однако html_entity_decode() давится и не может его распознать. Чиним, последовательно применяя:
PHP:
$element = trim(strip_tags( $element ));
$element = html_entity_decode( preg_replace( '/(&#[\d]+)/i', '$1;', $element ) , ENT_NOQUOTES, 'UTF-8');

Последний штрих - превращение последовательного скрипта в веб сервис. Я для этого привёл код к объектному виду, заменил глупые die() на исключения и использовал PHP SOAP. Скучно, тривиально и любовь к SOAP и REST выходит за рамки данной темы.

Зачем я раскатал всё это? Просто в качестве туториала по подходу к парсингу каких-то данных. Такого рода задачи решаются несложно при последовательном подходе. Напоследок, в виде заключения, несколько советов:
  • Изучите возможности CURL. Принимайте куки если есть необходимость (CURLOPT_COOKIEJAR, CURLOPT_COOKIEFILE). Маскируйтесь под нормальные браузеры (CURLOPT_USERAGENT). Читайте получаемые заголовки, работайте с SSL и авторизацией.
  • Для парсинга иногда имеет смысл читать полученную страницу, как DOM. Будьте внимательны - ресурсы съедаются на ура, но большие объёмы данных становится обрабатывать удобнее.
  • Для небольших задач DOM использовать не нужно - не стреляйте из пушки по воробьям; вполне можно обойтись регулярными выражениями.
  • Изучите SOAP. Некоторые сайты предоставляют точки входа в виде вебсервисов - это избавит вас от необходимости от возни с парсингом HTML.
 

Darkmind

SNMP maniac
Регистрация
31 Май 2006
Сообщения
185
Реакции
82
Поскольку модераторы перенесли топик в "Готовые решения", пошарю полностью итоговую версию кода.
Для удобства выложу его на гитхаб: Для просмотра ссылки Войди или Зарегистрируйся

P.S. Приношу свои извинения за даблпостинг, но раз это обучалка, то счёл что апдейт лучше выложить отдельно.
 
Статус
В этой теме нельзя размещать новые ответы.
Сверху