Парсинг данных из разных источников. Кто и как это делает, и как это делать правильно. Как нужно правильно использовать парсинг в Ларавел вообще и в Laravel 5.6 в частности. В данном конкретном случае я хочу акцентироваться на том, как создавались парсеры для порталов и сервисов, и почему это было наказанием, разгребать эти легаси решения.
Когда я пришел в команду, то никто не хотел разгребать логику и функционал парсеров, которых было десятки на проекте. Все старались держаться от этого подальше. А так как меня взяли на позицию сина, то руководитель проекта в первый же день вывалил мне это на голову.
Первое — это то, что все парсеры писались разными программистами и в разные временные периоды. Логика всех решений была абсолютно разная. Где-то парсили регулярками, где-то simpleHtml либой, где-то через встроенные в PHP DOM инструменты. В общем, кто в лес, кто по дрова.
Но все эти решения объединяли некоторые факторы. А именно: отсутствие документации и комментариев в коде, отсутствие самой концепции парсинга данных (какого-то единого подхода и логики реализации), огромное количество всяких проверок в DOM страницы (куча встроенных if-else, switch-case), множественные циклы, вложенные друг в друга.
Ну и самое страшное — это то, что сама логика жестко привязана к структуре документа. В итоге, буквально через 3-6 месяцев после написания этих парсеров, сайты-доноры меняли дизайн, верстку, кто-то другой движок стал использовать и т.д. И все решения по парсингу перестали работать, а некоторые вообще не то забирали с других сайтов.
Вначале разработчики все эти парсера латали, подстраивая под изменения на сайте-доноре. Потом в какой-то момент руководители плюнули на это и поручили менеджерам в ручном режиме переносить информацию к нам на ресурс.
Типичные проблемы парсинга сайтов
Здесь я сразу хочу выделить ряд проблем, которые приводят к таким последствиям. Первое — это то, что команда не обсуждает архитектуру будущего решения, не создает документ с концепцией и архитектурой, не смотрят на много шагов вперед. И это не смотря на то, что люди работают по скраму.
Правда я видел что такое скрам в датской и бельгийской компании, и что такое скрам в отечественных компаниях. Так если в первом случае люди его используют для решения своих бизнес задач, то во втором — люди собираются вместе чтобы просто потрындеть о ни о чем и разойтись, чтобы начать изобретать свои велосипеды. А начальство с гордостью может заявить своим инвесторам, что они работают по скраму.
Дальше, следующая проблема, которая вытекает из первой. Разработчик в трекере получает задачу и тут же начинает ее пилить так, как он умеет или ему хочется. И последняя проблема — это опять хотелки руководителя проекта сделать всё на вчера, а если что, то завтра уже сделаем как положено. В 99.99% случаев, завтра не наступает никогда.
Требования к парсеру сайтов
Итак, поняв причины проблемы и саму проблему мы можем смело перейти к вопросу создания парсера. Давайте начнем формировать требования к парсеру, который будет нам упрощать жизнь, а не усложнять.
Давайте накидаем ряд пунктов (будем уже говорить о Laravel 5.6):
- Единый парсер для всех задач компании.
- Единая и понятная логика работы парсера.
- Добавление нового источника для парсинга должно занимать не более 10 минут.
- Парсер должен иметь админку с простым и понятным интерфейсом.
- Парсер должен уметь забирать не только текст, но и файлы.
- Перенастройка парсера должна занять не более 10 минут.
- Готовый парсер не должен требовать от программиста лазить в код.
- Вести лог того, что, когда и от куда. Успешно ли завершилась операция.
- Парсер не должен повторно обрабатывать уже обработанные данные.
- Система импорта/экспорта настроек.
Для начала 10 пунктов будет достаточно. Хотя, на самом деле, их будет определенно больше. Давайте перейдём к практике. Данный пост рассчитан на подготовленных и зрелых разработчиков, то я пройдусь по концепции и основным моментам реализации. Разжевывать код я не буду.
Главная задача — на тестовой машине отладить все настройки парсера, чтобы он правильно забирал данные без ошибок. И когда вы все настроите и убедитесь в том, что все работает как надо, вам эти настройки теперь нужно перенести на продакшн.
Логика парсера сайтов на PHP
Теперь пару слов о самой логике парсера. Предположим, что вам нужно забрать десять новостей с какого-то сайта. Как вы будете это делать? Самое простое и правильное — это заходить парсером на некую общую страницу, где содержится список новостей. Внимание! Не сама новость, а именно список новостей. Это может быть главная страница ил конкретный раздел новостей.
В настройках парсера у вас указана логика поиска таких ссылок на странице донора. Итак, парсер заходит на страницу раздела и находит все ссылки, которые введут на новости (конкретная новость с текстом, изображениями и т.д.). Потом он начинает переходить по этим ссылкам и забирать контент, согласно настройкам парсера.
Если у вас в настройках указано, что нужно забрать только одну новость, то парсер перейдет только по одной ссылке. При этом нужно не забывать сверять ссылки с данными в логе. Вам нужно перейти по новой ссылке и забрать новый контент.
Библиотеки для парсинга html сайтов
Вот наконец мы подошли и к самому коду. Я более чем уверен, что вы без проблем можете создать все миграции и модели самостоятельно.
Для начала нам нужно создать отдельную и абстрактную бизнес логику самого парсера. Я привык создавать в App папочку Modules и хранить там отдельные классы для независимых решений.
В Laravel последних версий встроен отличнейший инструмент для парсинга — это компонент от Symfony DomCrawler. Именно благодаря этому компоненту ваша жизнь может очень сильно упроститься.
Его нужно импортировать в класс парсера:
use Symfony\Component\DomCrawler\Crawler;
Метод получения контента примерно следующий:
/**
* Get content from html.
*
* @param $parser object parser settings
* @param $link string link to html page
*
* @return array with parsing data
* @throws \Exception
*/
public function getContent($parser, $link)
{
// Get html remote text.
$html = file_get_contents($link);
// Create new instance for parser.
$crawler = new Crawler(null, $link);
$crawler->addHtmlContent($html, 'UTF-8');
// Get title text.
$title = $crawler->filter($parser->settings->title)->text();
// If exist settings for teaser.
if (!empty(trim($parser->settings->teaser))) {
$teaser = $crawler->filter($parser->settings->teaser)->text();
}
// Get images from page.
$images = $crawler->filter($parser->settings->image)->each(function (Crawler $node, $i) {
return $node->image()->getUri();
});
// Get body text.
$bodies = $crawler->filter($parser->settings->body)->each(function (Crawler $node, $i) {
return $node->html();
});
$content = [
'link' => $link,
'title' => $title,
'images' => $images,
'teaser' => strip_tags($teaser),
'body' => $body
];
return $content;
}
// Create new instance for parser. $crawler = new Crawler($html);
Но если делать так, то вы получите ошибки путей при парсинге изображений. Поэтому, нужно обязательно передать в краулер линк (ссылка для парсинга), а потом и страницу:
// Create new instance for parser. $crawler = new Crawler(null, $link); $crawler->addHtmlContent($html, 'UTF-8');
Админка для парсера сайтов на Laravel
При создании роута обязательно делайте параметр secret token, чтобы только вы могли запустить парсер с сайта (если вы не используете демон), например так:
// Start parser. Secret token need for start parser via cron. Route::get('start-parser/{token}', 'ParserController@start')->name('parser.start');
// Get parsered links; $parseredLinks = ParserLog::whereParserId($parser->id)->select('parsered_indexes')->get(); $parserClass = new ParserClass(); // Make parsing sites from DB. Get parsing data. $parserData = $parserClass->parserData($parser, $parseredLinks);
В моём конкретном случае, получили для парсера все ссылки из лога, которые уже были распарсены, и передали вместе с парсером в метод parserData(). В переменную $parserData вы получите массив с «красивыми» данными, которые можете сохранять в базу. А уже в самом парсере делаете проверку на то, парсили ли вы по данной ссылке или нет.
Парсинг фото и картинок с сайтов
Теперь пару слов о том, что делать с изображениями в теле текста. Ведь все изображения хранятся на сайте донора. Здесь тоже поможет краулер и пакет UploadImage:
/**
* Save all remote images from body to disk and create new body.
*
* @param $body string body text
* @param $link string link to parsing site
*
* @return string new body
*/
public function saveImagesFromBody($body, $link)
{
// Create new instance for parser.
$crawler = new Crawler(null, $link);
$crawler->addHtmlContent($body, 'UTF-8');
// Get path for images store.
$savePathArray = \Config::get('upload-image')['image-settings'];
$savePath = UploadImage::load($savePathArray['editor_folder']);
$contentType = $savePathArray['editor_folder'];
// Get images from body.
$crawler->filter('img')->each(function (Crawler $node, $i) use ($contentType, $savePath) {
$file = $node->image()->getUri();
try {
// Upload and save image.
$image = UploadImage::upload($file, $contentType, true)->getImageName();
// Replace image in body.
$node->getNode(0)->removeAttribute('src');
$node->getNode(0)->setAttribute('src', $savePath . $image);
$node->getNode(0)->removeAttribute('imagescaler');
$node->getNode(0)->removeAttribute('id');
$node->getNode(0)->removeAttribute('class');
$node->getNode(0)->removeAttribute('srcset');
$node->getNode(0)->removeAttribute('sizes');
} catch (UploadImageException $e) {
return $e->getMessage() . '<br>';
}
});
$newBody = strip_tags($crawler->html(), '<img><span><iframe><blockquote><div><br><p>');
return $newBody;
}
В этот метод мы передаем в краулер само тело и ссылку на донора, откуда был получен данный текст. И полностью проходимся по DOM с изображениями. Как только мы получили ссылку на изображение, то сразу его сохраняем к нам на сервер (если нужно, то прикрепляем водяной знак к изображению), и меняем пути у изображения, попутно удаляя весь возможный мусор из тэга.
В конце из текста вырезаем все html тэги, кроме разрешённых.
Публикация данных в фейсбук
В контролере, когда вы сохраняете в данные базу, можно навесить событие по отправке данных на страницу в фэйсбук. Делается это очень просто, например так:
// Add post to facebook page. if($parser->facebook) { $this->facebookAddPostToPage(route('post.show', $post->slug), $post->title); }
Указанный метод проверяет ваши ключи и делает публикацию на странице фэйсбука. Лично я использую для этого вот этот пакет: https://github.com/SammyK/LaravelFacebookSdk
Подведение итогов
В принципе, если вначале не спеша сесть и подумать над реализацией парсера, нарисовать блок схемы и расписать концепцию, то сам код пишется за пол дня. Так что программист тоже не должен забывать о поговорке: «Семь раз отмерь и один раз отреж».
Как видите, ничего сложного нет. Сам файл парсера занял 300 строк кода, а контроллер — 200 строк. Для того, чтобы работать с категориями, вам придется сделать массив со словарём, по которому вы сможете получить идентификатор вашей категории (ключ массива), в зависимости от полученного из парсера названия категории (значения массива), например так:
/** * @var array Rubrics dictionary. */ public $rubrics = [ 1 => ['Авто', 'Авто/Мото'], 2 => ['Анекдоты', 'Смешное'], 3 => ['Баяны', 'Реклама'] ];
Теперь такое решение задачи парсера я могу назвать смело — Парсер с большой буквы. Его есть куда дорабатывать и усложнять. Всем спасибо за внимание, как всегда вопросы и отзывы оставляйте в комментариях.