Парсер сайта на PHP

Рассмотрим создание парсера на PHP с использованием языка запросов xPath.

Данные будут браться с одного популярного аниме-сайта (animevost.org). Нас будут интересовать следующие данные: название, год выхода, жанр, тип, количество серий, режиссер и описание. В результате использования xPath у нас отпадает необходимость в регулярных выражениях. Все это приятно отразится на скорости работы скрипта.

Листинг файла index.php.

<?php
set_time_limit(0);

libxml_use_internal_errors(true);

$start = microtime(true);

$url = 'http://animevost.org/zhanr/';
$refererUrl = 'http://animevost.org';
$nPagePause = 4;

$data = curlGetContents($url, $refererUrl);

if ($data['code'] == 200){

  $doc = new DOMDocument();
  $doc->loadHTML($data['data']);
  $xPath = new DOMXpath($doc);

  $startPage = 1;
  $endPage = parseNumberLastPage($xPath);

  echo 'Парсер начал работу...<br>';

  while($startPage <= $endPage){
    $link = "{$url}page/$startPage/";

    $data = curlGetContents($link, $refererUrl);

    if ($data['code'] == 200){
      $doc = new DOMDocument();
      $doc->loadHTML($data['data']);
      $xPath = new DOMXpath($doc);

      $data = [];

      // Название
      $d1 = parseContent($xPath, "//div[@id='dle-content']/div/div/h2/a");
      // Год выпуска
      $d2 = parseContent($xPath, "//div[@id='dle-content']/div[@class='shortstory']/div[@class='shortstoryContent']/table/tr/td/p", 'Год выхода:', 12);
      // Жанр
      $d3 = parseContent($xPath, "//div[@id='dle-content']/div[@class='shortstory']/div[@class='shortstoryContent']/table/tr/td/p", 'Жанр:', 6);
      // Тип
      $d4 = parseContent($xPath, "//div[@id='dle-content']/div[@class='shortstory']/div[@class='shortstoryContent']/table/tr/td/p", 'Тип:', 5);
      // Количество серий
      $d5 = parseContent($xPath, "//div[@id='dle-content']/div[@class='shortstory']/div[@class='shortstoryContent']/table/tr/td/p", 'Количество серий:', 18);

      // Режиссёр
      $d6 = parseGetProducer($xPath);

      // Описание
      $d7 = parseContent($xPath, "//div[@id='dle-content']/div[@class='shortstory']/div[@class='shortstoryContent']/table/tr/td/p", 'Описание:', 10);

      // Массив для записи в файл
      $c = count($d1);
      for($i=0;$i<$c;$i++){
        $data[] = implode('|', [$d1[$i], $d2[$i], $d3[$i], $d4[$i], $d5[$i], $d6[$i], $d7[$i]]);
      }

      unset($d1, $d2, $d3, $d4, $d5, $d6, $d7, $c);

      file_put_contents('data/' . str_replace(['http:', 'https:', '//', '/', '.'], ['', '', '', '-', '_'], rtrim($link, '/')) . '.txt', implode("\n", $data));

      // Каждую n-страницу делаем паузу в 3 сек.
      if ($startPage % $nPagePause == 0){
        sleep(3);
      }

    } else {
      file_put_contents('data/errors.txt', date('d-m-Y H:i:s', time()) . ' Страница недоступна: ' . $data['errors'][0][1] . "\n", FILE_APPEND);
    }

    $startPage++;
  }

  echo 'Парсер завершил работу за ' . round(microtime(true) - $start, 1) . ' сек.<br>';

} else {
  die('Что-то пошло не так');
}

/** Функции */

/**
 * Прочесть содержимое файла в строку при помощи cUrl
 *
 * @param      $pageUrl Ссылка-источник
 * @param      $baseUrl Ссылка referer
 * @param int  $pauseTime Пауза между запросами
 * @param bool $retry Разрешить / не разрешить повторение
 * @return mixed
 */
function curlGetContents($pageUrl, $baseUrl, $pauseTime = 4, $retry = true) {
  $errors = [];

  $ch = curl_init();

  curl_setopt($ch, CURLOPT_HEADER, 0);
  curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0);
  curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  curl_setopt($ch, CURLOPT_USERAGENT, getRandomUserAgent());

  curl_setopt($ch, CURLOPT_URL, $pageUrl);
  curl_setopt($ch, CURLOPT_REFERER, $baseUrl);

  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);

  $response['data'] = curl_exec($ch);

  $ci = curl_getinfo($ch);

  if($ci['http_code'] != 200 && $ci['http_code'] != 404) {
    $errors[] = [1, $pageUrl, $ci['http_code']];

    if($retry) {
      sleep($pauseTime);
      $response['data'] = curl_exec($ch);
      $ci = curl_getinfo($ch);

      if($ci['http_code'] != 200 && $ci['http_code'] != 404){
        $errors[] = [2, $pageUrl, $ci['http_code']];
      }
    }
  }

  $response['code'] = $ci['http_code'];
  $response['errors'] = $errors;

  curl_close($ch);

  return $response;
}

/**
 * Получить случайный заголовок браузера
 *
 * @return mixed
 */
function getRandomUserAgent()
{
  $userAgents = [
    'Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36',
    'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1',
    'Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A',
    'Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)',
    'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
    'Mozilla/5.0 (Windows; U; Win 9x 4.90; SG; rv:1.9.2.4) Gecko/20101104 Netscape/9.1.0285',
    'Lynx/2.8.8dev.3 libwww-FM/2.14 SSL-MM/1.4.1',
    'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko',
  ];

  $random = mt_rand(0, count($userAgents)-1);

  return $userAgents[$random];
}

/**
 * Номер последней страницы
 *
 * @param DOMXpath $xPath
 * @return mixed
 */
function parseNumberLastPage(DOMXpath $xPath){
  $q = $xPath->query("//div[@id='dle-content']/div/table/*/td[@class='block_4']/a[last()]");
  return $q->item(0)->textContent;
}

/**
 * Получить режиссера
 *
 * @param DOMXpath $xPath
 * @return array
 */
function parseGetProducer(DOMXpath $xPath){
  $q00 = $xPath->query("//div[@id='dle-content']/div[@class='shortstory']/div[@class='shortstoryContent']/table/tr/td/p[5]/strong");
  foreach ($q00 as $item) {
    $q11[] = ($item->textContent == 'Режиссёр: ');
  }

  $q1 = $xPath->query("//div[@id='dle-content']/div[@class='shortstory']/div[@class='shortstoryContent']/table/tr/td/p[5]");
  foreach ($q1 as $item) {
    $q2[] = mb_substr($item->textContent, 10);
  }

  foreach ($q11 as $k => $item) {
    $result[] = empty($item) ? '' : $q2[$k];
  }
  return $result;
}

/**
 * Получить контент
 *
 * @param DOMXpath $xPath
 * @param string   $query Запрос xPath
 * @param string   $compare Строка для поиска
 * @param int      $lenCut Длина обрезаемого слова
 * @return array
 */
function parseContent(DOMXpath $xPath, $query = '//', $compare = '', $lenCut = 0)
{
  $result = [];
  $i = 0;

  $q = $xPath->query($query);
  if (empty($compare)){
    foreach ($q as $k => $item) {
      $result[] = mb_substr($item->textContent, $lenCut);
    }

  } else {
    foreach ($q as $k => $item) {
      if (strpos($item->textContent, $compare) !== false){
        $result[$i++] = mb_substr(str_replace("\n", ' ', $item->textContent), $lenCut);
      }
    }
  }
  return $result;
}

/**
 * Отображение данных для отладки
 *
 * @param $data
 */
function varDump($data)
{
  echo '<pre>';
  print_r($data);
  echo '</pre>';
}

Данный файл располагаем в корне сайта. Здесь же создаем директорию с именем data. В этот каталог будут сохраняться все данные, собранные парсером.

Для информативности я добавил замер скорости работы скрипта и его отображение по окончанию работы.

Итак, при помощи встроенной в PHP функции libxml_use_internal_errors(true) мы включаем обработку ошибок пользователем.

Для удобства работы объявлено несколько важных переменных:

  • $url — содержит ссылку для парсера, т.е. откуда начинаем собирать данные. В нашем случае — это ссылка на страницу http://animevost.org/zhanr/.
  • $refererUrl — ссылка для содержимого заголовка «Referer», который будет использован в HTTP-запросе.
  • $nPagePause — количество страниц, через которое будет сделана пауза. Имитация человека, якобы он вручную переходит по ссылкам.

Функция function curlGetContents($pageUrl, $baseUrl, $pauseTime = 4, $retry = true) читает содержимое удаленной страницы в строку. В ней нет ничего особенного. Используется встроенная функция curl_setopt(). Более детально можно прочитать здесь.

Кстати, если происходит ошибка, то предпринимается еще одна попытка получения данных с сайта. Если же все-таки не получается получить данные по определенной ссылке, то создается файл с ошибками errors.txt.

В строке $doc = new DOMDocument(); происходит инициализация экземпляра класса DOMDocument, который представляет все содержимое HTML- или XML-документа. А с помощью метода loadHTML() мы загружаем HTML из строки. Далее в строке new DOMXpath($doc) создаем экземпляр класса DOMXPath, который поддерживает язык запросов XPath 1.0. Подробнее про все методы и их особенности можно ознакомиться по ссылкам выше.

Переменная $startPage содержит номер начальной страницы, т.е. самой первой страницы на сайте. Аналогично, переменная $endPage содержит номер самой последней возможной страницы на сайте (имеется в виду номер страницы, который доступен в контексте по адресу, указанному в переменной $url).

Далее операция получения данных с сайта работает до тех пор, пока не будет достигнута последняя страница.

Внутри цикла while каждую итерацию создается новый экземпляр DOMDocument, загружается HTML из строки и создается новый экземпляр класса DOMXpath. Большинство участков кода имеют комментарии и пояснения.

Обратим внимание на функцию function parseNumberLastPage(DOMXpath $xPath) — извлекает последний номер страницы. В данном случае анализируются ссылки на странице в самом низу сайта и получаем последнюю страницу.

Также была создана функция function parseContent(DOMXpath $xPath, $query = ‘//’, $compare = », $lenCut = 0) для получения контента. Особых сложностей в ее работе нет, так что не стану описывать подробнее.

Для получения информации о режиссере пришлось создать отдельную функцию function parseGetProducer(DOMXpath $xPath). Все это связано с тем, что в некоторых записях (анонсах аниме) отсутствовал режиссер. В результате происходило смещение и информация не правильно собиралась с сайта. В ней сначала анализируется страница и собирается массив с информацией о режиссере (если режиссера нет, то заполняется пустой строкой). Затем происходит сравнение, в котором проверяется, если узел содержит автора с пустой строкой, то аналогично заполняем пустой строкой, иначе берем информацию о режиссере из другого массива.

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

Еще я написал функцию getRandomUserAgent() для получения случайного заголовка браузера, т.к. некоторые сайты не хотят отдавать свои данные, если им не передать подобный заголовок. Случайность добавлена просто для разнообразия.

Отмечу, что это лишь демонстрация возможностей работы с xPath в PHP. Я не стал заморачиваться с проверкой различных ошибок, т.к. задача была в максимально короткий срок получить необходимые данные. Вы же в свою очередь можете доработать скрипт под себя и даже сделать его еще лучше. Таким образом можно парсить практически любой сайт. Для этого достаточно ознакомиться с синтаксисом xPath.

Полезные ссылки

Парсер сайта на PHP (исходный код на GitHub)

 

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

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

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: