Pull to refresh

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

Reading time5 min
Views12K

Предыстория


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

//забираем данные, необходимые для 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. Вот только меня спросили, можно ли с помощью этого клиента еще и изменять данные в системе работы с заявками. Похоже, это была угроза…
Tags:
Hubs:
Total votes 40: ↑18 and ↓22-4
Comments19

Articles