Пишем парсер с помощью XPath и Yii

94KONG

Проверенные
Сообщения
196
Реакции
87
Баллы
11,040
Введение

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

В качестве примера для данной статьи мы будем использовать выданное форума XenForo и заранее созданной темой, откуда будем парсить типичные данные: заголовок, время создания и сам текст темы, при этом парсинг будет осуществляться в авторизированном аккаунте форума. Все остальные данные можно будет взять по аналогии.

Сам парсер реализуем в виде компонента для удобного использования в Yii2.
.
Начнем

Создадим компонент ParserXenforo. Так как нам событий не нужно, вполне достаточно будет наследоваться от Object.
PHP:
namespace app\components;

use Yii;
use \yii\base\Object;

class ParserXenforo extends Object
{
}

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

PHP:
namespace app\components;

use Yii;
use \yii\base\Object;

class ParserXenforo extends Object
{
    /**
     * Uri к действию авторизации на форуме
     */
    const REQUEST_URI_LOGIN = 'login/login';
    /**
     * Название файла для сохранения cookies
     */
    const COOKIES_FILE_NAME = 'cookies.txt';
    /**
     * @var string загруженные данные страницы
     */
    private $_data;
    /**
     * @var string хост форума
     */
    public $host;
    /**
     * @var string логин пользователя
     */
    public $username;
    /**
     * @var string пароль пользователя
     */
    public $password;
    /**
     * @var array конфигурация cURL
     */
    public $curlOpt;
}

Добавим методы загрузки страницы.
Первым реализуем метод для получения установленных значений header и user-agent которые будут храниться в curlOpt, и в будущем передаваться в параметры cURL

PHP:
protected function getCurlOpt($nameOpt)
{
    if ($nameOpt !== 'userAgent' && $nameOpt !== 'header') {
        return false;
    }
    return $this->curlOpt[$nameOpt];
}

Для авторизации на форуме нужно передать через POST логин и пароль пользователя. Для этого сформируем url авторизации (host + url авторизации)

PHP:
protected function getLoginUrl()
{
    return $this->host . self::REQUEST_URI_LOGIN;
}

И строку POST запроса

PHP:
protected function createPostRequestForCurl()
{
    return 'login=' . $this->username . '&password=' . $this->password . '&remember=1';
}

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

PHP:
protected function getPathToCookieFile($cookieFileName = self::COOKIES_FILE_NAME)
{
    return Yii::getAlias('@app/runtime') . DIRECTORY_SEPARATOR . $cookieFileName;
}

Реализуем метод парсинга страницы с переданными параметрами. Сначала мы переходим на action авторизации где передаем POST значения и возвращаемся на переданный url но уже в авторизированном аккаунте. На всякий случай. Так как например часто видел что на этом форуме устанавливают модуль скрытия контента от неавторизированных пользователей.
После успешной загрузки данных в _data, логируем методом Yii::info() что данные загруженные.

PHP:
public function loadUsingCurl($url)
{
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $this->loginUrl);
    curl_setopt($ch, CURLOPT_FAILONERROR, 1);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_REFERER, $url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $this->getCurlOpt('header'));
    curl_setopt($ch, CURLOPT_COOKIEFILE, $this->pathToCookieFile);
    curl_setopt($ch, CURLOPT_COOKIEJAR, $this->pathToCookieFile);
    curl_setopt($ch, CURLOPT_FRESH_CONNECT, 1);
    curl_setopt($ch, CURLOPT_USERAGENT, $this->getCurlOpt('userAgent'));
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $this->createPostRequestForCurl());
    $this->_data = curl_exec($ch);
    if (curl_exec($ch) === false) {
        throw new \Exception(curl_errno($ch) . ': ' . curl_error($ch));
    }
    curl_close($ch);

    Yii::info(Yii::t('app', 'Loading data page'));

    return $this;
}

Базовая часть компонента реализованная. Теперь нужно его подключить в компонентах и настроить. Указав в user-agent данные своего компьютера например, где находиться компонент, базовый url и данные для авторизации.
Параметры для авторизации дали в демо admin:admin. Одно только но дали на несколько дней, а точнее до Mar 24, 2014 at 7:26 AM

PHP:
....
'components' => [
    ...
    'parser' => [
        'class' => 'app\components\ParserXenforo',
        'host' => 'http://9af5766eb2759a49.demo-xenforo.com/130/index.php?',
        'username' => 'admin',
        'password' => 'admin',
        'curlOpt' => [
            'userAgent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36',
            'header' => [
                'Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1',
                'Accept-Language: en-US,en;q=0.8,ru;q=0.6,uk;q=0.4',
                'Accept-Charset: Windows-1251, utf-8, *;q=0.1',
                'Accept-Encoding: deflate, identity, *;q=0',
            ]
        ]
    ],
    ...
],
....

В котроллере можем проверить работоспособность, вызвав в action и посмотреть в логах app.log все ли хорошо выполнилось

PHP:
$urlThread = 'http://9af5766eb2759a49.demo-xenforo.com/130/index.php?threads/some-thread.1/';
/** @var \app\components\ParserXenforo $dataParse */
$dataParse = Yii::$app->parser->loadUsingCurl($urlThread);

Парсинг данных

Начнем с создания метода для получения объекта класса DOMDocument нашей страницы и добавим свойство для хранения его. Перед тем отключив ошибки libxml и делаем обратное после загрузки. Чтобы избежать некоторых проблем с парсингом страницы. В итоге мы получаем DOM нашей страницы для дальнейшей работы с ним. Так же можно было бы использовать регулярные выражения. Но работа с DOM в данном случае более удобная.

PHP:
public function createDomDocument()
{
    $this->_dom = new \DOMDocument();
    libxml_use_internal_errors(true);
    if ($this->_dom->loadHTML($this->_data)) {
        Yii::info(Yii::t('app', 'Create DomDocument'));
    } else {
        Yii::info(Yii::t('app', 'An error occurred when creating an object of class DOMDocument'));
    }
    libxml_use_internal_errors(false);

    return $this;
}

Переходим к методу получения нового объекта класса DOMXPath, чтобы было удобно выполнять заданное XPath выражение для получения требуемых данных.

PHP:
public function createDomXpath()
{
    $this->_xpath = new \DOMXPath($this->_dom);

    Yii::info(Yii::t('app', 'Create DomXpath'));

    return $this;
}

Ну все теперь можно смело переходить к выполнению XPath запросов для получения наших данных:title, timestamp и content.
Сначала получим заголовок и добавим свойство _title

PHP:
public function parseTitle()
{
    $xpathQuery = '*//h1';
    $nodes = $this->_xpath->query($xpathQuery, $this->_dom);
    if ($nodes->length === 0) {
        Yii::info(Yii::t('app', 'Error parse title'));   
    }
    $this->_title = $nodes->item(0)->nodeValue;

    Yii::info(Yii::t('app', 'Parse title'));

    return $this;
}

Дальше timestamp нашей темы

PHP:
public function parseTimestamp()
{
    $xpathQuery = '*//p[@id="pageDescription"]/a/abbr';
    $nodes = $this->_xpath->query($xpathQuery, $this->_dom);
    if ($nodes->length === 0) {
        Yii::info(Yii::t('app', 'Error parse timestamp'));
        return $this;
    }
    // получаем значение timestamp
    $this->_timestamp = $nodes->item(0)->getAttribute('data-time');

    Yii::info(Yii::t('app', 'Parse timestamp'));

    return $this;
}

Последнее получим контент

PHP:
public function parseContent()
{
    $xpathQuery = '*//blockquote[@class="messageText ugc baseHtml"]';
    $nodes = $this->_xpath->query($xpathQuery, $this->_dom);
    if ($nodes->length === 0) {
        Yii::info(Yii::t('app', 'Error parse content'));
        return $this;
    }
    $this->_content = $nodes->item(0)->nodeValue;

    Yii::info(Yii::t('app', 'Parse content'));

    return $this;
}

Вертаемся немного назад и рассмотрим более подробно, что за XPath запросы мы сделали
  • '*//h1' ищем в DOM h1. *// означает искать по всему DOM
  • *//p[@id=«pageDescription»]/a/abbr ищем элемент p c id pageDescription в которого есть ссылка с элементом abbr
  • *//blockquote[@class=«messageText ugc baseHtml»] ищем цитату с class messageText ugc baseHtml


Cозданим метод для завершения парсинга (может и он не совсем нужен но все же более наглядно будет видно что парсинг данных завершен и все ли данные получили), а также методы для доступа к полученным данным

PHP:
/**
 * @return \app\components\ParserXenforo
 */
public function endParse()
{
    if (isset($this->_content, $this->_timestamp, $this->_content)) {
        Yii::info(Yii::t('app', 'End parse'));
    } else {
        Yii::info(Yii::t('app', 'Some data were not received'));
    }

    return $this;
}

/**
 * @return string title
 */
public function getTitle()
{
    return $this->_title;
}

/**
 * @return int timestamp
 */
public function getTimestamp()
{
    return $this->_timestamp;
}

/**
 * @return string content
 */
public function getContent()
{
    return $this->_content;
}

Вывод результатов

Компонент можно сказать что готов, можем посмотреть как он работает добавив в action нашего controller необхибые действия а view их вывод

PHP:
$urlThread = 'http://9af5766eb2759a49.demo-xenforo.com/130/index.php?threads/some-thread.1/';
/** @var \app\components\ParserXenforo $dataParse */
$dataParse = Yii::$app->parser
    ->loadUsingCurl($urlThread)
    ->createDomDocument()
    ->createDomXpath()
    ->parseTitle()
    ->parseTimeStamp()
    ->parseContent()
    ->endParse();
return $this->render('index', ['data' => $dataParse]);

PHP:
<?php
/**
 * @var yii\web\View $this
 * @var \app\components\ParserXenforo $data
 */
$this->title = 'My Yii Application';
?>
<div class="site-index">
    <h1><?= $data->title; ?></h1>
    <p>Created At: <?= date('Y-m-d H:i:s', $data->timestamp); ?></p>
    <p><?= $data->content; ?></p>
</div>

В результате получаем подобный результат

a8638f27c668c7.png
Вывод

В этой статье мы рассмотрели как сделать парсер контента страницы в виде компонента для Yii на примере парсинга темы форума XenForo.
По-аналогии можно сделать парсинг и других данных, или же создать немного другой класс который будет использовать созданный нами для парсинга например всех тем форума, по-принципу:
  • Получаем пагинацию страницы если есть.
  • Проходимся по страницах получая ссылки тем и записываем в какое-то промежуточное хранилище
  • Получаем контент по этим ссылкам.

Теоретический аспект не был затронут в данной статье, статья была ориентированная чтобы показать на более менее реальном но простом примере как получить данные страницы.
 
Сразу огворюсь,парсеров не пишу:whistling:
 
Современный облачный хостинг провайдер | Aéza
Назад
Сверху Снизу