Software, Web, Уроци

Web Scraping с PHP

Сподели с приятели

Често се сблъсквам с това – сайтове, които съдържат полезна публична информация, да нямат публично или никакво API. Това затруднява работата на програмиста, ако иска да използва предоставената информация. На помощ идва Web Scraping-ът (повече информация). С този метод се прочита съдържанието на една страница и се изважда необходимата информация. За целта на статията, ще скрейпвам (остъргвам) съдържанието на блога ми. Ако искате да ползвате готовото решение, можете да вземете кода от GitHub.

Опознай противника!

За да можем да автоматизираме процеса на парсване (правене на разбор) на интернет страница, първо трябва да анализираме нейното съдържание. Това е значителен ръчен труд, но след като веднъж проучим топологията на страницата, следва да я използваме и имплементираме в кода. Тъй като ще се придържам към моя блог, следва да разкрия и част от HTML кода, който се генерира от сървъра. Към момента се интересувам от статиите, затова като чета страницата ще търся тагове <article>. Ето и извадката от кода:

<article id="post-215" class="post-215 post type-post status-publish format-standard has-post-thumbnail hentry category-websites category-projects tag-php tag-web tag-29">
<figure class="featured-image index-image">
<a href="https://hstoyanov.com/%d0%b3%d1%80%d0%b0%d0%b4%d1%81%d0%ba%d0%b0-%d0%b1%d0%b8%d0%b1%d0%bb%d0%b8%d0%be%d1%82%d0%b5%d0%ba%d0%b0-%d0%bf%d0%b5%d0%bd%d1%8c%d0%be-%d0%bf%d0%b5%d0%bd%d0%b5%d0%b2/" rel="bookmark">
<img width="250" height="250" src="https://hstoyanov.com/wp-content/uploads/2018/03/cropped-Biblioteka-Penio-Penev-Dimitrovgrad-e15116832799731.jpg" class="attachment-hume-index-img size-hume-index-img wp-post-image" alt="" srcset="https://hstoyanov.com/wp-content/uploads/2018/03/cropped-Biblioteka-Penio-Penev-Dimitrovgrad-e15116832799731.jpg 250w, https://hstoyanov.com/wp-content/uploads/2018/03/cropped-Biblioteka-Penio-Penev-Dimitrovgrad-e15116832799731-150x150.jpg 150w" sizes="(max-width: 900px) 90vw, 800px" /> </a>
</figure><!-- .featured-image full-bleed -->
<div class="post__content">
<header class="entry-header">
<span class="cat-links"><a href="https://hstoyanov.com/category/projects/websites/" rel="category tag">Web Сайтове</a>, <a href="https://hstoyanov.com/category/projects/" rel="category tag">Проекти</a></span> <h2 class="entry-title"><a href="https://hstoyanov.com/%d0%b3%d1%80%d0%b0%d0%b4%d1%81%d0%ba%d0%b0-%d0%b1%d0%b8%d0%b1%d0%bb%d0%b8%d0%be%d1%82%d0%b5%d0%ba%d0%b0-%d0%bf%d0%b5%d0%bd%d1%8c%d0%be-%d0%bf%d0%b5%d0%bd%d0%b5%d0%b2/" rel="bookmark">Градска библиотека &#8222;Пеньо Пенев&#8220;</a></h2>
</header><!-- .entry-header -->
<div class="post-content__wrap">
<div class="entry-meta">
<span class="byline"> Written by <span class="author vcard"><a class="url fn n" href="https://hstoyanov.com/author/h-stoyanov/">H.Stoyanov</a></span></span> <span class="posted-on">Published <a href="https://hstoyanov.com/%d0%b3%d1%80%d0%b0%d0%b4%d1%81%d0%ba%d0%b0-%d0%b1%d0%b8%d0%b1%d0%bb%d0%b8%d0%be%d1%82%d0%b5%d0%ba%d0%b0-%d0%bf%d0%b5%d0%bd%d1%8c%d0%be-%d0%bf%d0%b5%d0%bd%d0%b5%d0%b2/" rel="bookmark"><time class="entry-date published" datetime="2018-03-05T20:07:59+00:00">05.03.2018</time><time class="updated" datetime="2018-03-13T14:09:39+00:00">13.03.2018</time></a></span> <span class="edit-link"><span class="extra">Admin </span><a class="post-edit-link" href="https://hstoyanov.com/wp-admin/post.php?post=215&#038;action=edit">Edit <span class="screen-reader-text">"Градска библиотека &#8222;Пеньо Пенев&#8220;"</span></a></span> </div><!-- .entry-meta -->
<div class="post-content__body">
<div class="entry-content">
<p>Още един успешен проект! Проектът имаше за цел да трансформира предходния сайт на градската библиотека в модерен такъв, който да съдържа и система за управление на съдържанието. Естествено, както бихте се досетили, реших да го направя на WordPress. Така се спести доста време за създаване на сайта, добави се система за управление на съдържанието и придоби висока сигурност. Сайтът бе изграден за 3 дни (с предварителния тест), като през цялото време имах подкрепата от екипа на библиотеката. Ето и крайния резултат &#8211; ГРАДСКА БИБЛИОТЕКА &#8222;ПЕНЬО ПЕНЕВ&#8220;. Какъв беше проблемът? Предишният сайт на библиотеката беше стар като дизайн и статичен като съдържание. Това водеше до голям брой проблеми. Основният проблем бе…</p>
</div><!-- .entry-content -->
</div><!-- .post-content__body -->
<div class="continue-reading">
<a href="https://hstoyanov.com/%d0%b3%d1%80%d0%b0%d0%b4%d1%81%d0%ba%d0%b0-%d0%b1%d0%b8%d0%b1%d0%bb%d0%b8%d0%be%d1%82%d0%b5%d0%ba%d0%b0-%d0%bf%d0%b5%d0%bd%d1%8c%d0%be-%d0%bf%d0%b5%d0%bd%d0%b5%d0%b2/" rel="bookmark">
Continue reading <span class="screen-reader-text">"Градска библиотека &#8222;Пеньо Пенев&#8220;"</span> </a>
</div><!-- .continue-reading -->
</div><!-- .post-content__wrap -->
</div><!-- .post__content -->
</article><!-- #post-## -->

Както се вижда от извадката, всяка статия съдържа множество поделементи, които можем да използваме. След като сме разгледали обстойно тага, който ни интересува, можем да пристъпим към кода.

Да сложим основите на PHP Web Scraping

За тази цел, ще изградим свой клас, който ще използваме, за да работим по-лесно с класа DOMDocument на PHP. После ще изградим друг клас, който ще използваме, за да пазим информацията за всяка статия в отделни обекти. Преди да започнем, трябва да създадем механизъм за автоматично зареждане (или прихващане) на файлове с класове. Това се налага поради факта, че на сървъра изискваме и изпълняваме един файл. Ако в този файл не сме добавили всички файлове, съдържащи класове, но създадем обект и/или извикаме клас, кода няма да работи и PHP ще хвърли Exception. Процесът на автоматично зареждане е доста прост, но се изисква обяснение, което ще опиша подробно в следваща статия.

Стига приказки, да се захващаме за работа! Нека започнем с това, да създадем празна директория, в която създаваме един файл index.php и една директория на име src. В директорията src създаваме нова директория Autoload и в нея създаваме клас файл – Loader.php. Последният съдържа:

<?php
// autoload classes as needed
namespace Stoyanov\Autoload;
class Loader
{
const UNABLE_TO_LOAD = 'Unable to load class';
// array of directories
protected static $dirs = array();
protected static $registered = 0;
public function __construct(array $dirs = array())
{
self::init($dirs);
}
public static function addDirs($dirs)
{
if (is_array($dirs)) {
self::$dirs = array_merge(self::$dirs, $dirs);
} else {
self::$dirs[] = $dirs;
}
}
/**
* Adds a directory to the list of supported directories
* Also registers "autoload" as an autoloading method
*
* @param array | string $dirs
*/
public static function init($dirs = array())
{
if ($dirs) {
self::addDirs($dirs);
}
if (self::$registered == 0) {
spl_autoload_register(__CLASS__ . '::autoload');
self::$registered++;
}
}
public static function autoLoad($class)
{
$success = FALSE;
$fn = str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php';
foreach (self::$dirs as $start) {
$file = $start . DIRECTORY_SEPARATOR . $fn;
if (self::loadFile($file)) {
$success = TRUE;
break;
}
}
if (!$success) {
if (!self::loadFile(__DIR__ . DIRECTORY_SEPARATOR . $fn)) {
throw new \Exception(self::UNABLE_TO_LOAD . ' ' . $class);
}
}
return $success;
}
protected static function loadFile($file)
{
if (file_exists($file)) {
require_once $file;
return TRUE;
}
return FALSE;
}
}

След като сме създали основата, можем да пристъпим към класа, който ще върши основната работа. В директорията src създаваме нова директория – Web и в нея файл Scraper.php. За да спестя пространство за четене, ще сложа само основата на файла. Както казах и в началото, целият проект ще бъде качен в GitHub.

<?php
// scraping a website
namespace src\Web;
use DOMDocument;
/**
* @author Hristo Stoyanov <hristo@hstoyanov.com>
*
* This class is used to simplify working with DOMDocument class.
*/
class Scraper
{
/* @var $content DOMDocument is used to save contents of a website */
protected $content = NULL;
/**
* Scraper constructor. It scrapes url and saves the data in @var $content
* @param string $url is the url we want to scrape
*/
public function __construct(string $url)
{
if (stripos($url, 'http') !== 0) {
$url = 'http://' . $url;
}
$this->content = new DOMDocument('1.0', 'utf-8');
// this is used to save precious memory
$this->content->preserveWhiteSpace = FALSE;
// @ used to suppress warnings generated from improperly configured web pages
@$this->content->loadHTMLFile($url);
}
/**
* Returns the content that is populated in constructor
*
* @return DOMDocument $content
*/
public function getContent()
{
return $this->content;
}
}

А index.php става на:

<?php
// load the autoloader
require __DIR__ . '/src/Autoload/Loader.php';
use src\Autoload\Loader as Loader;
use src\Web\Scraper as Scraper;
// add the current directory to the path
Loader::init(__DIR__ . '/');
$scraper = new Scraper('https://hstoyanov.com/blog/');
$content = $scraper->getContent();
view raw index.php hosted with ❤ by GitHub

До тук приключваме с основите. Време е да започнем работа с извличането на самите статии и запазването им в обекти.

На крачка от крайния резултат

Вече имаме съдържанието на страницата, така че е време за изцеждане на необходимата информация. Отсяваме нужната информация, за да можем по-лесно да създадем обекти. След като вече имаме и обектите, можем да ги преведем в нужния ни формат, за по-лесна работа. Тук ще се усети силата на Web Scraping-ът. Като за начало, ще добавим няколко реда към index.php:

$articleElements = $content->getElementsByTagName('article');
/* @var $articleElement DOMElement */
foreach ($articleElements as $articleElement) {
var_dump($articleElement);
}
view raw index.php hosted with ❤ by GitHub

Не забравяйте да си добавите коментар, тъй като той ще помогне на вашето IDE да разбере от какъв тип е променливата $articleElement. Това създава удобство и възможност за autocomplete. Освен това, тук е удобен момент да видим все пак как се държи кодът до момента. За целта използвам var_dump функцията. Сега ще създадем класа Article. Основната му част изглежда ето така:

<?php
// creating article from DOMElement
namespace src\Web;
use src\Web\Scraper as Scraper;
/**
* Class Article
* @author Hristo Stoyanov <hristo@hstoyanov.com>
* @package src\Web
*/
class Article
{
private $id;
protected $html;
/**
* Article constructor.
* @param \DOMElement $element is the article element
*/
public function __construct(\DOMElement $element)
{
$this->id = Scraper::getAttributeValue($element, 'id');
$this->html = $element->ownerDocument->saveHTML($element);
}
/**
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getHtml(): string
{
return $this->html;
}
}

Отново напомням, че това няма да е целият файл и проектът ще бъде качен в GitHub. Има още доста неща за вършене, като някои от тях са: да запазим линка към featured image (ако има такъв), да запазим цялото съдържание на статията, чрез извличането и от Continue Reading. За да съхраним цялото съдържание на всяка статия, ще взимаме връзката към пълната версия. След последните промени index.php приема следния вид:

<?php
// load the autoloader
require __DIR__ . '/src/Autoload/Loader.php';
use src\Autoload\Loader as Loader;
use src\Web\Scraper as Scraper;
// add the current directory to the path
Loader::init(__DIR__ . '/');
$scraper = new Scraper('https://hstoyanov.com/blog/');
$articleElements = $scraper->getContent()->getElementsByTagName('article');
$articlesArray = array();
/* @var $articleElement DOMElement */
foreach ($articleElements as $articleElement) {
$continueReading = Scraper::findElement($articleElement, 'div', 'class', 'continue-reading');
$anchorToFullArticle = $continueReading->getElementsByTagName('a')->item(0);
$scraper = new Scraper($anchorToFullArticle->getAttribute('href'));
$fullArticleElement = $scraper->getContent()->getElementsByTagName('article')->item(0);
$article = new \src\Web\Article($fullArticleElement);
$articlesArray[$article->getId()] = $article->getHtml();
}
view raw index.php hosted with ❤ by GitHub

От тук можем да развием класа Article и да добавим всичко, което ни е необходимо от една статия. По този начин можем да създадем методи, които да ни позволяват лесен начин да генерираме JSON или XML.

Още примери за употреба на Web Scraping

При правилно положена основа, този процес става значително по-лесен. С употребата на CRON задачи и/или други инструменти, можем да правим проучвания. Например – следене цените на продукти в няколко сайта. Просто трябва всеки ден да пускаме скрипта, а той да си върши работата, запазвайки ни необходимата информация. След визуализация на тази информация, можем да придобием реална представа за движението на цените.

Предложения и благодарности

Хареса Ви статията? Беше ли Ви полезна?

Можете да посетите страницата ми за контакт с мен. Освен това, можете да споделите тази статия във Facebook, Twitter, LinkedIn и където сметнете за добре. А аз Ви благодаря, че стигнахте до тази част на статията. Последно напомням, че целият код е качен в GitHub.


Сподели с приятели