Pull to refresh

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

PHP *Yii *
Sandbox
Tutorial
Введение

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

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

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

Что нам нужно

  • Для парсинга страницы будет использоваться библиотека cURL поэтому ее нужно заранее установить.
  • Так как Yii будет использоваться не сильно (для логирования процесса парсинга, самого вызова парсинга и рендеринга результата), только чтобы показать работоспособность самого парсера. Поэтому для подобных задачи вполне достаточно использовать Yii2 minimal.

Начнем

Создадим компонент 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

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

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

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

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

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

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

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

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

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

....
'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 все ли хорошо выполнилось

$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 в данном случае более удобная.

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 выражение для получения требуемых данных.

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

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

	return $this;
}


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

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 нашей темы

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;
}

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

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озданим метод для завершения парсинга (может и он не совсем нужен но все же более наглядно будет видно что парсинг данных завершен и все ли данные получили), а также методы для доступа к полученным данным

/**
 * @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 их вывод

$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
/**
 * @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>


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

Вывод

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

Теоретический аспект не был затронут в данной статье, статья была ориентированная чтобы показать на более менее реальном но простом примере как получить данные страницы.
Ссылку на код примера можно посмотреть в ресурсах.

Ресурсы

Описание Yii2 minimal
Документация Yii2
Спецификация xpath 1.0 на русском
Репозиторий с исходным кодом
Tags:
Hubs:
Total votes 18: ↑10 and ↓8 +2
Views 20K
Comments Comments 16