Пишемо парсер за допомогою XPath і Yii

Введення
Іноді бувають завдання коли потрібно реалізувати обгортку для роботи з 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 шукаємо елемент pc 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 російською
 Репозиторій з вихідним кодом

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.