[Из песочницы] История о парсинге одного aspx сайта
Предыстория Существует одна онлайн-система работы с заявками клиентов, с которой моему молодому человеку приходится работать. Система, вероятно, функциональная, хороша для администраторов, эффективная в управлении и прочая, но насколько же неудобна она в ежедневном использовании! Не запоминает логин, пароль и город — в результате после входа нужно дождаться загрузки всех заявок из дефолтного города, а потом переходить на свой. Не вся необходимая информация доступна из общего списка заявок. За частью ее приходится заглядывать внутрь заявки, а каждая из них открывается в новом окне (там джаваскрипт и нет даже нормального атрибута href, представляете?). Сделана эта прелесть на asp, и поэтому при каждом переходе гоняет по сети свои viewstate. Ну и минимальная ширина сайта в полторы с чем-то тысячи точек не доставляет удовольствия. Специфика же работы заставляет иногда заходить в систему с мобильного телефона и с мобильного же интернета.И если бы я работала с ней сама, то ничего не случилось бы — привыкла бы, адаптировалась, да и вообще, начальство же жаждет… Но близкого человека жалко, и возникла идея написать парсер заявок.История Вообще-то я верстальщик. И веб-разрабочик, но в этой стороне скилл не столь высок, всего лишь делаю сносные сайты на wordpress’е. Со всякими суровыми curl запросами до этого не сталкивалась. И с aspx сайтами — тоже.Но ведь интересно же!(вылилось это в месяц вечеров с php и несколько бессонных ночей. И море удовольствия, конечно же)Сначала были попытки кроссдоменных запросов с помощью javascript, но в этой стороне ничего не вышло.Потом несмелые раскопки в стороне phantomjs и прочей эмуляции поведения пользователя. Но, оказалось, что навыков js у меня все же не хватает.В результате все работает на curl запросах, идущих от php страницы.
Получение информации Авторизация получилась достаточно быстро, и заработала более или менее без проблем.Самой противной проблемой оказалось ограничение по количеству неправильных вводов пароля: два раза — и звони админу, восстанавливай доступ…А вот с переходом на нужный город упорно не получалось. Переход производился, но куда-то не туда, хотя POST запрос был выполнен по всем правилам.Оказалось, что preg_match некорректно работает с очень большими скрытыми полями.От этого спасает указание директивы
ini_set («pcre.backtrack_limit», 10000000); Сначала получаем начальное состояние страницы (так как мы еще не залогинены, то попадаем на страницу логина), и выдираем оттуда viewstate: $url = 'http://***/Default.aspx'; $content = curlFunction ($url); preg_match_all (»/id=\»__VIEWSTATE\» value=\»(.*?)\»/», $content, $arr_viewstate); $viewstate = urlencode ($arr_viewstate[1][0]); Теперь, уже имея на руках актуальный слепок состояния страницы, вводим логин и пароль.(postdata — это параметра POST запроса к странице, подсмотреть можно в том же firebug). $url = 'http://***/Default.aspx? ReturnUrl=%2fHome%2fRoutes.aspx'; $postdataArr = array ( '__LASTFOCUS=', '__EVENTTARGET=', '__EVENTARGUMENT=', '__VIEWSTATE='.$viewstate, 'ctl00$cphMainContent$loginBox$loginBox$UserName='.$login, 'ctl00$cphMainContent$loginBox$loginBox$Password='.$password, 'ctl00$cphMainContent$loginBox$loginBox$LoginButton=Войти', ); $postdata = implode ('&',$postdataArr); $content = curlFunction ($url, $postdata); preg_match_all (»/id=\»__VIEWSTATE\» value=\»(.*?)\»/iu», $content, $arr_viewstate); $viewstate = urlencode ($arr_viewstate[1][0]); Из-за того, что начальная ссылка выдана с редиректом, а у curl стоит настройка curl_setopt ($ch, CURLOPT_FOLLOWLOCATION, 1); // переходит по редиректам мы получаем мы получаем в результате viewstate нужной нам страницы.Именно на этом моменте и возникала проблема с неработающим preg_replace, но решение — спасибо хабру — нашлось.Есть! Теперь можно переходить на заявки для нужного города и заниматься уже парсингом.
$url = 'http://***/Home/Routes.aspx'; $postdataArr = array ( '__EVENTTARGET=ctl00$cphMainContent$ddlCityID', '__EVENTARGUMENT=', '__LASTFOCUS=', '__VIEWSTATE='.$viewstate, 'ctl00$cphMainContent$ddlCityID='.$city, 'ctl00$cphMainContent$tbConnectionDate='.$date, ); $postdata = implode ('&',$postdataArr); $content = curlFunction ($url, $postdata); Когда уже наконец понимаешь, что делаешь, все достаточно просто: надо перейти именно по той ссылке, viewstate которой получили в прошлом шаге.Обработка информации Добрались, начинаем парсить.Первый опыт был связан с регулярными выражениями. К сожалению, php на хостинге как-то очень странно работал с многострочными выражениями, и не выдирал полностью select (со всеми option), как бы я его не уговаривала (при этом на локалке все работало).Следующим шагом оказалась библиотека Simple Html Dom. Все хорошо, добрались, переходим по ссылкам и разбираем информацию… Получение одной страницы занимает 0,9 секунд, получение же данных из пяти input’ов на странице — еще 5 секунд. Когда нужно перейти по девяти таким ссылкам, все становится очень печально.
Гуглим, думаем, читаем. Находим Nokogiri. Вы знаете, легко и стояще! Действительно быстрая и приятная в работе вещь:
$html = new nokogiri ($content); //Получаем данные input’а $RepairNumber = $html→get ('#ctl00_cphMainContent_tbRepairNumber')→toArray (); $result['RepairNumber'] = $RepairNumber[0]['value']; //Получаем данные select’а $ConnectionTimeArr = $html→get ('#ctl00_cphMainContent_ddlConnectionTime')→toArray (); foreach ($ConnectionTimeArr as $e) { foreach ($e['option'] as $el) { if (isset ($el['selected'])) { $result['ConnectionTime'] = $el['#text'][0]; } } } Красота и оформление Внезапно появилась очень странная проблема: собственно заказчик с явственным недовольством пользовался версией разработчика без css, js и прочих наворотов. Точнее, он вообще не понимал, как этим можно пользоваться.Что ж, берем в руки гугл, и ищем информацию об XHR-запросах. Хорошо хоть не пришлось учить еще и css, с этой-то сферой разработки достаточно сталкиваюсь на работе!
//забираем данные, необходимые для POST-запроса var login = $('#login').val (); var password = $('#password').val (); var val = $('#datePicker').val (); //формируем запрос var params = 'login=' + encodeURIComponent (login) + '&password=' + encodeURIComponent (password) + '&date=' + encodeURIComponent (date) + '&firstlogin=true'; //открываем соединение и отдаем данные скрипту на сервере, который логинится, переходит по ссылкам, забирает нужную информацию var req = getXmlHttp () req.open ('POST', 'script.php', true) req.setRequestHeader ('Content-Type', 'application/x-www-form-urlencoded') req.send (params); //затемняем экран — ну нужны же плюшечки! $('.dark').fadeIn (); req.onreadystatechange = function () { if (req.readyState == 4) { if (req.status == 200) { //получаем данные, выводим и выключаем затемнение $('.dark').fadeOut (); $('#worker').html (req.responseText); } } } Профит! Пользователь радуется, мобильный телефон пользователя избавлен от необходимости перегонять по мобильному же интернету тонны viewstate’ов, да и управлять оформлением собственноручно написанной страницы как-то проще.
P.S. Вот только меня спросили, можно ли с помощью этого клиента еще и изменять данные в системе работы с заявками. Похоже, это была угроза…