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

Содержание статьи:



Предисловие


Так как я работаю на php, то мой взор пал на библиотеку phpQuery. Я, конечно, соглашусь, что есть множество других библиотек, в том числе и встроенная в php по умолчанию, но для рядового программиста, который подрабатывает фрилансом на выходных, нужно некое чудо. К счастью, всеми нами движет лень. Одного чеха лень привела к созданию phpQuery.

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

Приступим


PhpQuery не самая быстрая библиотека, но одна из. С новыми версиями php она почти незаметна. Основная нагрузка, как и раньше, ложится на подгрузку страниц.
У неё полно возможностей, о которых не говорится во многих русскоязычных руководствах.
Некоторые программисты, так и не разобравшись с phpQuery, бегут создавать собственные библиотеки (прямо как наши коллеги из мира js). Да, у этой библиотеки есть главный недостаток — код устарел, но вполне себе работает.

Начало работы


Новичкам довольно сложно сходу понять работу phpQuery. Но я постараюсь максимально «разжевать» все сложные моменты.

Многие методы это библиотеки нацелены на работу с Dom, как будто мы работаем на jQuery. Да и названия у данных библиотек максимально похожи.

И так. Для начала нам нужно определиться с сайтом, с которого мы будем забирать HTML код. К слову, это не обязательно должен быть сайт. Если у нас уже есть html (xml) в файле (переменной), то можем подгрузить и оттуда.

/**
Если сайт:
$siteName = "site.com/";

Если файл: 
$siteName = "index.html";
*/

$html = file_get_contents("$siteName");

Далее нам нужно передать полученный код обработчику phpQuery

$dom = phpQuery::newDocument($html);

Метод «newDocument()» вернет dom объект, с которым мы можем работать.

Теперь мы можем что-то найти в этом dom объекте. Давайте представим, что мы подтягиваем страничку сайта, где есть такой блок:

<div class="product-essential">
    <a class="brand-link" href="https://какой-то_сайт.com/какой-то_бренд" title="Какой-то бренд">
        <span class="brand-name">Какой-то бренд</span>
    </a>
    <div class="product-name">
        <h1>Jeans Denim</h1>
    </div>
    <div class="price-info">
        <div class="price-box">
                <span class="regular-price" id="product-price-424337">
                    <span class="price">€ 200</span>
                </span>
        </div>
    </div>
    <div class="description">
        <span class="product-description">Описание товара</span>
        <div class="sku">
            <span> ID продукта:</span>
            <span>830214303</span>
        </div>
    </div>
</div>

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

Практическая часть


Попробуем получить все вышеперечисленные данные.

// Получаем код
$html = file_get_contents("https://какой-то_сайт.com/");

// Получаем объект dom
$dom = phpQuery::newDocument($html);

// Ищем в объекте dom элемент с классом .product-essential, обращаясь к методу find(). Он вмещает в себя все данные о продукте.
foreach($dom->find(".product-essential") as $key => $value){

        // Преобразуем dom объект в объект phpQuery. Делаем сие действие с помощью метода pq(); который является аналогом ($) в jQuery.
    $pq = pq($value);

    // Находим в этом элементе элемент с классом .brand-link и получаем значение атрибута "href" с помощью метода attr();
    $productHref[$key]["brand-href"] = $pq->find(".brand-link")->attr("href");

    // Получаем название бренда. Оно находится в строке <span class="brand-name">Какой-то бренд</span>.
    // Мы можем получить текст, содержащийся в <span> и других тегах с помощью метода text();
    $productHref[$key]["brand-name"] = $pq->find(".brand-name")->text();

    // Далее нам необходимо получить название товара.
    // Помимо указания класса элемента, мы можем указать имя вложенного элемента.
    // В данном случае имя бренда находится в элементе <h1>, который находится в элементе <div class="brand-name">
    $productHref[$key]["product-name"] = $pq->find(".product-name h1")->text();

    // PhpQuery позволяет перечислять классы нескольких, вложенных друг в друга, элементов.
    // Только не забывайте следить за порядком!
    // Тут мы получаем цену товара.
    $productHref[$key]["product-price"] = $pq->find(".price-info .price-box .regular-price .price")->text();

    // Получаем описание товара
    $productHref[$key]["product-description"] = $pq->find(".description .product-description")->text();

    // Так же есть возоможность шагать по элементам.
    // Деется это с помощью метода next();
    // В данном случае мы получим только числовой идентификатор без лишних строк.
    $productHref[$key]["product-id"] = $pq->find(".description .sku span")->next()->text();
    
}

На выходе получаем вот такой массив:

Array
(
    [0] => Array
        (
            [brand-href] => https://какой-то_сайт.com/какой-то_бренд
            [brand-name] => Какой-то бренд
            [product-name] => Jeans Denim
            [product-price] => € 200
            [product-description] => Описание товара
            [product-id] => 830214303
        )

)

Заключение


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

phpQuery::unloadDocuments();

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

В этой библиотеке есть возможность добавлять элементы «на лету». Но эту тему мы затронем в следующей статье.

Комментарии (33)


  1. alexxz
    15.10.2019 20:25

    Сдаётся мне, что большая часть задач просто решается с использованием штатного DOMXPath и, вероятнее всего, заметно быстрее. Да, синтаксис не как у CSS селекторов, но не намного сложнее. Потому библиотека, созданная 10 лет назад, и не обрела себе новой жизни.


    1. gjnext Автор
      15.10.2019 22:27

      Согласен. Но не каждый будет париться с DOMХpath, когда есть сиюминутное решение. Хотя я полностью поддерживаю изучение встроенной библиотеки. У неё, в отличии от phpQuery, есть какое-то будущее.


      1. NiceDay
        15.10.2019 22:41

        phpQuery это просто обёртка для стандартной библиотеки DOM.


      1. BoShurik
        15.10.2019 23:03

        В том же Chrome есть возможность скопировать XPath через панель разработчика. Так что париться не придется


      1. den_rad
        17.10.2019 14:19

        xpath очень простой язык. Если его лень учить, можно dev mode браузера узнать xpath у любого элемента и все.


  1. DarkPreacher
    15.10.2019 22:13

    А ещё есть DiDOM, современная, лёгкая и весьма шустрая библиотека. Можно даже на русском почитать.


    1. gjnext Автор
      15.10.2019 22:28

      Спасибо за совет!


  1. atcliff
    15.10.2019 22:25

    Этот бесконечный

    pq($value)
    ужасно бесит, особенно, если нужно парсить вложенные элементы. DiDom гораздо удобнее для этого


  1. NiceDay
    15.10.2019 22:25
    +2

    господи, неужели кто-то еще пользуется этой библиотекой? не, я понимаю лет эдак 10 назад еще ладно, но мы ведь вроде в 2019ом, php развивается, сообщества перестают выкладывать говнокод и начали задумываться над качеством того, что они делают.

    есть symfony/dom-crawler, который враппер для стандартной DOM библиотеки. И с помощью symfony/css-selector можно писать в без XPath, оно само конвертирует.
    Только вдобавок в отличии от phpQuery оно еще и написано не на статических свойствах, которые хранят хрен пойми, включая прошлые документы и не течет как соломенная крыша.

    // Получаем код
    $html = file_get_contents("https://какой-то_сайт.com/");
    
    // Получаем объект dom
    $dom = phpQuery::newDocument($html);
    
    // Ищем в объекте dom элемент с классом .product-essential, обращаясь к методу find(). Он вмещает в себя все данные о продукте.
    foreach($dom->find(".product-essential") as $key => $value){
    
            // Преобразуем dom объект в объект phpQuery. Делаем сие действие с помощью метода pq(); который является аналогом ($) в jQuery.
        $pq = pq($value);
    
        // Находим в этом элементе элемент с классом .brand-link и получаем значение атрибута "href" с помощью метода attr();
        $productHref[$key]["brand-href"] = $pq->find(".brand-link")->attr("href");
    
        // Получаем название бренда. Оно находится в строке <span class="brand-name">Какой-то бренд</span>.
        // Мы можем получить текст, содержащийся в <span> и других тегах с помощью метода text();
        $productHref[$key]["brand-name"] = $pq->find(".brand-name")->text();
    
        // Далее нам необходимо получить название товара.
        // Помимо указания класса элемента, мы можем указать имя вложенного элемента.
        // В данном случае имя бренда находится в элементе <h1>, который находится в элементе <div class="brand-name">
        $productHref[$key]["product-name"] = $pq->find(".product-name h1")->text();
    
        // PhpQuery позволяет перечислять классы нескольких, вложенных друг в друга, элементов.
        // Только не забывайте следить за порядком!
        // Тут мы получаем цену товара.
        $productHref[$key]["product-price"] = $pq->find(".price-info .price-box .regular-price .price")->text();
    
        // Получаем описание товара
        $productHref[$key]["product-description"] = $pq->find(".description .product-description")->text();
    
        // Так же есть возоможность шагать по элементам.
        // Деется это с помощью метода next();
        // В данном случае мы получим только числовой идентификатор без лишних строк.
        $productHref[$key]["product-id"] = $pq->find(".description .sku span")->next()->text();
        
    }


    а можно было вот так:
    $html = file_get_contents("https://какой-то_сайт.com/");
    
    $crawler = (new Crawler($html));
    $productHref = $crawler->filter('.product-essential')->each($node) {
        return [
            'brand-href'          => $node->filter('.brand-link')->first()->attr('href'),
            'brand-name'          => $node->filter('.brand-name')->first()->text(),
            'product-name'        => $node->filter('.product-name h1')->first()->text(),
            'product-price'       => $node->filter('.price-info .price-box .regular-price .price')->text(),
            'product-description' => $node->filter('.description .product-description')->text(),
            'product-id'          => $node->filter('.description .sku span')->eq(1)->text(),
        ];    
    });


    Сильно сложно?


    1. gjnext Автор
      15.10.2019 22:31
      -1

      Начнем с того, что пользоваться Symfony — уже огромная проблема. Сомневаюсь, что кто-то будет собирать маленький проект на Symfony. Да, согласен, есть новые библиотеки. Одну из них я в статье указал. Но, как мне кажется, phpQuery одна из самых легких для понимания.


      1. DarkPreacher
        15.10.2019 22:36
        +1

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


        1. gjnext Автор
          15.10.2019 22:44
          -2

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


          1. DarkPreacher
            15.10.2019 22:52
            +1

            Не совсем понимаю в чём проблема. Композер ставится глобально, библиотеки подключаются одной командой в консоли, которая копипастится из ридми репозитория, чаще всего. Дальше подключаете сформированный автолоадер в свой проект и пользуетесь. Не надо лазить по разным репозиториям, качать архивы, распаковывать, подключать. Банальная экономия времени.


            1. gjnext Автор
              15.10.2019 23:17
              -1

              Замечательно. Давайте упакуем все это в докер, и, для полноты картины, будем следить за актуальностью БД с помощью доктрины. Тогда проект будет максимально удобным и максимально усложненным.
              Повторюсь, я за использование актуальных библиотек. Но если уж кто-то услышал про phpQuery (а слышат о нем сейчас только новички, либо вспоминают старички), то почему бы не выдать нормальное объяснение с примером?
              А потом эти люди спустятся в комментарии, увидят ваше объяснение про Crawler, заинтересуются им. Будут разворачивать замечательные проекты. Сплошные плюсы же.


              1. NiceDay
                16.10.2019 00:20

                вот вы любите все усложнять:)
                тут к чему ведут, так это что установка пакета композером нисколько не тяжелей скачивания библиотеки руками.
                три строки а консоли (хоть в linux, хоть в каком-нибудь OpenServer под windows) и можно начинать накидывать код. даже если представить что у кого-то еще остался шаред хост, без консоли и вот этого вот всего, то вам же все равно закидывать туда проект по какому-нибудь ftp, так какая разница это папка vendor или phpQuery? и какая разница, будете ли вы писать require 'vendor/autoload.php' в своём скрипте или require 'phpQuery/phpQuery.php'?


              1. trawl
                16.10.2019 04:14

                Но если уж кто-то услышал про phpQuery (а слышат о нем сейчас только новички, либо вспоминают старички), то почему бы не выдать нормальное объяснение с примером?

                Потому что не надо уже. Есть более актуальные и изящные решения для ровно тех же задач.


                Зачем новичкам открывать мир старого? Куда лучше учить их сразу хорошим практикам


          1. NiceDay
            15.10.2019 22:58

            mkdir myproject
            cd myproject
            wget https://getcomposer.org/composer.phar
            php composer.phar require symfony/dom-crawler
            php composer.phar require symfony/css-selector


            если использовать phpquery его же тоже надо скачать, разархивировать, подключить.


          1. SbWereWolf
            16.10.2019 15:30

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


            1. gjnext Автор
              16.10.2019 16:31

              Я же не против. Ни разу, правда, так не делал.


      1. NiceDay
        15.10.2019 22:38
        +1

        symfony/dom-crawler и symfony/css-selector — это два самобытных компонента, для жизни которых не нужна симфони. и вообще ничего не нужно, кроме стандартной DOM-библиотеки, на которой же и ваш phpQuery и основан.

        phpQuery одна из самых легких для понимания.

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


        1. gjnext Автор
          15.10.2019 22:50

          Обходил ~ 3 тысячи страниц за ~ 4000-4500 секунд. Одна страница загружалась чуть больше, чем за секунду.
          Не защищаю phpQuery. О её моральной старости написал в статье. Но она по-прежнему жива.


      1. baldrs
        16.10.2019 08:50

        Symfony это в первую очередь набор компонентов, а уж потом фреймворк. Вы всегда можете установить symfony/dom-crawler не устанавливая все остальное


      1. pOmelchenko
        16.10.2019 17:41

        Мы и так отмыться от стереотипов никак не можем :( Так еще и динозавров выкапывают


    1. joker2k1
      16.10.2019 02:12

      интересное определение функции ) надеюсь это описка


      1. NiceDay
        16.10.2019 16:57

        да, в спешке набрасывал)


    1. morgot
      16.10.2019 16:03

      Я вообще регулярками все делаю, правда я не гуру в РНР.
      Но, какая разница, если работает?


      1. NiceDay
        16.10.2019 16:59
        +1

        неужели городить регулярку проще, чем написать что-то в духе

        (new Crawler($html))
            ->filter('.classname')
            ->first()
            ->attr('id')


        1. morgot
          17.10.2019 02:34

          Не проще, конечно, но как-то привычней. Возможно, просто дело в том, что большинство парсеров я писал на Perl… Как то пробовал разные РНР библиотеки, так ни на чем и не остановился. Тем более, современный РНР это уже совсем не то, что лет 10 назад. Композер и прочее, ради 2 строчек кода. Впрочем, это уже оффтоп, извините.


          1. NiceDay
            17.10.2019 17:57

            не ну само собой, что если нужно просто выдрать один атрибут или контент из тега, то регулярка сойдет.
            но посмотрите на пример из поста — там же регексп будет длинней, чем оставшийся код на php:)
            плюс DOM-парсеры более универсальное решение все же.


  1. kovserg
    16.10.2019 01:34

    Чем phpQuery лучше simplehtmldom?


    1. baldrs
      16.10.2019 08:51

      На jQuery похож


  1. northmule
    16.10.2019 08:20

    Так как «пишу» и «писал» ранее много парсеров, то в своей работе использовал и simplehtmdom, затем по каким-то причинам перешёл на phpQuery (перешёл наверное из за того что сначала просто попробовал, а потом заметил кратное увеличение скорости работы парсреа) и она мне понравилась больше (она это библиотека). Совсем недавно попробовал для парсинга DomCrawler от Symfony и мне она по удобству показалась такой же как phpQuery. Даже сказал бы так что «phpQuery»==«DomCrawler» для разбора страниц.
    PS: Про удобство DomCrawler конечно же имею ввиду вкупе с css-selector пакетом


  1. Oniry
    16.10.2019 14:23

    Сравнение с другими парсерами (1.6.3)
    Потребления памяти (в байтах)
    Максимальное
    Nokogiri — 763568
    DiDom — 793096
    Zend Dom — 954712
    DomCrawler — 1534512
    Simple HTML DOM — 16839400
    В конце теста
    Nokogiri — 157168
    DiDom — 158896
    Zend Dom — 329232
    DomCrawler — 567440
    Simple HTML DOM — 14113456
    Затраченное время (в секундах)
    DiDom — 27.0787
    Nokogiri — 27.1009
    DomCrawler — 36.0982
    Zend Dom — 48.3222
    Simple HTML DOM — 188.0247

    1 в поисковиках вылазит в большом количестве Simple. Но он протекает и медленный.