Darkmind
SNMP maniac
- Регистрация
- 31 Май 2006
- Сообщения
- 185
- Реакции
- 82
- Автор темы
- #1
Столкнулся с необходимостью автоматизации работы с веб-мордой базы телефонных номеров. В процессе решения задачи отловил несколько забавных моментов и, поскольку тема парсинга сайтов всегда была популярной, решил поделиться с сообществом в образовательных целях. Тема обучающая, взломом её тоже назвать нельзя. Плюс она исключительно специфическая и в "готовые решения" её тоже публиковать смысла нет. Поэтому я размещу её в общем форуме. На этом закончим с лирикой.
Суть: есть сайт Для просмотра ссылки Войдиили Зарегистрируйся, представляющий собой сервис, выдающий название оператора и регистратора по запросу телефонного номера. Блоки номеров, конечно, распределены по операторам, но действующее законодательство страны позволяет переходить от одного оператора к другому, сохраняя номер телефона. При этом тарификация звонков между сетями разных операторов различается и может возникнуть необходимость проверки принадлежности номера к определенной сети. Конечно, имело бы смысл использовать родной веб-сервис, но никакого SLA этот сайт не предоставляет и гарантий актуальности данных не даёт. Да и задача на первый взгляд несложная - отправить POST и получить ответ.
Посетитель видит текстовое поле и submit. Однако... Подключаемся Dragonfly (или Firebug'ом) и отправляем пробный пост. Оказывается, форма засылает куда большее количество полей, большая часть из которых пустые. Заглянем в исходник страницы и видим 20 текстовых полей для ввода номера, пара hidden полей, одно из которых содержит стрёмный 256-битный ключ (который меняется с каждым рефрешем), а другое - не менее стрёмный хэш.
При этом при отправке формы введённый номер телефона отправляется в двух полях. Одно поле остаётся неизменным, а второе каждый раз разное. Можно сделать промежуточный вывод - второе поле является контрольным. Поскольку мы отправляем уже сгенерированную форму, то второе поле заполняется автоматически и алгоритм его получения находится в коде страницы. Ищем onsubmit или onclick и, разумеется, находим. Вызывается некая функция r(), которая в свою очередь вызывает соседнюю функцию D(). Переводим обе функции в удобочитаемый вид и вот он алгоритм получения ID'шника второго "контрольного" поля.
Дело за малым - последовательно выполнить вполне понятные действия:
Откровенная эксплуатация недокументированных особенностей бразуеров - они дополняют пропущенные точки с запятой и символ всё равно выводится в читаемом формате, однако html_entity_decode() давится и не может его распознать. Чиним, последовательно применяя:
Последний штрих - превращение последовательного скрипта в веб сервис. Я для этого привёл код к объектному виду, заменил глупые die() на исключения и использовал PHP SOAP. Скучно, тривиально и любовь к SOAP и REST выходит за рамки данной темы.
Зачем я раскатал всё это? Просто в качестве туториала по подходу к парсингу каких-то данных. Такого рода задачи решаются несложно при последовательном подходе. Напоследок, в виде заключения, несколько советов:
Суть: есть сайт Для просмотра ссылки Войди
Посетитель видит текстовое поле и submit. Однако... Подключаемся Dragonfly (или Firebug'ом) и отправляем пробный пост. Оказывается, форма засылает куда большее количество полей, большая часть из которых пустые. Заглянем в исходник страницы и видим 20 текстовых полей для ввода номера, пара hidden полей, одно из которых содержит стрёмный 256-битный ключ (который меняется с каждым рефрешем), а другое - не менее стрёмный хэш.

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

Дело за малым - последовательно выполнить вполне понятные действия:
- Получаем стартовую страницу через 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 );
- Выцепляем из полученной страницы ключевые поля: хэш и 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);
- Затем выцепляем функцию получения номера контрольного поля. Это делается для того, чтобы преобразовать ее на лету в PHP-функцию. Как оказалось - движок меняет алгоритм получения номера контрольного поля каждый день.
PHP:
preg_match('/(function D\(A\).+return h;\})/i', $ce, $funcD);
- Преобразовываем функцию в 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);
- Вторая функция r() неизменна и её пока можно определить руками
PHP:function r( $keyfield, $c ) { if( !function_exists("D") ) { die("Function D() is not defined"); } return D( $keyfield ) % $c; }
- Судя по изменяемому алгоритму, я предположил, что с них станется и количество текстовых полей поменять, поэтому скрипт определяет их количество. Заодно при анализе исходного кода стало понятно, что реально видимое поле может изменяться и его тоже можно определить. Одним выстрелом двух зайцев:
PHP:
preg_match('/<input\s+name.+type="submit".+onclick="r\( document\.getElementById\(\''.$this->txtBoxId.'(\d+)\'\),.+,.+,(\d+)\)/i', $ce, $matches);
- Теперь можно вычислять какое поле из 19 "фальшивых" будет контрольным
PHP:
$controlField = r($keyfield[1], $txtBoxes[1]);
- Формируем массив на отправку. Для отправки POST'а будет достаточно обычного ассоциативного массива. Код приводить не буду - простой обход по полученному количеству полей и добавление искомого номера телефона к двум нужным полям (вычислено выше)
- Отправляем страницу точно так же через 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 );
- Вуаля, мы получили данные.

Откровенная эксплуатация недокументированных особенностей бразуеров - они дополняют пропущенные точки с запятой и символ всё равно выводится в читаемом формате, однако 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.