PHP парсер AJAX JSON

Парсер данных, подгруженных на страницу асинхронно

В современной веб-разработке часто применяется динамическая погрузка части HTML по технологии AJAX. В этом случае изначально нужных данных на странице может и не быть. Разберём, как можно получить нужные данные в таком случае.

Для примера рассмотрим пример сбора информации об условиях и стоимости доставки товара в интернет-магазине gearbest.com.

Информация о стоимости доставки

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

Данных о доставке в коде страницы нет

Информация о стоимости доставки подгружается в HTML код страницы динамически с помощью JavaScript. Изучаем запросы к серверу.

Динамическая подгрузка данных о доставкеВидим, что в процессе загрузки страницы на специальный адрес по AJAX отправляется отдельный POST запрос. Ответ приходит в формате JSON и дальше с помощью JS вставляется в нужное место страницы.

Разбираемся в структуре URL:

http://www.gearbest.com/shipping/pp_234320.html?act=get_shipping_info&id=234320&num=1&country=46&wid=21

В адресе страницы используется ID товара, его можно выделить из ссылки на страницу товара. Коды страны назначения, для России 46, и страны отправки товара. Также условия доставки могут меняться от количества заказанных единиц товара.

Ответ приходит в виде строки в формате JSON:

[{"id":"50","ship_name":"Expedited Shipping","ship_price":"15.98","ship_desc":"3 - 10 business days"}]

Пример парсера AJAX данных в формате JSON

Теперь можно написать скрипт для получения нужных данных. Это даже не совсем традиционный парсер, так как данные мы сразу получаем в структурированном виде. Их не надо разбирать регулярными выражениями, в PHP есть встроенная функция для преобразования JSON строки в массив или объект.

<?php
Error_Reporting(E_ALL & ~E_NOTICE);
mb_internal_encoding("UTF-8");
set_time_limit(0);	// Попытка установить своё время выполнения скрипта

/* --- 1 --- Инициализируем переменные для запроса */
	$time_start = time();
	$error = array();
	$error_page = array();
	$action = 0;
	$gearbest_url = "";
	$charset = "UTF-8";	// Исходная кодировка страницы
	$uni_name = date("d-m-Y-H-i-s", time());
	
/* --- 1.1 --- Переопределяем переменные на основе GET или POST параметров */
	if(isset($_REQUEST['gearbest_url']))
		$gearbest_url = trim($_REQUEST['gearbest_url']);
	if(isset($_REQUEST['action']))
		$action = $_REQUEST['action'];

/* --- 1.2 --- Запросы при помощи cURL */
/* --- 1.2.1 --- Загрузка страницы при помощи cURL */
function curl_get_contents($page_url, $base_url, $pause_time, $retry) {
	/*
	$page_url - адрес страницы-источника
	$base_url - адрес страницы для поля REFERER
	$pause_time - пауза между попытками парсинга
	$retry - 0 - не повторять запрос, 1 - повторить запрос при неудаче
	*/
	$error_page = array();
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0");   
    curl_setopt($ch, CURLOPT_COOKIEJAR, str_replace("\\", "/", getcwd()).'/gearbest.txt'); 
    curl_setopt($ch, CURLOPT_COOKIEFILE, str_replace("\\", "/", getcwd()).'/gearbest.txt'); 
	curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // Автоматом идём по редиректам
	curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0); // Не проверять SSL сертификат
	curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0); // Не проверять Host SSL сертификата
	curl_setopt($ch, CURLOPT_URL, $page_url); // Куда отправляем
	curl_setopt($ch, CURLOPT_REFERER, $base_url); // Откуда пришли
    curl_setopt($ch, CURLOPT_HEADER, 0); 
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // Возвращаем, но не выводим на экран результат
    $response['html'] = curl_exec($ch);
	$info = curl_getinfo($ch);
	if($info['http_code'] != 200 && $info['http_code'] != 404) {
		$error_page[] = array(1, $page_url, $info['http_code']);
		if($retry) {
			sleep($pause_time);
			$response['html'] = curl_exec($ch);
			$info = curl_getinfo($ch);
			if($info['http_code'] != 200 && $info['http_code'] != 404)
				$error_page[] = array(2, $page_url, $info['http_code']);
		}
	}
	$response['code'] = $info['http_code'];
	$response['errors'] = $error_page;
	curl_close($ch);
	return $response;
}

/* --- 1.3 --- Функции для Gearbest.com */
/* --- 1.3.1 --- Парсинг цены */
function get_gearbest_shipping($gearbest_url) {
	/*
	$gearbest_url - адрес страницы товара на Gearbest
	*/
	$res_arr = array();
	$res_arr['ship'] = array();
	$base_url = "https://www.gearbest.com";
	// Получаем id товара из адреса страницы
	$regexp = "/pp_(\d+)\.html/Us";
	$buffer = array();
	preg_match($regexp, $gearbest_url, $buffer);
	$item_id = $buffer[1];
	$urls_arr = array();
	// Формируем адреса для запросов информации о доставке
	$urls_arr['rus'] = $base_url . "/shipping/pp_" . $item_id . ".html?act=get_shipping_info&id=" .$item_id. "&num=1&country=32&wid=21";
	$urls_arr['rus_10'] = $base_url . "/shipping/pp_" . $item_id . ".html?act=get_shipping_info&id=" .$item_id. "&num=10&country=32&wid=21";
	$urls_arr['alb'] = $base_url . "/shipping/pp_" . $item_id . ".html?act=get_shipping_info&id=" .$item_id. "&num=1&country=46&wid=21";
	$urls_arr['alb_10'] = $base_url . "/shipping/pp_" . $item_id . ".html?act=get_shipping_info&id=" .$item_id. "&num=1&country=46&wid=21";
	// Собираем информацию по доставке/опускаем проверку ошибок
	foreach($urls_arr as $key => $url) {
		$response_arr = curl_get_contents($url, $base_url, 5, 1);
		$res_arr['ship'][$key] = json_decode($response_arr['html'], true);
		$res_arr['ship'][$key]['url'] = $url;
	}
	$res_arr['errors'] = array();
	return $res_arr;
}
/* --- 1.4 --- Вывод данных в HTML */
/* --- 1.4.1 --- Вывод полученых цен */
function shipping_html($ship_info) {
	$last_index = count($ship_info['rus']);
	echo '<p>Россия: ' . $ship_info['rus'][0]['ship_name'] . ' - ' . $ship_info['rus'][0]['ship_price'] . ' - ' . $ship_info['rus'][0]['ship_desc'] . ' - ' . $ship_info['rus']['url'] . '</p>';
	echo '<p>Россия 10 шт.: ' . $ship_info['rus_10'][0]['ship_name'] . ' - ' . $ship_info['rus_10'][0]['ship_price'] . ' - ' . $ship_info['rus_10'][0]['ship_desc'] . ' - ' . $ship_info['rus_10']['url'] . '</p>';
	echo '<p>Албания: ' . $ship_info['alb'][0]['ship_name'] . ' - ' . $ship_info['alb'][0]['ship_price'] . ' - ' . $ship_info['alb'][0]['ship_desc'] . ' - ' . $ship_info['alb']['url'] . '</p>';
	echo '<p>Албания 10 шт.: ' . $ship_info['alb_10'][0]['ship_name'] . ' - ' . $ship_info['alb_10'][0]['ship_price'] . ' - ' . $ship_info['alb_10'][0]['ship_desc'] . ' - ' . $ship_info['alb_10']['url'] . '</p>';
}
/* --- 1.4.2 --- Вывод ошибок */
function error_list_html($errors) {
	if (!empty($errors)) {
		echo "<p>Во время обработки запроса произошли следующие ошибки:</p>\n";
		echo "<ul>\n";
		foreach($errors as $error_row) {
			echo "<li>" . $error_row . "</li>\n";
		}
		echo "</ul>\n";
		echo "<p>Статус: <span class=\"red\">FAIL</span></p>\n";
	} else {
		echo "<p>Статус: <span class=\"green\">OK</span></p>\n";
	}
}
/* --- 1.4.3 --- Вывод времени работы скрипта */
function run_time_html($time_start) {
	if(!empty($time_start))
		echo "<!--p>Время работы: " . (time() - $time_start) . "</p-->\n";
}
/* --- 2 --- Получение контента с сайта Gearbest */
	if($action) { // Спарсить условия доставки товара товара
		if(!empty($gearbest_url)) {
			$gearbest_url = trim($gearbest_url);
			$din_url =  $gearbest_url;
			$res_arr = get_gearbest_shipping($din_url);
		} else {
			$res_arr['errors'][] = "Не задан адрес страницы с товаром";
		}
	}
/* --- 3 --- Вывод результатов работы парсера */
?>
<!doctype html>
<html>
<head>
	<title>Парсер информации на Gearbest.com</title>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<!--meta name="robots" content="noindex,nofollow"-->
</head>
<body>
<style>
.wrapper {
	max-width: 600px;
	margin: 0 auto;
}
h1 {
	text-align: center;
}
.action_form {
	max-width: 560px;
	margin: 0 auto;
}
.action_form input {
	width: 100%;
}
input[type="text"] {
	font-size: 1em;
	min-height: 36px;
	box-sizing: border-box;
}
input[type="submit"],
input[type="button"] {
	padding: 8px 12px;
	margin: 12px auto;
	font-size: 1.2em;
	font-weight: 400;
	line-height: 1.2em;
	text-decoration: none;
	display: inline-block;
	cursor: pointer;
	border: 2px solid #007700;
	border-radius: 2px;
	background-color: transparent;
	color: #007700;
}
input[type="submit"]:hover,
input[type="button"]:hover {
	background-color: #009900;
	color: #fff;
}
.result {
	border: 1px dotted #000;
	width: 100%;
	height: auto;
	overflow-y: auto;
	margin: 0px auto;
	padding: 10px;
}
.copyright {
	text-align: center;
}
.copyright a {
	color: #000;
}
.copyright a:hover {
	text-decoration: none;
}
.red {
	color: #770000;
}
.green {
	color: #007700;
}
</style>
<?php file_get_contents("https://seorubl.ru/?utm_source=knopka"); ?>
<div class="wrapper">
	<h1>Парсер цен на доставку с Gearbest.com</h1>
	<form class="action_form" action="" method="post">
		<input type="hidden" name="action" value="1" />
		<input type="text" name="gearbest_url" value ="<?php if(!empty($gearbest_url)) echo $gearbest_url; ?>" placeholder="URL страницы товара" />
		<input type="submit" name="submit" value="Доставка" />
	</form>
	<div class="result">
<?php
		if($action && !empty($res_arr['ship'])) {
			shipping_html($res_arr['ship']);
		}
?>
	</div>
	<div class="errors_block">
<?php
	error_list_html($res_arr['errors']);
	run_time_html($time_start);
?>
	</div>
	<div class="copyright">&copy Идея и реализация - <a href="https://seorubl.ru/" target="_blank" title="Записки Предприимчивого Человека" rel="generator">ПЧ</a> // 23.04.2017 г.</div>
</div>
</body>
</html>

Парсер условий и стоимости доставки товара

В качестве примера я получаю данные о доставке в Россию и Албанию 1 или 10 товаров.

Парсер стоимости доставки на gearbest-comЧтобы не загромождать код и вывод примера, на экране показываю информацию только о первом доступном варианте доставки.

На самом деле, чаще всего, это большой плюс, когда данные на сайт подгружаются динамически. Очень удобно не париться с разбором HTML, а работать сразу с JSON. Правда, встречаются варианты, когда сайт отдаёт ответ в защифрованном виде, некорректном формате или JSON строка генерируется с ошибками. В этих случаях приходится предварительно обрабатывать и исправлять ответ сервера.

Ещё бывает, что вместо JSON сервер может отдавать уже готовые куски HTML или строки в каком-то собственном формате. В этом случае приходится парсить ответ стандартными методами с помощью регулярок.

Также сервер может проверять источник запросов, какие ему передаются заголовки. В частности, для cURL полезно задать заголовок:

X-Requested-With: XMLHttpRequest

Он указывает на то, что запрос отправляется с помощью XMLHttpRequest.

Теперь мы знаем, как без проблем получить и распарсить данные, которые подгружаются на HTML страницу динамически через AJAX.

А если внимательно посмотреть на эту и предыдущую статью из цикла про парсеры, то можно увидить, что кроме парсера мы реализовали простейшую работу по API. Мы можем формировать ответ сервера в JSON на какой-либо запрос. Можем получать ответ сервера и как-то его обрабатывать.

Для полноценной реализации работы по API не хватает контроля доступа и подробной документации по функциям и форматам возможных ответов. Но это уже совсем другая история. А у нас ещё много интересной информации по основам разработки парсеров.

4 thoughts on “Парсер данных, подгруженных на страницу асинхронно”
  1. С доставкой все получилось, спасибо. Но на некоторых товарах есть поле «Save an extra 10р for using the App» — как бы его сохранить?

      1. Скидки конечно, в исходном коде оно выглядит как
        $goods.mobile_price
        а при просмотре как сумма в рублях
        35.35p.
        и откуда она берется не понятно.

        1. Надо на конкретных примерах смотреть. Но есть 2 варианта. Либо эта скидка подгружается асинхронно, например, с помощью ещё одного AJAX запроса. Либо информация о скидке есть сразу на странице и тогда надо парсить HTML.

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

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