falbar Пишем парсер контента на PHP

Пишем парсер контента на PHP

3 февраля 2018 44623 14

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

Реклама

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

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

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

svyaznoy-stranitsa-kataloga

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

svyaznoy-stranitsa-kataloga-podkategorii

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

svyaznoy-stranitsa-kataloga-vse-telefony

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

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

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

  1. Нужно получить HTML код страницы, которой нам необходим;
  2. Разбор полученного кода с сохранением данных и дальнейшей обработки их (как и в первой статье по парсингу мы будем использовать 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" => "http://httpbin.org/ip"
]);

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

otvet-servera-na-httpbin

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

otvet-servera-na-httpbin-oshibka

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

curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

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

curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

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

otvet-servera-na-httpbin-pravilnyj-vid

Двигаемся далее, мы описали переменные $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);
}

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

otvet-servera-na-httpbin-pravilnyj-vid-head

Для работы со ссылками с 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);
  }
}

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

curl-kuki-s-sajta-falbar

Всё получилось, двигаемся дальше и нам осталось добавить в параметры сеанса: прокси, заголовки и возможность отправки запросов 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" => "https://www.svyaznoy.ru/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 дереву и вытягиваем нужные нам данные, их я решил протримить, чтобы убрать лишние пробелы. А теперь выведем категории на экран:

<ul>
  <?php foreach($tmp as $value): ?>
 <li>
      <a href="https://www.svyaznoy.ru<?php echo($value["url"]); ?>" target="_blank">
           <?php echo($value["text"]); ?>
      </a>
      <ul>
          <? if(!empty($value["submenu"])): ?>
            <?php foreach($value["submenu"] as $val): ?>
            <li>
              <a href="https://www.svyaznoy.ru<?php echo($val["url"]); ?>" target="_blank">
                 <?php echo($val["text"]); ?>
                </a>
          </li>
         <?php endforeach; ?>
          <? endif; ?>
      </ul>
 </li>
 <?php endforeach; ?>
</ul>
curl-poluchili-kategorii-i-vyvodim-na-ekran

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

$html = Parser::getPage([
    "url"       => "https://www.svyaznoy.ru/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();
}

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

<div class="tovars">
   <?php foreach($tmp as $value): ?>
 <a href="https://www.svyaznoy.ru<?php echo($value["url"]); ?>" target="_blank" class="tovar">
       <img src="<?php echo($value["image"]); ?>" alt="<?php echo($value["name"]); ?>" />
        <span class="name">
         <?php echo($value["name"]); ?>
      </span>
       <span class="price">
            <?php echo($value["price"]); ?>
     </span>
       <ul class="chars">
          <? if(!empty($value["chars"])): ?>
          <?php foreach($value["chars"] as $val): ?>
          <li>
              <?php echo($val); ?>
          </li>
         <?php endforeach; ?>
          <? endif; ?>
      </ul>
 </a>
  <?php endforeach; ?>
</div>
curl-poluchili-tovary-i-vyvodim-na-ekran

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

Полный пример с библеотекай phpQuery вы найдете на github.
Реклама
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
Спасибо, Антон. Я щас раскапываю почему белый экран. Сорян за троллинговый комент, реально ! С твоей статьи начался путь изучения парсинга, белый экран я исправлю. Судя по коду скрипт ТВОЙ РАБОЧИЙ ! Надо просто копнуть его) я короче заморочуть, и отпишу тебе. Думаю еще поугараем, где ошибка у меня была. Я просто под другой магаз его адаптирую. И понял что тут самое важное не ошибится с ориентирами тегов поиска части страницы которую надо вытащить. СПАСИБО за годный контент. Удачи ! Статья про парсер новостей тоже ГУД.
userok
25 сентября 2019
Fingerprint2 они подключили и он не показывает контент, поэтому парсер не работает. Если кто знает, как обойти, подскажите пожалуйста!
morilon
1 ноября 2019
Как спарсить данные если они подгружаются во время прокрутки? Как имитировать прокрутку чтобы получить все данные?
Даниил Чистяков
20 апреля 2020
Доброго времени суток, возвращает белый экран, открывал Ваш файл с GitHub "parser.php". Пробовал на Denwer и тестовом хосте.
no_avatar