В современной веб-разработке часто применяется динамическая погрузка части 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">© Идея и реализация - <a href="https://seorubl.ru/" target="_blank" title="Записки Предприимчивого Человека" rel="generator">ПЧ</a> // 23.04.2017 г.</div> </div> </body> </html>
Парсер условий и стоимости доставки товара
В качестве примера я получаю данные о доставке в Россию и Албанию 1 или 10 товаров.
Чтобы не загромождать код и вывод примера, на экране показываю информацию только о первом доступном варианте доставки.
На самом деле, чаще всего, это большой плюс, когда данные на сайт подгружаются динамически. Очень удобно не париться с разбором HTML, а работать сразу с JSON. Правда, встречаются варианты, когда сайт отдаёт ответ в защифрованном виде, некорректном формате или JSON строка генерируется с ошибками. В этих случаях приходится предварительно обрабатывать и исправлять ответ сервера.
Ещё бывает, что вместо JSON сервер может отдавать уже готовые куски HTML или строки в каком-то собственном формате. В этом случае приходится парсить ответ стандартными методами с помощью регулярок.
Также сервер может проверять источник запросов, какие ему передаются заголовки. В частности, для cURL полезно задать заголовок:
X-Requested-With: XMLHttpRequest
Он указывает на то, что запрос отправляется с помощью XMLHttpRequest.
Теперь мы знаем, как без проблем получить и распарсить данные, которые подгружаются на HTML страницу динамически через AJAX.
А если внимательно посмотреть на эту и предыдущую статью из цикла про парсеры, то можно увидить, что кроме парсера мы реализовали простейшую работу по API. Мы можем формировать ответ сервера в JSON на какой-либо запрос. Можем получать ответ сервера и как-то его обрабатывать.
Для полноценной реализации работы по API не хватает контроля доступа и подробной документации по функциям и форматам возможных ответов. Но это уже совсем другая история. А у нас ещё много интересной информации по основам разработки парсеров.
С доставкой все получилось, спасибо. Но на некоторых товарах есть поле «Save an extra 10р for using the App» — как бы его сохранить?
Возможно, имеются ввиду скидки при покупке через приложение магазина GearBest.
Скидки конечно, в исходном коде оно выглядит как
$goods.mobile_price
а при просмотре как сумма в рублях
35.35p.
и откуда она берется не понятно.
Надо на конкретных примерах смотреть. Но есть 2 варианта. Либо эта скидка подгружается асинхронно, например, с помощью ещё одного AJAX запроса. Либо информация о скидке есть сразу на странице и тогда надо парсить HTML.