Парсинг Сервис

Как написать парсер на php.

Чтобы написать хороший и работоспособный скрипт для парсинга контента нужно потратить немало времени. А подходить к сайту-донору, в большинстве случаев, стоит индивидуально, так как есть масса нюансов, которые могут усложнить решение нашей задачи. Сегодня мы рассмотрим и реализуем скрипт парсера при помощи CURL, а для примера получим категории и товары одного из популярных магазинов.

Если вы попали на эту статью из поиска, то перед вами, наверняка, стоит конкретная задача и вы еще не задумывались над тем, для чего ещё вам может пригодится парсер. Поэтому, перед тем как вдаваться в теорию и непосредственно в код, предлагаю прочесть предыдущею статью – парсер новостей, где был рассмотрен один из простых вариантов, да и я буду периодически ссылаться на неё.

Работать мы будем с CURL, но для начала давайте разберёмся, что эта аббревиатура обозначает. CURL – это программа командной строки, позволяющая нам общаться с серверами используя для этого различные протоколы, в нашем случаи HTTP и HTTPS. Для работы с CURL в PHP есть библиотека libcurl, функции которой мы и будем использовать для отправки запросов и получения ответов от сервера.

Двигаемся дальше и определяемся с нашей целью. Для примера я выбрал наверняка всем известный магазин svyaznoy. Для того, чтобы спарсить категории этого магазина, предлагаю перейти на страницу каталога:

Как можно увидеть из скриншота все категории находятся в ненумерованном списке, а подкатегории:

Внутри отельного элемента списка в таком же ненумерованном. Структура несложная, осталось только её получить. Товары мы возьмем из раздела «Все телефоны»:


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

Пишем скрипт парсера

Если вы уже прочли предыдущею статью, то из неё можно было подчеркнуть, что процесс и скрипт парсинга сайта состоит из двух частей:

Нужно получить HTML код страницы, которой нам необходим;
Разбор полученного кода с сохранением данных и дальнейшей обработки их (как и в первой статье по парсингу мы будем использовать phpQuery, в ней же вы найдете, как установить её через composer).

Для решения первого пункта мы напишем простой класс с одним статическим методом, который будет оберткой над CURL. Так код можно будет использовать в дальнейшем и, если необходимо, модифицировать его. Первое, с чем нам нужно определиться - как будет называться класс и метод и какие будут у него обязательные параметры:

class Parser{
public static function getPage($params = []){
if($params){
if(!empty($params[""url""])){
$url = $params[""url""];
// Остальной код пишем тут
}
}
return false;
}
}

Основной метод, который у нас будет – это getPage() и у него всего один обязательный параметр URL страницы, которой мы будем парсить. Что ещё будет уметь наш замечательный метод, и какие значения мы будем обрабатывать в нем:

$useragent – нам важно иметь возможность устанавливать заголовок User-Agent, так мы сможем сделать наши обращения к серверу похожими на обращения из браузера;
$timeout – будет отвечать за время выполнения запроса на сервер;
$connecttimeout – так же важно указывать время ожидания соединения;
$head – если нам потребуется проверить только заголовки, которые отдаёт сервер на наш запрос этот параметр нам просто будет необходим;
$cookie_file – тут всё просто: файл, в который будут записывать куки нашего донора контента и при обращении передаваться;
$cookie_session – иногда может быть необходимо, запрещать передачу сессионных кук;
$proxy_ip – параметр говорящий, IP прокси-сервера, мы сегодня спарсим пару страниц, но если необходимо несколько тысяч, то без проксей никак;
$proxy_port – соответственно порт прокси-сервера;
$proxy_type – тип прокси CURLPROXY_HTTP, CURLPROXY_SOCKS4, CURLPROXY_SOCKS5, CURLPROXY_SOCKS4A или CURLPROXY_SOCKS5_HOSTNAME;
$headers – выше мы указали параметр, отвечающий за заголовок User-Agent, но иногда нужно передать помимо его и другие, для это нам потребуется массив заголовков;
$post – для отправки POST запроса.

Конечно, обрабатываемых значений много и не всё мы будем использовать для нашей сегодняшней задачи, но разобрать их стоит, так как при парсинге больше одной страницы многое выше описанное пригодится. И так добавим их в наш скрипт:

$useragent = !empty($params[""useragent""]) ? $params[""useragent""] : ""Mozilla/5.0 (Windows NT 6.3; W…) Gecko/20100101 Firefox/57.0"";
$timeout = !empty($params[""timeout""]) ? $params[""timeout""] : 5;
$connecttimeout = !empty($params[""connecttimeout""]) ? $params[""connecttimeout""] : 5;
$head = !empty($params[""head""]) ? $params[""head""] : false;
$cookie_file = !empty($params[""cookie""][""file""]) ? $params[""cookie""][""file""] : false;
$cookie_session = !empty($params[""cookie""][""session""]) ? $params[""cookie""][""session""] : false;
$proxy_ip = !empty($params[""proxy""][""ip""]) ? $params[""proxy""][""ip""] : false;
$proxy_port = !empty($params[""proxy""][""port""]) ? $params[""proxy""][""port""] : false;
$proxy_type = !empty($params[""proxy""][""type""]) ? $params[""proxy""][""type""] : false;
$headers = !empty($params[""headers""]) ? $params[""headers""] : false;
$post = !empty($params[""post""]) ? $params[""post""] : false;

Как видите, у всех параметров есть значения по умолчанию. Двигаемся дальше и следующей строчкой напишем кусок кода, который будет очищать файл с куками при запросе:

if($cookie_file){
file_put_contents(__DIR__.""/"".$cookie_file, """");
}

Так мы обезопасим себя от ситуации, когда по какой-либо причине не создался файл.

Для работы с CURL нам необходимо вначале инициализировать сеанс, а по завершению работы его закрыть, также при работе важно учесть возможные ошибки, которые наверняка появятся, а при успешном получении ответа вернуть результат, сделаем мы это таким образам:

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
// Далее продолжаем кодить тут
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
$content = curl_exec($ch);
$info = curl_getinfo($ch);
$error = false;
if($content === false){
$data = false;
$error[""message""] = curl_error($ch);
$error[""code""] = self::$error_codes[
curl_errno($ch)
];
}else{
$data[""content""] = $content;
$data[""info""] = $info;
}
curl_close($ch);
return [
""data"" => $data,
""error"" => $error
];

Первое, что вы могли заметить – это статическое свойство $error_codes, к которому мы обращаемся, но при этом его ещё не описали. Это массив с расшифровкой кодов функции curl_errno(), давайте его добавим, а потом разберем, что происходит выше.

private static $error_codes = [
""CURLE_UNSUPPORTED_PROTOCOL"",
""CURLE_FAILED_INIT"",
// Тут более 60 элементов, в архиве вы найдете весь список
""CURLE_FTP_BAD_FILE_LIST"",
""CURLE_CHUNK_FAILED""
];

После того, как мы инициализировали соединения через функцию curl_setopt(), установим несколько параметров для текущего сеанса:

CURLOPT_URL – первый и обязательный - это адрес, на который мы обращаемся;
CURLINFO_HEADER_OUT –массив с информацией о текущем соединении.

Используя функцию curl_exec(), мы осуществляем непосредственно запрос при помощи CURL, а результат сохраняем в переменную $content, по умолчанию после успешной отработки результат отобразиться на экране, а в $content упадет true. Отследить попутную информацию при запросе нам поможет функция curl_getinfo(). Также важно, если произойдет ошибка - результат общения будет false, поэтому, ниже по коду мы используем строгое равенство с учетом типов. Осталось рассмотреть ещё две функции это curl_error() – вернёт сообщение об ошибке, и curl_errno() – код ошибки. Результатом работы метода getPage() будет массив, а чтобы его увидеть давайте им воспользуемся, а для теста сделаем запрос на сервис httpbin для получения своего IP.

Кстати очень удобный сервис, позволяющий отладить обращения к серверу. Так как, например, для того что бы узнать свой IP или заголовки отправляемые через CURL, нам бы пришлось бы писать костыль.$html = Parser::getPage([
""url"" => ""httpbin.org/ip""
]);

Если вывести на экран, то у вас должна быть похожая картина:

Если произойдет ошибка, то результат будет выглядеть так:

При успешном запросе мы получаем заполненную ячейку массива data с контентом и информацией о запросе, при ошибке заполняется ячейка error. Из первого скриншота вы могли заметить первую неприятность, о которой я выше писал контент сохранился не в переменную, а отрисовался на странице. Чтобы решить это, нам нужно добавить ещё один параметр сеанса CURLOPT_RETURNTRANSFER.

curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

Обращаясь к страницам, мы можем обнаружить, что они осуществляют редирект на другие, чтобы получить конечный результат добавляем:

curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

Теперь можно увидеть более приятную картину:

Двигаемся далее, мы описали переменные $useragent, $timeout и $connecttimeout. Добавляем их в наш скрипт:

curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $connecttimeout);

Для того, чтобы получить заголовки ответа, нам потребуется добавить следующий код:

if($head){
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_NOBODY, true);
}

Мы отключили вывод тела документа и включили вывод шапки в результате:

Для работы со ссылками с SSL сертификатом, добавляем:

if(strpos($url, ""https"") !== false){
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
}

Уже получается весьма неплохой скрипт парсера контента, мы добрались до кук и тут стоит отметить - частая проблема, когда они не сохраняются. Одной из основных причин может быть указание относительного пути, поэтому нам стоит это учесть и написать следующие строки:

if($cookie_file){
curl_setopt($ch, CURLOPT_COOKIEJAR, __DIR__.""/"".$cookie_file);
curl_setopt($ch, CURLOPT_COOKIEFILE, __DIR__.""/"".$cookie_file);
if($cookie_session){
curl_setopt($ch, CURLOPT_COOKIESESSION, true);
}
}

Предлагаю проверить, а для этого я попробую вытянуть куки со своего сайта:

Всё получилось, двигаемся дальше и нам осталось добавить в параметры сеанса: прокси, заголовки и возможность отправки запросов POST:

if($proxy_ip && $proxy_port && $proxy_type){
curl_setopt($ch, CURLOPT_PROXY, $proxy_ip."":"".$proxy_port);
curl_setopt($ch, CURLOPT_PROXYTYPE, $proxy_type);
}
if($headers){
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
if($post){
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
}

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

Парсим категории и товары с сайта

Теперь, при помощи нашего класса Parser, мы можем сделать запрос и получить страницу с контентом. Давайте и поступим:

$html = Parser::getPage([
""url"" => ""www.svyaznoy/catalog""
]);

Следующим шагом разбираем пришедший ответ и сохраняем название и ссылку категории в результирующий массив:

if(!empty($html[""data""])){
$content = $html[""data""][""content""];
phpQuery::newDocument($content);
$categories = pq("".b-category-menu"")->find("".b-category-menu__link"");
$tmp = [];
foreach($categories as $key => $category){
$category = pq($category);
$tmp[$key] = [
""text"" => trim($category->text()),
""url"" => trim($category->attr(""href""))
];
$submenu = $category->next("".b-category-submenu"")->find("".b-category-submenu__link"");
foreach($submenu as $submen){
$submen = pq($submen);
$tmp[$key][""submenu""][] = [
""text"" => trim($submen->text()),
""url"" => trim($submen->attr(""href""))
];
}
}
phpQuery::unloadDocuments();
}

Чуть более подробно работу с phpQuery я разобрал в первой статье по парсингу контента. Если вкратце, то мы пробегаемся по DOM дереву и вытягиваем нужные нам данные, их я решил протримить, чтобы убрать лишние пробелы. А теперь выведем категории на экран:


В результате мы получили все ссылки на категории. Для получения товаров используем тот же принцип:

$html = Parser::getPage([
""url"" => ""www.svyaznoy/catalog/phone/224"",
""timeout"" => 10
]);

Получаем страницу, тут я увеличил время соединения, так как 5 секунд не хватило, и разбираем её, парся необходимый контент:

if(!empty($html[""data""])){
$content = $html[""data""][""content""];
phpQuery::newDocument($content);
$products = pq("".b-listing__generated-container"")->find("".b-product-block .b-product-block__content"");
$tmp = [];
foreach($products as $key => $product){
$product = pq($product);
$tmp[] = [
""name"" => trim($product->find("".b-product-block__name"")->text()),
""image"" => trim($product->find("".b-product-block__image img"")->attr(""data-original"")),
""price"" => trim($product->find("".b-product-block__misc .b-product-block__visible-price"")->text()),
""url"" => trim($product->find("".b-product-block__info .b-product-block__main-link"")->attr(""href""))
];
$chars = $product->find("".b-product-block__info .b-product-block__tech-chars li"");
foreach($chars as $char){
$tmp[$key][""chars""][] = pq($char)->text();
}
}
phpQuery::unloadDocuments();
}

Теперь проверим, что у нас получилось, и выведем на экран:

Вот мы и написали парсер контента PHP, как видите, нет нечего сложного, при помощи этого скрипта можно легко спарсить страницы любого сайта, но перед тем, как заканчивать статью, хотелось пояснить некоторые моменты. Во-первых, если вы хотите парсить более одной страницы, то не стоит забывать, что сам процесс парсинга ресурса затратная операция, поэтому в идеале лучше, чтобы скрипт был вынесен на отдельный сервер, где и будет запускаться по крону. Ещё один момент - к каждому донору стоит подходить индивидуально, так как, во-первых: у них разный HTML код и он, с течением времени, может меняться, во-вторых: могут быть различные защиты от парсинга и проверки, поэтому для подбора необходимого набора заголовков и параметров может потребоваться отладочный прокси (я пользуюсь Fiddler). И последние, что я добавлю - используйте для парсинга прокси и чем больше, тем лучше, так как, когда на сервер донора полетят тысячи запросов, то неизбежно IP, с которого осуществляется обращение будет забанен, поэтому стоит прогонять свои запросы через прокси-сервера.

Полный пример с библеотекай phpQuery вы найдете на github.

#Парсер
Реклама
Пишем парсер новостей на PHP
19 января 2018 Антон Кулешов
ins1de 4 февраля 2018
Отличная статья. Спасибо. Как раз сейчас разбираю пхп и тему парсеров.
Антон Кулешов 4 февраля 2018
Рад, что статья вам понравилась. В одной из следующих расскажу об уже готовых решениях для парсинга сайтов.
Милена Быстрова 8 февраля 2018
ц ц цц)) се понравилось но не понятно все :D
Штиф Васлер 5 января 2019
Получается, принцип парсинга множества страниц - получаем, условно говоря, со страницы-каталога список ссылок дочерних ресурсов, по ним осуществляем переходы, получая контент. Это универсальный алгоритм обхода для любого парсера?
Антон Кулешов 6 января 2019
Да, в большинстве случаев так и делается. Также можно в зависимости от сайта нужную инфу вытягивать из: RSS-каналов, карты сайта или, если на сайте реализовано API, то и через него (но тут больше гемора, так как сложней подобрать все необходимые заголовки и т.д.).
Стринг Интегрович 20 января 2019
Случайно наткнулся на сайт по теме парсера, но оказалось что на сайте очень много полезной и интересной инфы, прямо кландайк для начинающих разрабов. Спасибо за труды
Антон Кулешов 20 января 2019
Спасибо за позитивный отзыв о сайте! Надеюсь он станет для вас хорошим источником полезной информации.
Эдуард Антонов 3 июня 2019
Антон, здравствуйте.
Сейчас сайт связного возвращает js код с редиректом. Можно ли его как-то обойти?
Посмотрите, пожалуйста.
Волтер Вайт 28 июня 2019
Странно. У меня белый экран. Просто как не крути белый экран))
Антон Кулешов 29 июня 2019
Здравствуйте, трудно сказать почему у вас белый эран. На время написания статьи, скрипт работал и отлично парсил материалы с сайта связной. Вообще, не просто написать универсальный парсер, обычно они пишутся под конкретный сайт и в течении его работы допиливается (так как сайты постепенно тоже дорабатываются). В статье приведен лишь пример написания и принцип.
Для того, чтобы написать парсер, который будет учитывать сложную разметку и JavaScript на странице, можно капнуть в другую сторону и использовать для этого Node.js и его библиотеки (например Webdriver и CasperJS) – это будет гораздо удобней и шустрей, чем реализовать скрипт на PHP.
Волтер Вайт 2 июля 2019
Спасибо, Антон. Я щас раскапываю почему белый экран. Сорян за троллинговый комент, реально ! С твоей статьи начался путь изучения парсинга, белый экран я исправлю. Судя по коду скрипт ТВОЙ РАБОЧИЙ ! Надо просто копнуть его) я короче заморочуть, и отпишу тебе. Думаю еще поугараем, где ошибка у меня была. Я просто под другой магаз его адаптирую. И понял что тут самое важное не ошибится с ориентирами тегов поиска части страницы которую надо вытащить. СПАСИБО за годный контент. Удачи ! Статья про парсер новостей тоже ГУД.