Пишем парсер контента php. Парсинг HTML и скрепинг с помощью простой библиотеки HTML DOM

10
Сен

Как написать парсер для сайта

Недавно мне была поставлена задача написать php парсер сайта. Поставленная задача была выполнена и благодаря ей появилась эта заметка. До этого я ничего подобного не делал, так что не судите строго. Это мой первый парсер php.

И так с чего начать решение вопроса "как написать парсер". Давайте для начала разберёмся что это такое. В простонародье парсер(parser) или синтаксический анализатор - это программа которая получает данные (например веб страница), как то их анализирует структурирует, делает выборку и потом проводит какие то операции с ними (пишем данные в файл, в БД или выводим на экран). Данную задачу нам нужно выполнить в рамках веб программирования.

Для заметки я придумал такую тестовую задачу. Нужно спарсить по определённому поисковому запросу ссылки на сайты с 5 первых страниц выдачи и вывести их на экран. Парсить я решил выдачу поисковой системы bing. А почему бы не написать парсер яндекса или гугла спросите вы. Такие матёрые поисковики имеют не хилую защиту от парсинго(капча, бан ip, меняющаяся разметка, куки и тд), и это тема отдельной статьи. В этом плане с бингом таких проблемм нет. И так что нам нужно будет сделать:

  • Получить (спарсить) контент html страницы средствами php
  • Получить интересующие нас данные(а конкретно ссылки)
  • Спарсить постраничную навигацию и получить ссылку на следующую страницу
  • Опять спарсить страницу по ссылке, получить данные, получить следующую ссылку
  • Проделать выше описанную операцию N количество раз
  • Вывести все полученные ссылки на экран
Получение и парсинг страницы

Сначала напишем функцию, потом её разберём

Function getBingLink($link){ $url="https://www.bing.com/search"; //получаем контент сайта $content= file_get_contents($url.$link); //убираем вывод ошибок libxml_use_internal_errors(true); //получаем объект класса DOMDocument $mydom = new DOMDocument(); //задаём настройки $mydom->preserveWhiteSpace = false; $mydom->resolveExternals = false; $mydom->validateOnParse = false; //разбираем HTML $mydom->loadHTML($content); //получаем объект класса DOMXpath $xpath = new DOMXpath($mydom); //делаем выборку с помощью xpath $items=$xpath->query("//*[@class="b_algo"]/h2/a"); //выводим в цикле полученные ссылк static $a=1; foreach ($items as $item){ $link=$item->
"; $a++; } }

И так разберём функцию. Для получения контента сайта используем php функцию file_get_contents($url.$link) . В неё подставляем адрес запроса. Есть ещё много методов получения контента html страницы, например cUrl, но на мой взгляд file_get_contents самый простой. Потом вызываем объект DOMDocument и так далее. Это всё стандартно и об этом можно почитать в интернете поподробнее. Хочу заакцентировать внимание на методе выборки нужных нам элементов. Для этой цели я использую xpath. Мою xpath шпаргалку можно глянуть . Есть и другие методы выборки такие как регулярные выражения, Simple HTML DOM, phpQuery. Но на мой взгляд лучше разобраться с xpath, это даст дополнительные возможности при работе с xml документами, синтаксис полегче чем с регулярными выражениями, в отличии от css селекторов можно найти элемент по находящемуся в нём тексту. Для примера прокомментирую выражение //*[@class="b_algo"]/h2/a . Подробнее синтаксис можно посмотреть в моей шпаргалке xpath. Мы выбираем со всей страницы ссылки лежащие в теге h2 в диве с классом b_algo . Сделав выборку мы получим массив и которого в цикле выведем на экран все полученные ссылки.

Парсинг постраничной навигации и получение ссылки на следующую страницу

Напишем новую функцию и по традиции разберём её позже

Function getNextLink($link){ $url="https://www.bing.com/search"; $content= file_get_contents($url.$link); libxml_use_internal_errors(true); $mydom = new DOMDocument(); $mydom->preserveWhiteSpace = false; $mydom->resolveExternals = false; $mydom->validateOnParse = false; $mydom->loadHTML($content); $xpath = new DOMXpath($mydom); $page = $xpath->query("//*[@class="sb_pagS"]/../following::li/a"); foreach ($page as $p){ $nextlink=$p->getAttribute("href"); } return $nextlink; }

Почти идентичная функция, изменился только xpath запрос. //*[@class="sb_pagS"]/../following::li/a получаем элемент с классом sb_pagS (это класс активной кнопки постраничной навигации), поднимаемся на элемент вверх по dom дереву, получаем первый соседний элемент li и получаем в нём ссылку. Эта и есть ссылка на следующую страницу.

Парсим выдачу N количество раз

Пишем функцию

Function getFullList($link){ static $j=1; getBingLink($link); $nlink=getNextLink($link); if($j

Данная функция вызывает getBingLink($link) и getNextLink($link) пока не кончится счётчик j. Функция рекурсивная, то есть вызывает сама себя. Про рекурсию почитайте подробнее в интернете. Обратите внимание что $j статическая, то есть она не удаляется при следующем вызове функции. Если бы это было не так, то рекурсия бы была бесконечной. Ещё добавлю из опыта, если хотите пройти всю постраничную навигацию то пишите if условие пока есть переменная $nlink. Есть ещё пара подводных камней. Если парсер работает долго то это может вызвать ошибку из за времени выполнения скрипта. По умолчанию 30с. Для увеличения времени в начале файла ставте ini_set("max_execution_time", "480"); и задавайте нужное значение. Так же может возникать ошибка из за большого количества вызовов одной функции (более 100 раз). Фиксится отключением ошибки, ставим в начало скрипта ini_set("xdebug.max_nesting_level", 0);

Теперь нам осталось написать html форму для ввода запроса и собрать парсер воедино. Смотрите листинг ниже.

Команда Игры Победы Ничьи Проигрыши Мячи Очки
even odd

Теперь напишем код, чтобы вывести готовую турнирную таблицу.

$results = parse_site(array("zona_vostok" => array("url" => "http://www.championat.com/football/_russia2d/589/table/all.html", "xpath" => "xpath" => "//div[@id="section-statistics"]/table", "xsl" => __DIR__."/football.xsl")); print $results["zona_vostok"];

И на выходе получим вот такой код HTML:

Команда Игры Победы Ничьи Проигрыши Мячи Очки ...
1 Луч-Энергия 20 12 6 2 30-17 42
2 Чита 20 12 5 3 28-14 41

Скачать «Микропарсер»

Вот несколько способов заполучить «Микропарсер»:

  • Форкните на Гитхабе: git clone https://github.com/franzose/microparser.git
  • Скачайте архив:
  • Вы узнаете, как получить список всех статей, опубликованных на сайте.

    Шаг 1. Подготовка

    В первую очередь нужно скопировать библиотеку simpleHTMLdom , которая доступна на сайте

    В архиве для загрузки хранятся несколько файлов, но вам нужен только один simple_html_dom.php . Все остальные файлы - это примеры и документация.

    Шаг 2. Основы парсинга

    Данную библиотеку очень просто использовать, но есть несколько основных моментов, которые следует изучить до того, как вы начнете приводить ее в действие.

    $html = new simple_html_dom();
    // Загрузка из строки
    $html->load("

    Hello World!

    ");
    // Загрузка файла
    $html->load_file("http://net.tutsplus.com/");

    Вы можете создать исходный объект загрузив HTML либо из строки, либо из файла. Загрузка из файла может быть выполнена либо через указание URL , либо из вашей локальной файловой системы.

    Примечания: Метод load_file() делегирует работу функции PHP file_get_contents . Если allow_url_fopen не установлен в значение true в вашем файле php.ini , то может отсутствовать возможность открывать удаленные файлы таким образом. В этом случае вы можете вернуться к использованию библиотеки CURL для загрузки удаленных страниц, а затем прочитать с помощью метода load() .

    Доступ к информации

    Как только у вас будет объект DOM, вы сможете начать работать с ним, используя метод find() и создавая коллекции. Коллекция - это группа объектов, найденных по селектору. Синтаксис очень похож на jQuery.



    Hello World!


    We"re Here.




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

    1. # создаем и загружаем HTML
    2. include("simple_html_dom.php");
    3. $html = new simple_html_dom();
    4. $html->load(“

    Hello World!

    “);
    5. # получаем элемент представляющий второй параграф
    6. $element = $html->find(“p“);
    7. # модифицируем его
    8. $element->innertext .= “ and we"re here to stay.“;
    9. # Выводим!
    10. echo $html->save();

    Строки 2-4 : Загружаем HTML из строки, как объяснялось выше.

    Строка 6 : Находим все тэги

    В HTML, и возвращаем их в массив. Первый параграф будет иметь индекс 0, а последующие параграфы индексируются соответственно.

    Строка 8 : Получаем доступ ко второму элементу в нашей коллекции параграфов (индекс 1), добавляем текст к его атрибуту innertext. Атрибут innertext представляет содержимое между тэгами, а атрибут outertext представляет содержимое включая тэги. Мы можем заменить тэг полностью, используя атрибут outertext.

    Теперь добавим одну строку и модифицируем класс тэга нашего второго параграфа.

    $element->class = "class_name";
    echo $html->save();

    Окончательный вид HTML после команды save будет иметь вид:



    Hello World!


    We"re here and we"re here to stay.



    Другие селекторы

    Несколько других примеров селекторов. Если вы использовали jQuery, все покажется вам знакомым.

    # получаем первый найденный элемент с id=“foo“
    $single = $html->find("#foo", 0);
    # получаем все элементы с классом “foo“
    $collection = $html->find(".foo");
    # получаем все теги ссылок на странице
    $collection = $html->find("a");
    # получаем все теги ссылок, которые расположены внутри тега H1
    $collection = $html->find("h1 a");
    # получаем все теги img с title="himom"
    $collection = $html->find("img");

    Первый пример требует пояснений. Все запросы по умолчанию возвращают коллекции, даже запрос с ID, который должен вернуть только один элемент. Однако, задавая второй параметр, мы говорим “вернуть только первый элемент из коллекции”.

    Это означает, что $single - единичный элемент, а не не массив элементов с одним членом.

    Остальные примеры достаточно очевидны.

    Документация

    Полная документация по библиотеке доступна на .

    Шаг 3. Пример из реального мира

    Для демонстрации библиотеки в действии мы напишем скрипт для скрепинга содержимого сайта net.tutsplus.com и формирования списка заголовков и описания статей, представленных на сайте….только в качестве примера. Скрепинг относится к области трюков в веб, и не должен использоваться без разрешения владельца ресурса.

    Include("simple_html_dom.php");
    $articles = array();
    getArticles("http://net.tutsplus.com/page/76/");

    Начнем с подключения библиотеки и вызова функции getArticles с указанием страницы, с которой мы хотим начать парсинг.

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



    ...

    Title

    ...

    Description



    Так представлен основой формат поста на сайте, включая комментарии исходного кода. Почему важны комментарии? Они подсчитываются парсером как узлы.

    Шаг 4. Начало функции парсинга function getArticles($page) {
    global $articles;
    $html = new simple_html_dom();
    $html->load_file($page);
    // ... Дальше...
    }

    Начинаем с объявления глобального массива, создаем новый объект simple_html_dom , и затем загружаем страницу для парсинга. Данная функция будет рекурсивно вызываться, поэтому устанавливаем для нее в качестве параметра URL страницы.

    Шаг 5. Находим ту информацию, которая нам нужна

    1. $items = $html->find("div");
    2. foreach($items as $post) {
    3. # помним про учет комментариев в качестве узлов
    4. $articles = array($post->children(3)->outertext,
    5. $post->children(6)->first_child()->outertext);
    6. }

    Это суть функции getArticles . Нужно разобраться более детально, чтобы понять, что происходит.

    Строка 1 : Создаем массив элементов - тег div с классом preview. Теперь у нас есть коллекция статей, сохраненная в $items .

    Строка 4 : $post теперь ссылается на единичный div класса preview. Если мы взглянем в оригинальный HTML, то увидим, что третий элемент потомок - это тег H1 , который содержит заголовок статьи. Мы берем его и присваиваем $articles .

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

    Строка 5 : Шестой потомок $post - это . Нам нужен текст описания из него, поэтому мы используем outertext - в описание будет включен тег параграфа. Единичная запись в массиве статей будет выглядеть примерно так:

    $articles = “Заголовок статьи“;
    $articles = “This is my article description“

    Шаг 6, Работа со страницами

    первым делом нужно определить, как найти следующую страницу. На сайте Nettuts+ о номере страницы очень легко догадаться по URL, но нам нужно получать ссылку в парсинге.

    Если посмотреть на HTML, то можно найти следующее:

    »

    Это сслыка на следующую страницу, и мы можем легко ее найти по классу ‘nextpostslink ’. Теперь эта информация может быть использована.

    If($next = $html->find("a", 0)) {
    $URL = $next->href;
    $html->clear();
    unset($html);
    getArticles($URL);
    }

    В первой строке мы проверяем, можно ли найти ссылку с классом nextpostslink . Отметим использование второго параметра в функции find() . Таким образом мы указываем, что хотим получить первый элемент (индекс 0) в возвращаемой коллекции. $next содержит единичный элемент, а не коллекцию.

    Затем мы присваиваем ссылку HREF переменной $URL. Это важно, потому, что далее мы удаляем объект HTML. Чтобы предотвратить утечку памяти в php5, текущий объект simple_html_dom должен быть очищен и разустановлен, прежде чем другой объект будет создан. Если этого не сделать, то вся доступная память может быть поглощена.

    В завершение, мы вызываем функцию getArticles с URL следующей страницы. Рекурсия прерывается, когда не остается страниц для парсинга.

    Шаг 7. Вывод результатов

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

    #main {
    margin:80px auto;
    width:500px;
    }
    h1 {
    font:bold 40px/38px helvetica, verdana, sans-serif;
    margin:0;
    }
    h1 a {
    color:#600;
    text-decoration:none;
    }
    p {
    background: #ECECEC;
    font:10px/14px verdana, sans-serif;
    margin:8px 0 15px;
    border: 1px #CCC solid;
    padding: 15px;
    }
    .item {
    padding:10px;
    }

    Затем мы пишем маленькую функцию на PHP в странице для вывода предварительно сохраненной информации.


    Окончательный результат - это одна страница HTML со списком всех статей со страниц Nettuts+, начиная с той, которая была указана в первом вызове getArticles() .

    Шаг 8. Заключение

    Если Вы запускаете парсинг для большого количества страниц (скажем, весь сайт), то это может занять много времени. На таком сайте как Nettuts+, который имеет боле 86страниц, процесс парсинга может длиться более минуты.

    Данный урок открывает для вас тему парсинга HTML. Существуют другие методы методы работы с DOM, которые позволяют работать с селектором xpath для поиска элементов. Описанная в данном уроке библиотека проста для использования и отлично подходит для быстрого старта. Помните, что нужно спрашивать разрешения, прежде проводить скрепинг сайта.

    Для того, чтобы спарсить страницу сайта (то есть разобрать ее HTML код), ее для начала следует получить. А затем уже полученный код можно разобрать с помощью регулярных выражений и, либо каким-то образом его проанализировать, либо сохранить в базу данных, либо и то, и другое.

    Получение страниц сайтов с помощью file_get_contents

    Итак, для начала давайте поучимся получать страницы сайтов в переменную PHP. Это делается с помощью функции file_get_contents , которая чаще всего используется для получения данных из файла, однако, может быть использована для получения страницы сайта - если передать ей параметром не путь к файлу, а url страницы сайта.

    Учтите, что эта функция не идеальна и существует более мощный аналог - библиотека CURL , которая позволяет работать с куками, с заголовками, позволяет отправлять формы и переходить по редиректам. Все это file_get_contents делать не умеет, однако для начала нам сойдет и она, а работу с CURL мы разберем в следующем уроке.

    Итак, давайте для примера получим главную страницу моего сайта и выведем ее на экран (сделайте это):

    Что вы получите в результате: у себя на экране вы увидите страницу моего сайта, однако, скорее всего без CSS стилей и картинок (будут ли работать CSS и картинки - зависит от сайта, почему так - разберем попозже).

    Давайте теперь выведем не страницу сайта, а ее исходный код. Запишем его в переменную $str и выведем на экран с помощью var_dump :

    Учтите, что var_dump должен быть настроен корректно в конфигурации PHP (см. предыдущий урок для этого). Корректно - это значит вы должны видеть теги и не должно быть ограничения на длину строки (код страницы сайта может быть очень большим и желательно видеть его весь).

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

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

    Должна быть включена директива allow_url_fopen http://php.net/manual/ru/filesystem.configuration.php#ini.allow-url-fopen

    Парсинг с помощью регулярных выражений

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

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

    Подводные камни

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

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

    Какие еще подводные камни вас ждут - будем разбирать постепенно в течении данного урока.

    Попробуем разобрать теги

    Пусть мы каким-то образом (например, через file_get_contents ) получили HTML код сайта. Вот он:

    Это заголовок тайтл Это основное содержимое страницы.

    Давайте займемся его разбором. Для начала давайте получим содержимое тега , тега , и тега .

    Итак, получим содержимое тега (в переменной $str хранится HTML код, который мы разбираем):

    Содержимое :

    Содержимое :

    В общем-то ничего сложного нет, только обратите внимание на то, что как уголки тегов, так и слеш от закрывающего тега экранировать не надо (последнее верно, если ограничителем регулярки является не слеш /, а, например, решетка #, как у нас сейчас).

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

    Что же у нас не так? На самом деле тег - такой же тег, как и остальные и в нем вполне могут быть атрибуты. Чаще всего это атрибут class , но могут быть и другие (например, onload для выполнения JavaScript).

    Итак, перепишем регулярку с учетом атрибутов:

    Но и здесь мы ошиблись, при чем ошибок несколько. Первая - следует ставить не плюс + , а звездочку * , так как плюс предполагает наличия хотя бы одного символа - но ведь атрибутов в теге может и не быть - и в этом случае между названием тега body и уголком не будет никаких символов - и наша регулярка спасует (не понятно, что я тут написал - учите регулярки).

    Поправим эту проблему и вернемся к дальнейшему обсуждению:

    Вторая проблема следующая: если внутри будут другие теги (а так оно и будет в реальной жизни) - то наша регулярка зацепит лишнего. Например, рассмотрим такой код:

    Это заголовок тайтл

    Регулярка найдет не , как ожидалось, а

    Абзац{

    } - потому что мы не ограничили ей жадность. Сделаем это: место напишем - в этом случае будет все хорошо.

    Но более хорошим вариантом будет написать вместо точки конструкцию [^>] (не закрывающий уголок ), вот так - ]*?> - в этом случае мы полностью застрахуем себя от проблем такого рода, так как регулярка никогда не сможет выйти за тег.

    Получение блока по id

    Давайте рассмотрим следующий код:

    Это заголовок тайтл Контент Еще див

    Напишем регулярку, которая получит содержимое блока с id, равным content .

    Итак, попытка номер один (не совсем корректная):

    #(.+?)#su

    Что здесь не так? Проблема с пробелами - ведь между названием тега и атрибутом может быть сколько угодно пробелов, так же, как и вокруг равно в атрибутах.

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

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

    Давайте поправим нашу регулярку:

    #(.+?)#su

    Обратите внимание на то, что вокруг равно пробелы могут быть, а могут и не быть, поэтому там стоит оператор повторения звездочка * .

    Кроме того, перед закрывающем уголком тега тоже могут быть пробелы (а могут и не быть) - учтем и это:

    #(.+?)#su

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

    #(.+?)#su

    Обратите внимание, что после стоит регулярка .*? - это не ошибка, так и задумано, ведь после может вообще не быть других атрибутов (кроме нашего id) и пробела тоже может не быть.

    Регулярка стала еще более хорошей, но есть проблема: лучше не использовать точку в блоках типа .*? - мы вполне можем хватануть лишнего выйдя за наш тег (помните пример выше с body?). Лучше все-таки использовать [^>] - это гарантия безопасности:

    #]+? id\s*?=\s*?"content" [^>]*? >(.+?)#su

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

    #]+?id\s*?=\s*? ["\"] content ["\"] [^>]*?>(.+?)#su

    Обратите внимание на то, что одинарная кавычка заэкранирована - мы это делаем, так как внешние кавычки от строки PHP у нас тоже одинарные, вот тут:

    В общем-то регулярка достаточно хороша, но иногда идут дальше и делают так, чтобы первая кавычка от тега совпадала со второй (исключаем вариант id="content"). В этом случае делают так - первая кавычка ложится в карман, а вторая кавычка указывается карманом, чтобы совпадала с первой:

    #]+?id\s*?=\s*? (["\"]) content \1 [^>]*?>(.+?)#su

    Для нашей задачи это особо не нужно (можно быть точно уверенным, что такое id="content" - врядли где-то будет), но есть атрибуты, где это существенно. Например, в таком случае: и даже

    Понравилась статья? Поделиться с друзьями: