Посты / Laravel 5.5 и парсинг. Что такое краулер (crawler).

04.10.2017 16:52
Привет друзья! Хоть год еще и не закончился, но я уже начинаю понемногу подводить итоги того, что было сделано, что не успел и почему. В общем раскладываю работу по полочкам и анализирую. И решил написать о такой важной теме, как парсинг данных из разных источников. Кто и как это делает, и как это делать правильно. Как нужно правильно использовать парсинг в Ларавел вообще и в Ларавел 5.5 в частности. Итак, мои измышления на эту тему находятся в посте.

В этом году я успел побывать на двух очень крупных проектах. Оба проекта касались финансовой тематики и их обслуживали команды приличных размеров (около 40 программистов на проекте). В данном конкретном случае я хочу акцентироваться на том, как создавались парсеры для порталов и сервисов, и почему это было наказанием, разгребать эти легаси решения.

Когда я пришел в команду, то никто не хотел разгребать логику и функционал парсеров, которых было десятки на проекте. Все старались держаться от этого подальше. А так как меня взяли на позицию сина, то руководитель проекта в первый же день вывалил мне все это "счастье" на голову.

Итак, с каким ужасом мне пришлось столкнуться. Первое - это то, что все парсера писались разными программистами и в разные временные периоды. Логика всех решений была абсолютно разная. Где-то парсили регулярками, где-то simpleHtml либой, где-то через встроенные в PHP DOM инструменты. В общем, кто в лес, кто по дрова.

Но все эти решения объединяли некоторые факторы. А именно: отсутствие документации и комментариев в коде, отсутствие самой концепции парсинга данных (какого-то единого подхода и логики реализации), огромное количество всяких проверок в DOM страницы (куча встроеных if-else, switch-case), множественные циклы, встроенные друг в друга.

Ну и самое страшное - это то, что сама логика жестко привязана к структуре документа. В итоге, буквально через 3-6 мес. после написания этих парсеров, сайты-доноры меняли дизайн, верстку, кто-то другой движок стал использовать и т.д. И все решения по парсингу перестали работать, а некоторые вообще не то забирали с других сайтов. Вначале разработчики все эти парсера латали, подстраивая под изменения на сайте-доноре. Потом в какой-то момент руководители плюнули на это и поручили менеджерам в ручном режиме переносить информацию к нам на ресурс.

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

Правда я видел что такое скрам в датской и бельгийской компании, и что такое скрам в отечественных компаниях. Так если в первом случае люди его используют для решения своих бизнес задач, то во втором - люди собираются вместе чтобы просто "потрындеть" о ни о чем и разойтись, чтобы начать изобретать свои велосипеды. А начальство с гордостью может заявить своим инвесторам, что они работают по скраму.

Дальше, следующая проблема, которая вытекает из первой. Разработчик в трекере получает задачу и тут же начинает ее пилить так, как он умеет или ему хочется. И последняя проблема - это опять хотелки руководителя проекта сделать все на вчера, а если что, то завтра уже сделаем как положено. В 99.99% случаев, завтра не наступает никогда.

Итак, поняв причины проблемы и саму проблему мы можем смело перейти к вопросу создания парсера. Давайте начнем формировать требования к парсеру, который будет нам упрощать жизнь, а не усложнять.

Давайте накидаем ряд пунктов (будем уже говорить о Laravel 5.5):
1. Должен быть единый парсер для всех наших задач.
2. Должна быть единая и понятная логика работы нашего парсера.
3. Добавление нового сайта донора в парсер должно занимать не более 5-10 минут
4. Парсер должен иметь свою админку с простым и понятным интерфейсом.
5. Парсер должен уметь забирать не только текст, но и файлы.
6. Если на сайте-доноре меняется структура документа, то перенастройка парсера должна занять пару минут.
7. Наш готовый парсер не должен требовать от программиста лазить в код.
8. Нам нужно вести лог того, что, когда и от куда мы забираем. Успешно ли мы завершили операцию.
9. Парсер должен понимать, что мы уже забирали эту информацию, а значит мы ее уже не забираем снова.
10. Мы можем парсить громадное количество источников, а значит нам нужна система импорта/экспорта настроек.

Я думаю, что для начала 10 пунктов будет достаточно. Хотя, на самом деле, их будет определенно больше.

По теории, думаю, хватит. Давайте перейдем к практике. Я покажу, как я решал данную задачу. Поскольку данный пост рассчитан на подготовленных и зрелых разработчиков, то я пройдусь по концепции и основным моментам реализации. Разжевывать код я не буду.

Давайте начнем с интерфейсов. Так как я имею приличный опыт по созданию различных платежных шлюзов для порталов и интернет-магазинов, и под эти нужды уже разрабатывал админку по управлению платежами, то я решил не изобретать велосипед, и взять за основу подобный интерфейс.

Как видно на изображении (я замазал сайты-доноры), у меня настроено 30 разных сайтов-доноров откуда я забираю различную информацию. В моем конкретном случае, я это делаю для развлекательного сайта. О нем я скажу в конце.

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

После заполнения данной формы мы увидим парсер в левой колонке (первый рисунок). Для удобства я подсвечиваю активирован ли парсер, и нужно ли постить данные после получения данных в фэйсбук. Так я с первого взгляда понимаю, что будет происходить при парсинге.

Также есть две кнопки вверху страницы: Все активировать и Все деактивировать. Когда у вас один-два парсера, то они особо не нужны. А когда их у вас десятки, как у меня, то без них вам придется вручную заходить в каждый парсер и менять его статус. Это уйма времени и тонна раздражения.

Справа (первый рисунок) вы видите уже сами настройки для парсера. При клике на конкретный парсер вы увидите его настройки. Это уже отдельная таблица с конкретными настройками (связана с таблицей парсера один к одному) того, что именно и откуда нужно спарсить. Для разных задач/проектов поля, которые вам нужно спарсить могут быть разными.

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

Вот так выглядит форма настроек:

Как вы видите на изображении, каждое поле я получаю путем обращения к конкретному DOM элемента. Это очень просто и быстро настраивается и изменяется. Лазить каждый раз в код парсера мне не нужно.

Также к таблице парсера подвязана таблица с логами. Для каждого срабатывания парсера создается своя лог запись, где указывается дата парсинга, и что парсилось. При следующем срабатывании парсера алгоритм заглядывает в лог файл, чтобы не получать тот контент, который уже мы один раз забрали.

Система импорта/экспорта сделана через JSON массив:

Нажали кнопку экспорта, всплыло диалоговое окно с настройками. Скопировали их. На другом проекте нажали кнопку импорта и вставили эти настройки в такое же диалоговое окно. Все, настройки будут импортированы.

Зачем это вообще нужно. Главная задача - это на тестовой машине отладить все настройки парсера, чтобы он правильно забирал данные без ошибок. И когда вы все настроите и убедитесь в том, что все работает как надо, вам эти настройки теперь нужно перенести на продакшн. Думаю, дальше уже пояснять не нужно :)

Теперь пару слов о самой логике парсера.  Предположим, что вам нужно забрать десять новостей с какого-то сайта. Как вы будете это делать? Самое простое и правильное - это заходить парсером на некую общую страницу, где содержится список новостей. Внимание! Не сама новость, а именно список новостей. Это может быть главная страница ил конкретный раздел новостей.

В настройках парсера у вас указана логика поиска таких ссылок на странице донора. Итак, парсер заходит на страницу раздела и находит все ссылки, которые введут на новости (конкретная новость с текстом, изображениями и т.д.). Потом он начинает переходить по этим ссылкам и забирать контент, согласно настройкам парсера.

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

Вот наконец мы подошли и к самому коду. Я более чем уверен, что вы без проблем можете создать все миграции и модели самостоятельно.

Для начала нам нужно создать отдельную и абстрактную бизнес логику самого парсера. Я привык создавать в App папочку Modules и хранить там отдельные классы для независимых решений.

В самом Ларавел последних версий встроен отличнейший инструмент для парсинга - это компонент от Symfony DomCrawler.
Именно благодаря этому компоненту ваша жизнь может очень сильно упроститься. Почитать подробнее можно здесь: https://symfony.com/doc/current/components/dom_crawler.html

Его нужно импортировать в ваш класс парсера:
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;
}
В этом методе не хватает еще множества всяких проверок на существование нужных полей, изображений, на тип кодировки и так далее. Здесь только показан общий принцип того, на сколько просто можно получить данные благодаря Crawler.

Укажу о паре граблей, на которые я наступил. Инициализировать краулер можно и так:
    // Create new instance for parser.
$crawler = new Crawler($html);
Но если делать так, то вы получите ошибки путей при парсинге изображений. Поэтому, нужно обязательно передать в краулер линк (ссылка для парсинга), а потом и страницу:
    // Create new instance for parser.
$crawler = new Crawler(null, $link);
$crawler->addHtmlContent($html, 'UTF-8');

При создании роута обязательно делайте параметр 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 о котором я уже писал ранее (уже есть версия под Ларавел 5.5):
/**
* 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

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

Как обещал в начале, даю ссылку на живой проект, где крутится данный парсер: развлекательный сайт YouSuper.org
Парсер срабатывает по крону через каждые пол часа в дневное время. Все работает четко, как часы. После сохрания постов на сайте они сразу же постятся на страницу фэйсбука: https://www.facebook.com/YouSuper.org/

Как видите, ничего сложного нет. Сам файл парсера занял 300 строк кода, а контроллер - 200 строк. Да, еще момент. Для того, чтобы работать с категориями, вам придется сделать массив со словарем, по которому вы сможете получить идентификатор вашей категории (ключ массива), в зависимости от полученного из парсера названия категории (значения массива), например так:
/**
* @var array Rubrics dictionary.
*/
public $rubrics = [
1 => ['Авто', 'Авто/Мото', 'Авторевю'],
2 => ['Анекдоты', 'Смешное'],
3 => ['Баяны', 'Реклама']
];

Теперь такое решения задачи парсера я могу назвать смело - Парсер с большой буквы. Его есть куда дорабатывать и усложнять.  Всем спасибо за внимание, как всегда вопросы и отзывы оставляйте в комментариях.
5

Комментарии (16):

Andrij писал(а):
Здравствуйте. Спасибо за ответ на предыдущий коммент. Назрел еще такой вопрос, возможно сталкивались. Если страница после прокрутки до конца, подгружает дополнительный контент, можно ли как-то симулировать на crawler эту прокрутку или ajax запрос, чтобы контент который после ajax запроса появляется, тоже спарсить. Спасибо.
Дело в том, что все JS обрабатываются на стороне клиента. Поэтому стандартным способом подгружаемый контент спарсить не получится. Именно по этой причине у одностраничных сайтов (SPA) проблемы с индексацией поисковиками. Что можно сделать с этим. Можно зайти на сайт и в панели разработчика посмотреть, какой запрос убегает на сервер для получения порции свежих данных. И уже после понимания этого момента делать парсинг не фронт стороны сайта, а именно сервера, скармливая ему запросы, как вроде отрабатывает прокрутка на фронте. Но это в том случае, если запросы отдаются GET запросом.
Здравствуйте. Спасибо за ответ на предыдущий коммент. Назрел еще такой вопрос, возможно сталкивались. Если страница после прокрутки до конца, подгружает дополнительный контент, можно ли как-то симулировать на crawler эту прокрутку или ajax запрос, чтобы контент который после ajax запроса появляется, тоже спарсить. Спасибо.
Andrij писал(а):
Скажите, а используете ли вы proxy с этой библиотекой парсинга, если да то может уточните как именно?Спасибо.
Здравствуйте. Прокси не использовал. Все запросы напрямую с сервера происходили. Чтобы не попасть в бан, нужно соблюдать тайминги при парсинге. Как заставить работать скрипты через прокси, то не изучал этот вопрос. Скорее всего, можно как-то через curl настроить этот процесс. Но придется приличную обертку писать, для постоянной смены проксиков, плюс проверять пинги, чтобы прокси не тормозили.
Скажите, а используете ли вы proxy с этой библиотекой парсинга, если да то может уточните как именно?Спасибо.
Привет!
сможешь такой проект сделать bankrot - spy.ru (убрать пробелы вокруг дефиса)?
сколько будет стоить? пиши в телегу mr _ petr0vi4  (убрать пробелы вокруг подчеркивания)
mirilis писал(а):
Доброй ночи, как с вами связаться? очень нужен парсер на сайт с комиксами :(( в ручную уже лям картинок залил.... я не смог найти ваших контактов на сайте, пожалуйста напишите мне в скайп хотя бы если не сложно alexmaster9111 очень нужна ваша помощь в написании простенького парсера на на сайт что бы сканил пару тройку сайтов :)
Здравствуйте. Внизу сайта, в нижнем меню, есть форма контактов. Через нее можно со мной связаться.
Доброй ночи, как с вами связаться? очень нужен парсер на сайт с комиксами :(( в ручную уже лям картинок залил.... я не смог найти ваших контактов на сайте, пожалуйста напишите мне в скайп хотя бы если не сложно alexmaster9111 
очень нужна ваша помощь в написании простенького парсера на на сайт что бы сканил пару тройку сайтов :)
Ой, вот это очень ценная статья с действительно интересным подходом к решению. Благодарю :) 
Да, кстати, для интересующихся моей реализацией. За точку входа я всегда брал страницу, где появляются свежие посты. Это может быть блок, а может быть страница. Так вот, через какое-то время на этой странице должны появляться новые посты. Парсер у меня настроен был (можно самому настроить) так, чтобы не заходить подряд на тот же сайт. То есть, если у меня для парсинга было указано 30 сайтов, а сегодня я парсил первый, то возвращался к первому я только тогда, когда пропарщу остальные 29. Плюс я веду лог спарщенных данных, самое простое - это проверка по url поста. Если я уже забирал от туда данные, то просто пропускаю такой урл.

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

Может, если я найду время, то выложу исходники и подробно расскажу, как это все работает. Кстати, у меня разработка идет на локальном компе, а парсинг на боевом сервере. И когда я для проверки набивал десятки сайтов с настройками на тестовом компе, то переносить это руками на боевой - еще тот адов трэш. Поэтому я дополнительно написал систему экспорта импорта данных в один клик.
darkkkk писал(а):
Ответ на комментарий Как вы получаете идентификатор поста? В примере вижу использование палочек | что-то вроде a|p?= или a|archives/{d}. Это фишка краулера или ваша реализация? Если ваша реализация, то не могли бы вы подсказать, как сделать также?
Я использовал свои модификаторы. Писал функционал давно, уже точно не помню, что и как я делал. Но глянул код, все достаточно просто. Вот мой метод, который получает ссылки для парсинга и считывает модификаторы:
/**
* Get links for posts.
*
* @param $parser object collection with parsing settings
* @return array with links
*/
public function getLinks($parser)
{
// Create empty array for links.
$links = [];

// Get html remote text.
$html = file_get_contents($parser->parsing_site);

// Create new instance for parser.
$crawler = new Crawler($html, $parser->parsing_site);

// Check exist additional condition.
if (strpos($parser->settings->link, '|')) {
// Get link conditions.
list($linkCondition, $stringInUrl) = explode('|', $parser->settings->link);
} else {
$linkCondition = $parser->settings->link;
$stringInUrl = '';
$condition = false;
}


// If exist string URL condition.
if (!empty($stringInUrl)) {

// If exist only numbers condition.
if (strpos($stringInUrl, '{d}')) {
$stringInUrl = str_replace('{d}', '', $stringInUrl);
$condition = true;
}
}

// Get links from mail link page.
$crawler->filter($linkCondition)->each(function (Crawler $node, $i) use (&$links, $parser, $stringInUrl, $condition) {
$link = $node->link()->getUri();

// If no condition for exist string in URL.
if (empty($stringInUrl) && $this->checkLink($link)) {
$links[$link] = $link;
}

// If exist condition for exist string in URL.
if (!empty($stringInUrl) && strpos($link, $stringInUrl) !== false) {
// If not number condition.
if (!$condition && $this->checkLink($link)) {
$links[$link] = $link;
}

// If exist number condition.
if ($condition) {
// Get data after condition string.
$linkData = explode('/', substr($link, strpos($link, $stringInUrl), strlen($link)));

if (is_numeric($linkData[1]) && $this->checkLink($link)) {
$links[$link] = $link;
}
}
}
});

// Check count entries.
if (count($links) > $parser->settings->count) {
$i = 1;
// Delete part entries.
foreach ($links as $key => $value) {
if ($i > $parser->settings->count) {
unset($links[$key]);
}
$i++;
}
}

return $links;
}
Как вы получаете идентификатор поста? В примере вижу использование палочек | что-то вроде a|p?= или a|archives/{d}. Это фишка краулера или ваша реализация? Если ваша реализация, то не могли бы вы подсказать, как сделать также?
Den писал(а):
Сталкивались с этим?
В моем конкретном случае, я использую
// Upload and save image.
$image = UploadImage::upload($file, $contentType, true)->getImageName();
Метод сам проверит существование изображения. И если оно есть, то сохранит. Если абстрагироваться, то просто получаете путь к изображению и проверяете на существование, например так:
// url файла для проверки на существование
$url = "http://site.com/image.jpg";
// открываем файл для чтения
if (fopen($url, "r")) {
    echo "Файл существует";
} else {
    echo "Файл не найден";
}
Здравствуйте,ответ по шаблону оч помог. Но появилась другая проблема. Как вы проверяете, существование нужных полей(картинок)? Такой вариант не работает: if(isset($crawler->filter($query->screenshots)->image())){ заношу в базу}else{ пропускаю}, но до else код не доходит - выдает ошибку  The current node list is empty. IF( $crawler->filter($query->screenshots)->image() !==null(false))- то же самое и так тоже не могу убрать ошибку if( !null==($crawler->filter($query->screenshots)->image()->getUri() )). Сталкивались с этим?

Den писал(а):
Привет, классная штука! Все работает! Но оформить парсер как у вас не получилось.Почему-то при добавлении второй формы настроек(кнопки) в одну вьюшку фреймворк выдает ошибку. У вас каждая кнопка работает через ajax?Можете подробней рассказать про реализацию шаблона для этого парсера?
Привет. Шаблон простой. В цикле перебираем коллекцию парсеров, а кнопки формируем через роуты:
<div class="row margin-top-20 margin-bottom-20">
<div class="col-lg-3 text-left">
<a href="{!! route('admin.parser.settings.edit', $parser->id) !!}" class="btn btn-primary form-horizontal">
Изменить настройки</a>
</div>

<div class="col-lg-2">
<a href="{!! route('admin.parser.edit', $parser->id) !!}" class="btn btn-primary form-horizontal">
Изменить парсер</a>
</div>

<div class="col-lg-2">
<a href="{!! route('admin.parser.log', $parser->id) !!}" class="btn btn-primary form-horizontal">
Смотреть логи</a>
</div>

<div class="col-lg-2">
{!! Form::open(['route' => 'admin.parser.copy']) !!}
{!! Form::hidden('id', $parser->id) !!}
{!! Form::submit('Копировать', ['class' => 'btn btn-primary form-horizontal']) !!}
{!! Form::close() !!}
</div>

<div class="col-lg-3 text-right">
{!! Form::open([
'method' => 'DELETE',
'route' => ['admin.parser.destroy', $parser->id],
'style' => 'display: inline-block;',
'onsubmit'=>"return confirm('" . trans('interface.delete?') . "')"
]) !!}

<button type="submit" id="delete-parser-{{ $parser->id }}" class="btn btn-danger"
title="{{ trans('interface.Delete') }}">
<i class="demo-icon icon-trash"></i> Удалить парсер
</button>

{!! Form::close() !!}
</div>
</div>
Это кусок шаблона с кнопками, которые находятся в цикле. Как видите, пути будут автоматически созданы для кнопок и ссылок. Конфликтов при этом не будет.

А систему отображения вкладок я сделал через JS. В шаблоне формируются все вкладки, но отображаются настройки только для первой, по умолчанию. У остальных display:none. При клике на другую вкладку, я все настройки скрываю, а для этой вкладки отображаю. Все, в принципе, просто. Можно заморочиться и с аяксом, но особого смысла в этом нет.

Привет, классная штука! Все работает! Но оформить парсер как у вас не получилось.Почему-то  при добавлении второй формы настроек(кнопки) в одну вьюшку фреймворк выдает ошибку. У вас каждая кнопка работает через ajax?Можете подробней рассказать про реализацию шаблона для этого парсера?