История о парсинге одного aspx сайта

от автора

Предыстория

Существует одна онлайн-система работы с заявками клиентов, с которой моему молодому человеку приходится работать. Система, вероятно, функциональная, хороша для администраторов, эффективная в управлении и прочая, но насколько же неудобна она в ежедневном использовании!

  1. Не запоминает логин, пароль и город — в результате после входа нужно дождаться загрузки всех заявок из дефолтного города, а потом переходить на свой.
  2. Не вся необходимая информация доступна из общего списка заявок. За частью ее приходится заглядывать внутрь заявки, а каждая из них открывается в новом окне (там джаваскрипт и нет даже нормального атрибута href, представляете?).
  3. Сделана эта прелесть на asp, и поэтому при каждом переходе гоняет по сети свои viewstate.
  4. Ну и минимальная ширина сайта в полторы с чем-то тысячи точек не доставляет удовольствия.

Специфика же работы заставляет иногда заходить в систему с мобильного телефона и с мобильного же интернета.
И если бы я работала с ней сама, то ничего не случилось бы — привыкла бы, адаптировалась, да и вообще, начальство же жаждет… Но близкого человека жалко, и возникла идея написать парсер заявок.

История

Вообще-то я верстальщик. И веб-разрабочик, но в этой стороне скилл не столь высок, всего лишь делаю сносные сайты на 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. Вот только меня спросили, можно ли с помощью этого клиента еще и изменять данные в системе работы с заявками. Похоже, это была угроза…

ссылка на оригинал статьи http://habrahabr.ru/post/221811/


Комментарии

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

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