Парсинг на php. Основы
В общем случае, парсер получает html-разметку, из которой он извлекает полезную информацию, путём доступных ему средств (DOM-парсинг, регулярные выражения, и т.п.). И, вот, эту полезную информацию, мы, на выходе, и получаем, в чистом виде. Как обрабатывать её, тоже решать нам.
И так, можно выделить 3 основных этапа парсинга:
Для того, чтобы получить разметку сайта, нужно сделать http-запрос. Вот для этого, в PHP и существует такая программа, как CURL. Это программа, которая делает запрос к указанному серверу, с указанными нами параметрами (тело запроса, заголовки, cookies, метод запроса, и т.д.). На самом деле, эта программа используется и для отправки API-запросов, однако, большее распространение она получила именно в написании ботов, и парсеров. Для запросов к API, обычно используют функцию file_get_contents, вместе с надстройкой для отправки кастомных запросов.
На практике, парсеры бывают очень полезными. Так, с помощью них можно скачать все картинки с сайта, все видео, или все статьи.
Часто новые интернет-магазины, для автоматического наполнения товарами, как раз и пользуются парсерами, которые автоматизируют работу копирования товаров, или автоматическое обновление цен, в зависимости от цен конкурентов.
При этом, парсеры могут содержать дополнительную логику по преобразованию (удаление фрагментов текста, редактирование картинок, увеличение цены на определённый процент).
Новостные сайты, агрегирующие новости одновременно с нескольких сайтов, аналогично, не могут обходиться без средств автоматизации.
Говоря о практическом применении, последним моим парсером был скрипт, копирующий видеоуроки с сайта, сохраняя каждый видеоурок в отдельную папку.
Ещё в начале пути я интересовался, как создать парсер на php. И, на удивление, это оказалось достаточно просто. Для того, чтобы понять, как парсеры работают внутри, прочитайте две вводных статьи:
Эта статья является исключительно теорией. Она только объясняет основные термины, которые нужны для понимания того, как работают парсеры. Потому, чтобы научиться писать хорошие парсеры, нужно писать много парсеров, и читать последующие статьи по теме =)
Subscribe to Блог php программиста: статьи по PHP, JavaScript, MySql
Get the latest posts delivered right to your inbox
Как я html-парсер на php писал, и что из этого вышло. Вводная часть
Сегодня я хочу рассказать, как написать html парсер, а также с какими проблемами я столкнулся, разрабатывая подобный парсер на php. А проблем было много. И в первой части я расскажу о проектировании парсера, и о возникших проблемах, ведь html парсер отличается от парсера привычных всем языков программирования.
Введение
Я старался написать текст этой статьи максимально понятно, чтобы любой, кто даже не знаком с общим устройством парсеров мог понять то, как работает html парсер.
Здесь и далее в статье я буду называть документ, содержащий html просто «Документ».
Dom дерево, находящееся в элементе, будет называться «Подмассив».
Что должен делать парсер?
Давайте сначала определимся, что должен делать парсер, чтобы в будущем отталкиваться от этого при разработке. А именно, парсер должен:
Впрочем, это мелочи. Основного функционала вполне хватит, чтобы поломать голову пару ночей напролет.
Но тут есть проблема, с которой я столкнулся сразу же: Html — это не просто язык, это язык гипертекста. У такого языка свой синтаксис, и обычный парсер не подойдет.
Разделяй и властвуй
Для начала, нужно разделить работу парсера на два этапа:
Для описания первого этапа я нарисовал схему, которая наглядно показывает, как обрабатываются данные на первом этапе:
Пишем парсер контента на PHP
Чтобы написать хороший и работоспособный скрипт для парсинга контента нужно потратить немало времени. А подходить к сайту-донору, в большинстве случаев, стоит индивидуально, так как есть масса нюансов, которые могут усложнить решение нашей задачи. Сегодня мы рассмотрим и реализуем скрипт парсера при помощи CURL, а для примера получим категории и товары одного из популярных магазинов.
Если вы попали на эту статью из поиска, то перед вами, наверняка, стоит конкретная задача и вы еще не задумывались над тем, для чего ещё вам может пригодится парсер. Поэтому, перед тем как вдаваться в теорию и непосредственно в код, предлагаю прочесть предыдущею статью – парсер новостей, где был рассмотрен один из простых вариантов, да и я буду периодически ссылаться на неё.
Работать мы будем с CURL, но для начала давайте разберёмся, что эта аббревиатура обозначает. CURL – это программа командной строки, позволяющая нам общаться с серверами используя для этого различные протоколы, в нашем случаи HTTP и HTTPS. Для работы с CURL в PHP есть библиотека libcurl, функции которой мы и будем использовать для отправки запросов и получения ответов от сервера.
Как можно увидеть из скриншота все категории находятся в ненумерованном списке, а подкатегории:
Внутри отельного элемента списка в таком же ненумерованном. Структура несложная, осталось только её получить. Товары мы возьмем из раздела «Все телефоны»:
На странице получается 24 товара, у каждого мы вытянем: картинку, название, ссылку на товар, характеристики и цену.
Пишем скрипт парсера
Если вы уже прочли предыдущею статью, то из неё можно было подчеркнуть, что процесс и скрипт парсинга сайта состоит из двух частей:
Основной метод, который у нас будет – это getPage() и у него всего один обязательный параметр URL страницы, которой мы будем парсить. Что ещё будет уметь наш замечательный метод, и какие значения мы будем обрабатывать в нем:
Конечно, обрабатываемых значений много и не всё мы будем использовать для нашей сегодняшней задачи, но разобрать их стоит, так как при парсинге больше одной страницы многое выше описанное пригодится. И так добавим их в наш скрипт:
Как видите, у всех параметров есть значения по умолчанию. Двигаемся дальше и следующей строчкой напишем кусок кода, который будет очищать файл с куками при запросе:
Так мы обезопасим себя от ситуации, когда по какой-либо причине не создался файл.
Для работы с CURL нам необходимо вначале инициализировать сеанс, а по завершению работы его закрыть, также при работе важно учесть возможные ошибки, которые наверняка появятся, а при успешном получении ответа вернуть результат, сделаем мы это таким образам:
После того, как мы инициализировали соединения через функцию curl_setopt(), установим несколько параметров для текущего сеанса:
Кстати очень удобный сервис, позволяющий отладить обращения к серверу. Так как, например, для того что бы узнать свой IP или заголовки отправляемые через CURL, нам бы пришлось бы писать костыль.
Если вывести на экран, то у вас должна быть похожая картина:
Если произойдет ошибка, то результат будет выглядеть так:
При успешном запросе мы получаем заполненную ячейку массива data с контентом и информацией о запросе, при ошибке заполняется ячейка error. Из первого скриншота вы могли заметить первую неприятность, о которой я выше писал контент сохранился не в переменную, а отрисовался на странице. Чтобы решить это, нам нужно добавить ещё один параметр сеанса CURLOPT_RETURNTRANSFER.
Обращаясь к страницам, мы можем обнаружить, что они осуществляют редирект на другие, чтобы получить конечный результат добавляем:
Теперь можно увидеть более приятную картину:
Для того, чтобы получить заголовки ответа, нам потребуется добавить следующий код:
Мы отключили вывод тела документа и включили вывод шапки в результате:
Для работы со ссылками с SSL сертификатом, добавляем:
Предлагаю проверить, а для этого я попробую вытянуть куки со своего сайта:
Всё получилось, двигаемся дальше и нам осталось добавить в параметры сеанса: прокси, заголовки и возможность отправки запросов POST:
Парсим категории и товары с сайта
Теперь, при помощи нашего класса Parser, мы можем сделать запрос и получить страницу с контентом. Давайте и поступим:
Следующим шагом разбираем пришедший ответ и сохраняем название и ссылку категории в результирующий массив:
Чуть более подробно работу с phpQuery я разобрал в первой статье по парсингу контента. Если вкратце, то мы пробегаемся по DOM дереву и вытягиваем нужные нам данные, их я решил протримить, чтобы убрать лишние пробелы. А теперь выведем категории на экран:
В результате мы получили все ссылки на категории. Для получения товаров используем тот же принцип:
Получаем страницу, тут я увеличил время соединения, так как 5 секунд не хватило, и разбираем её, парся необходимый контент:
Теперь проверим, что у нас получилось, и выведем на экран:
Написание парсера DBML на PHP
Иногда возникает задача парсинга произвольного DSL для дальнейшей работы с ним на уровне PHP кода. И я хочу поделиться опытом решения этой проблемы с примерами.
Достаточно долгое время я пользуюсь сервисом dbdiagram для проектирования структуры БД для будущих или существующих проектов. Данный сервис я выбрал потому, что он достаточно прост в использовании. Описываем структуру таблиц на языке DBML и сразу видим результат.
Пример структуры и визуального представления
Как я говорил ранее, данный сервис использует DBML для описания структуры БД, который они же и придумали и написали спецификацию по нему.
Зачем парсить
После долгого пользования сервисом и создания массивных схем БД, в голове периодически возникала идея: как бы эту схему превратить в PHP код, чтобы потом из этого кода сгенерировать модели и миграции, например для Laravel фреймворка.
Первая попытка
Я решил не изобретать велосипед, а изучить парсер написанный на GO, который работает на конечных автоматах, и повторить все то, что она делает на PHP.
Парсер работает следующим образом: разбивка посимвольно всего документа, токенизация (разбор входной последовательности символов на распознаваемые группы (лексемы) с целью получения на выходе идентифицированных последовательностей, называемых «токенами»). Ну собственно итогом токенизации являетя последовательнось (массив) токенов со значениями.
Пример
После того как токены определены, необходимо пройтись по каждому из них и создать абстрактное синтаксическое дерево.
Рассмотрим следующий пример
И последовательность токенов для него
По токену PROJECT мы понимаем, что далее нас ждет описание проекта и последовательно пытаемся провалидировать структуру проекта.
После реализации на 50% парсинга всех структур DBML, нервы не выдержали и хотя структура проекта достаточно простая, и кажется что все легко, но дальше появляются гораздо более сложные конструкции с другими вложенными конструкциями, валидировать становится все очень сложно и я решил отказаться от этого подхода и посмотреть в сторону других решений.
Библиотека phplrt
Буквально на днях на конференции PHP Russia 2021 @SerafimArtsвыступал с докладом о своем инструменте phplrt, который занимается синтаксическим разбором языка и построением AST. Т.е. данный инструмент решает мою задачу и совершенно другим способом.
Если совсем грубо говоря, то это парсер, который позволяет описать структуру, в моем случае DBML, используя синтаксис EBNF. Согласно EBNF phplrt разбирает структуру и достает из нее значения, токенизирует по своим алгоритмам и строит AST. Далее мы можем работать с построенным деревом.
phplrt использует EBNF для генерирации регулярки или компиляции php файла, который содержит все необходимые инструкции для разбора синтаксиса языка.
Регулярка не для слабонервных для DBML :)))
Ну а теперь разберем пару примеров
Вернемся к нашему примеру со структурой проекта
И опишем основные токены для этой структуры, т.е. опишем элементы, которые встречаются в ней.
Ну а теперь самое интересное, давайте расскажем парсеру о струтктуре проекта с помощью токенов
Я пока что не стал описывать внутреннюю структуру проекта, а только внешнюю часть, чтобы не запутать.
А теперь опишем внутренню часть
Ну а теперь соберем все воедино
Общая идея надеюсь понятна. Я намеренно упростил схему, чтобы ее легче было понять. Полноценную рабочую схему можно посмотреть здесь.
По итогу в тестах мы можем получать xml представление нашего дерева.
Пример
Окей, схему определили, XML получили, что дальше?
А дальше нам нужно превратить все это в объекты. В phplrt есть такое понятие как PHP инъекции
. Важно. Работают толко после компиляции. При генерации XML на лету, они игнорируются.
Инъекции позволяют для каждого правила в нашей схеме сопоставить объект, в который будут переданы все значения правила.
Пример PHP классов
Ну я думаю идея понятна. После того, как парсер получает DBML документ, он его раскладывает по объектам согласно описанным правилам.
Процесс написания парсера с использованием phplrt занял несколько дней и сам процесс создания EBNF выглядел следующим образом:
Брал структуру из DBML документа
описывал её в EBNF
генерировал XML структуру, которая покрывалась тестами
Как только я покрыл все вариации структуры DBML документа тестами и получен итоговой EBNF, то phplrt компилирует её в php код, который далее и будет использоваться для парсинга (Компилятор компилятора).
Первый этап в моем плане реализован. Теперь осталось сделать генератор моделей и миграций.
По итогам работы с инстурментом phplrt хочу выразить респект и уважение @SerafimArts за него, который помог в корне изменить подход к парсингу языка и решить мою задачу.
Отдельное спасибо @greabock и @SerafimArts за помощь в подготовке материала и помощи в разработке парсера.
Парсинг и обработка веб-страницы на PHP: выбираем лучшую библиотеку
Задача спарсить и обработать необходимую информацию со стороннего сайта встает перед веб-разработчиком довольно часто и по самым разнообразным причинам: таким образом можно заполнять свой проект контентом, динамически подгружать какую-то информацию и так далее.
В таких случаях перед программистом встает вопрос: какую из десятков библиотек выбрать? В этой статье мы постарались рассмотреть самые популярные варианты и выбрать из них лучший.
Регулярные выражения
Даже не смотря на то, что «регулярки» — это первое, что приходит на ум, использовать их для настоящих проектов не стоит.
Да, с простыми задачами регулярные выражения справляются лучше всех, но его использование значительно затрудняется, когда нужно спарсить большой и сложный кусок HTML-кода, который, к тому же, не всегда соответствует какому-то определенному шаблону и вообще может содержать синтаксические ошибки.
Вместо «допиливания» своего регулярного выражения при каждом малейшем изменении кода рекомендуем использовать инструменты ниже — это и проще, и удобнее, и надежнее.
XPath и DOM
DOM и XPath не являются библиотеками в привычном смысле этого слова, это стандартные модули, которые встроены в PHP начиная с пятой версии. Именно отсутствие необходимости использовать сторонние решения делает их одними из лучших инструментов для парсинга HTML страниц.
На первый взгляд может показаться, что низкий порог входа — это не о них, некоторые места и вправду являются весьма сложными. Но это только на первый взгляд: стоит только немного разобраться с синтаксисом и базовыми принципами, как XPath тут же станет для вас инструментом для парсинга номер один.
Вот, например, код с использованием DOM и XPath, который ищет в разметке все теги и модифицирует их атрибуты src :
Тем не менее, данный вариант не лишен минусов — для парсинга используется движок, в первую очередь предназначенный для работы с XML, а XML и HTML хоть и являются очень похожими языками, но всё же различаются. Из этого вытекают специфические требования к разметке: например, все HTML теги должны быть закрыты.
Simple HTML DOM
Simple HTML DOM — PHP-библиотека, позволяющая парсить HTML-код с помощью удобных jQuery-подобных селекторов.
Она лишена главного недостатка XPath — библиотека умеет работать даже с невалидным HTML-кодом, что значительно упрощает работу. Вы также забудете о проблемах с кодировкой: все преобразования выполняются автоматически.
Как и JQuery, Simple HTML DOM умеет искать и фильтровать вложенные элементы, обращаться к их атрибутам и даже выбирать отдельные логические элементы кода, например, комментарии.
Несмотря на не самую высокую производительность, по сравнению с другими вариантами, Simple HTML DOM имеет самое большое русскоязычное комьюнити и наибольшую распространенность в рунете — для новичков это делает написание кода с её использованием значительно проще.
phpQuery
Как и Simple HTML DOM, phpQuery является PHP вариантом JQuery, но на этот раз более похожим на своего «старшего javascript-брата».
Портировано почти всё, что есть в JS-фреймворке: поддержка селекторов, атрибутов, манипуляций, обхода, плагинов, событий (в том числе имитации кликов и т.д.) и даже AJAX. Использовать можно как через PHP, так и через командную строку в виде отдельного приложения.
Более того, согласно нашим бенчмаркам, phpQuery оказался в 8 (!) раз быстрее Simple HTML DOM.
Вот небольшой пример на phpQuery, в котором происходит обработка заранее выбранных элементов списка ( li ):
Подробную документацию и больше примеров найдете на официальной странице в Google Code.
htmlSQL
htmlSQL — экспериментальная PHP библиотека, позволяющая манипулировать HTML-разметкой посредством SQL-подобных запросов.
Простейший пример, извлекающий атрибуты href и title всех ссылок (элементы a ) с классом list :
Как и с обычными mysql_ функциями, воспользовавшись методами fetch_array() или fetch_objects(), мы можем получить результат выполнения данного запроса в виде привычного ассоциативного массива или объекта.
Стоит также упомянуть о высоком быстродействии htmlSQL: часто она справляется в несколько раз быстрее phpQuery или того же Simple HTML DOM.
Тем не менее, для сложных задач вам может не хватить функциональности, а разработка библиотеки давно прекращена. Но даже несмотря на это, она всё ещё представляет интерес для веб-разработчиков: в ряде случаев значительно удобнее использовать язык SQL вместо CSS-селекторов. Особенно когда вы не знаете, что такое CSS-селекторы ?
Вывод
В своем мини-исследовании мы пришли к выводу, что в большинстве случаев для парсинга лучше использовать библиотеку phpQuery: она быстрая, функциональная и современная.
С другой стороны, для совсем простых задач логично было бы использовать стандартные модули PHP, такие как XPath, DOM или, на крайний случай, регулярные выражения.
Что-то ещё?
Для PHP существуют ещё десятки разнообразных библиотек и инструментов для парсинга, но в этой статье мы рассмотрели только самые интересные, функциональные и производительные.
Подробнее о других способах парсинга средствами PHP можно прочитать в соответствующей теме на StackOverflow.
Если вы не используете PHP, то можете ознакомится с кратким списком похожих инструментов для других языков программирования: