Често се сблъсквам с това – сайтове, които съдържат полезна публична информация, да нямат публично или никакво 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">Градска библиотека „Пеньо Пенев“</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&action=edit">Edit <span class="screen-reader-text">"Градска библиотека „Пеньо Пенев“"</span></a></span> </div><!-- .entry-meta --> | |
<div class="post-content__body"> | |
<div class="entry-content"> | |
<p>Още един успешен проект! Проектът имаше за цел да трансформира предходния сайт на градската библиотека в модерен такъв, който да съдържа и система за управление на съдържанието. Естествено, както бихте се досетили, реших да го направя на WordPress. Така се спести доста време за създаване на сайта, добави се система за управление на съдържанието и придоби висока сигурност. Сайтът бе изграден за 3 дни (с предварителния тест), като през цялото време имах подкрепата от екипа на библиотеката. Ето и крайния резултат – ГРАДСКА БИБЛИОТЕКА „ПЕНЬО ПЕНЕВ“. Какъв беше проблемът? Предишният сайт на библиотеката беше стар като дизайн и статичен като съдържание. Това водеше до голям брой проблеми. Основният проблем бе…</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">"Градска библиотека „Пеньо Пенев“"</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(); |
До тук приключваме с основите. Време е да започнем работа с извличането на самите статии и запазването им в обекти.
На крачка от крайния резултат
Вече имаме съдържанието на страницата, така че е време за изцеждане на необходимата информация. Отсяваме нужната информация, за да можем по-лесно да създадем обекти. След като вече имаме и обектите, можем да ги преведем в нужния ни формат, за по-лесна работа. Тук ще се усети силата на Web Scraping-ът. Като за начало, ще добавим няколко реда към index.php
:
$articleElements = $content->getElementsByTagName('article'); | |
/* @var $articleElement DOMElement */ | |
foreach ($articleElements as $articleElement) { | |
var_dump($articleElement); | |
} |
Не забравяйте да си добавите коментар, тъй като той ще помогне на вашето 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(); | |
} |
От тук можем да развием класа Article и да добавим всичко, което ни е необходимо от една статия. По този начин можем да създадем методи, които да ни позволяват лесен начин да генерираме JSON или XML.
Още примери за употреба на Web Scraping
При правилно положена основа, този процес става значително по-лесен. С употребата на CRON задачи и/или други инструменти, можем да правим проучвания. Например – следене цените на продукти в няколко сайта. Просто трябва всеки ден да пускаме скрипта, а той да си върши работата, запазвайки ни необходимата информация. След визуализация на тази информация, можем да придобием реална представа за движението на цените.
Предложения и благодарности
Хареса Ви статията? Беше ли Ви полезна?
Можете да посетите страницата ми за контакт с мен. Освен това, можете да споделите тази статия във Facebook, Twitter, LinkedIn и където сметнете за добре. А аз Ви благодаря, че стигнахте до тази част на статията. Последно напомням, че целият код е качен в GitHub.