парсим сайты легко и непринуждённо вместе с phpQuery

    Привет хабр, думаю у некоторых из вас возникала потребность стянуть что нибудь нужное с какова нибудь сайта. Но писать тонны функций, чтобы вытащить фразу, это очень муторно, долго и не продуктивно.
    Представляю вашему вниманию phpQuery. Это php вариант известного javascript фраемворка — jQuery.
    Автор постарался на славу и портировал почти всё что нам нужно.
    • Selectors
    • Attributes
    • Traversing
    • Manipulation
    • Ajax
    • Events
    • Utilities
    • Plugin ports

    Давайте посмотрим что она умеет.

    А умеет она всё, что умеет её старший брат jQuery.
    Собственно, для начала, далеко ходить не будем и остановимся на %username%.habrahabr.ru/blog/

    Имеется довольно много способов сделать это.
    phpQuery::newDocument($html, $contentType = null) Создаём новый документ из разметки. Если не указали $contentType, будет определен основываясь на разметке. Если не получится то будем считать что это, text/html в utf-8.
    phpQuery::newDocumentFile($file, $contentType = null) Создаём новый документ из файла. Работает также как и newDocument()
    phpQuery::newDocumentHTML($html, $charset = 'utf-8')
    phpQuery::newDocumentXHTML($html, $charset = 'utf-8')
    phpQuery::newDocumentXML($html, $charset = 'utf-8')
    phpQuery::newDocumentPHP($html, $contentType = null) Подробнее можете почитать об этом здесь.
    phpQuery::newDocumentFileHTML($file, $charset = 'utf-8')
    phpQuery::newDocumentFileXHTML($file, $charset = 'utf-8')
    phpQuery::newDocumentFileXML($file, $charset = 'utf-8')
    phpQuery::newDocumentFilePHP($file, $contentType) Подробнее можете почитать об этом здесь.

    Ну а мы, далеко ходить не будем. Давай %username%, распарсим твои записи в блоге. Сначала скачаем phpQuery. Теперь создаём что то вроде index.php
    <?php
       require ('phpQuery/phpQuery.php');
       
      $habrablog = file_get_contents('http://%username%.habrahabr.ru/blog/');
      
      $document = phpQuery::newDocument($habrablog);
      
      $hentry = $document->find('div.hentry');
      
      foreach ($hentry as $el) {
        $pq = pq($el); // Это аналог $ в jQuery
        $pq->find('h2.entry-title > a.blog')->attr('href', 'http://%username%.habrahabr.ru/blog/')->html('%username%'); // меняем атрибуты найденого элемента
        $pq->find('div.entry-info')->remove(); // удаляем ненужный элемент
        $tags = $pq->find('ul.tags > li > a');
        $tags->append(': ')->prepend(' :'); // добавляем двоеточия по бокам
        $pq->find('div.content')->prepend('<br />')->prepend($tags); // добавляем контент в начало найденого элемента
      }
      
      echo $hentry;
    ?>
    

    Это всего лишь малая часть того что возможно сделать.
    Также вместе с ней поставляется такая штука как jQueryServer. По сути, это тоже самое что и phpQuery, но на стороне клиента.
    Пример из демки
    <script type="text/javascript">
    jQuery.serverConfig.url = '/phpQuery/jQueryServer/jQueryServer.php';
    function demo() {
      $.server({
          url: document.location.toString(),
          dataType: 'json'
        })
        .find('li')
        .client(function(response){
          $.each(response, function(k, li){
            $('ul').append(li);
          });
        });
    }
    $(function(){
      $('ul').append('<li>above LIs will be downloaded and appended below in 2 seconds...</li>');
      setTimeout(demo, 2000);
    });
      </script>
    

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

    Google Code
    Официальный блог

    Если вам интересно в следующей статье я хочу рассмотреть парсинг сайтов доступных только авторизованным пользователям (без капчи конечна). Да, phpQuery умеет и это, правда не без помощи Zend Framework.
    Метки:
    Поделиться публикацией
    Комментарии 35
    • +1
      Спасибо, интересно было почитать. А по поводу производительности — вы сравнивали насколько это быстрее/медленнее чем парсить используя регекспы?
      • 0
        сегодня с утра как раз ручками писал… особо писать нечего кроме регулярки
        • 0
          работает. но кривота гербедж коллектора в php такова что на парсинге сайтов в 20 000 страниц не успевает удалять объекты (которые удаляются implicitly) скрипт отжирает до гига памяти. мораль — учим XPath & DOMXML
          • 0
            Таким способом «XPath & DOMXML» память намного экономнее используется?
            • +1
              ну метров 102-105 на неоптимизированном php-cli. скорость работы тоже на пару порядков выше — 20-30 минут вместо 4 часов на phpquery. да и кода меньше получается.
              • 0
                Спасибо за консультацию, тоже в принципе достаточно много, но как говорится, за удобство приходится платить. Если памяти мало, то мой вывод — лучше использовать регулярные выражения.
                • 0
                  это user memory на процесс. memory_get_usage дает от десяти до 30 мегабайт.

                  ну хз хз. ругйлярки мне не подошли, потому что приходилось парсить выражения типа «h2 который находится в том же div что и третья таблица в документе» — без классов и id. xpath это делает одной строкой — //table[3]/../h2
          • 0
            И еще интересно, как на счет потребления памяти по сравнению с регулрными выражениями?
            Я конечно не тестировал, но предполагаю, что ругулярки будут выигрывать на порядок.
            • 0
              Регулярные выражения не подходят для парсинга xml-подобного документа. Дело не в скорости, а в сложности.
            • 0
              Штука очень забавная. Правда не без глюков.
              А насчет регулярок — да, быстрее, но с ней гораздо проще, особенно при разборе сложных докментов.
              • 0
                Именно это удобство меня и зацепило в ней.
                Насчёт глюков, библиотека ещё молодая, если судить по дате публикации архивов =) Да и документированность плохая, радуют «Enter description here...» в коде =)
              • +1
                А если документ невалидный в плане хмл?
                • 0
                  Т.к. phpQuery использует DOMXML & XPath внутри, то и отношение к malformed xml у него аналогичное, поэтому наверное лучше перед загрузкой прогнать через Tidy
                • 0
                  Хороший инструмент. спасибо!
                  Как бы ещё добраться до контента, генерируемого динамически, например тем же Javascript'ом?
                  • +1
                    тем же javascript'ом и добраться :)
                    передать на сервер можно через ajax
                    • 0
                      Не совсем понял что Вы Имеете в виду. Вы предлагаете с помощью phpquery решить этот вопрос или «вручную» разбираться в ява скрипте? А если там какое-либо сложное высчитывамие переменных для запроса или не запрос вовсе, а какой-нибудь document.write, но опять-таки со сложновысчитанными параметрами?
                      • 0
                        Вижу очень безумную идею, расширение к firefox, которое дампит данные по факту загрузки страницы и отсылает их на сервер. где они уже и сохраняются, при этом можно firefox автоматизировать, пусть сам ходит по страницам и отсылает данные, жаль что это клиент side
                        • +1
                          нет. Если проблема частного характера, то можно тем же jquery повесить обработчики и генерируемый контент пересылать на сервер.
                          Если общего и неизвестно какие функции генерируют контент, можно проверять блоки данных на изменение с первоначальной информацией.
                          вообще к задаче лучше подходить практически — решать конкретно то что нужно делать. потом и обобщить можно…
                          • 0
                            Можете привести пример задачи общего характера?
                            • +1
                              притянутый за уши только :)
                              к примеру есть несколько сайтов с разной структурой, где стоят какие то информеры, которые тянуться уже после загрузки страницы. расположение неизвестно.

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

                                если они подгружатеся динамически уже после загрузки страницы?
                                • 0
                                  без понятия. надо рассматривать конкретную задачу :)

                                  ***теоретически… допустим у нас есть сайт, которые разбит по блокам с какими то id
                                  при первом парсинге запоминаем эти блоки и их длину (можно на клиенте str.length)
                                  проходимся опять по блокам и сраниваем их длину. если контента стало больше — появилось свеженькое.
                    • 0
                      Афигенная штука, если надо распарсить сложный древовидный документ. На одной странице работает терпимо, а весь сайт парит медленно. За удобство приходится платить?

                      Думаю, это библиотека подходит для использования в условиях выделенного сервера с возможностью запуска PHP без ограничения памяти и времени исполнения, а на виртуальном хостинге — обрывается.
                      • 0
                        Как вариант работа с домашнего сервера с предварительной загрузкой страницы через wget.
                        + прикручиваем потоки для php, время должно сократиться, а вот загрузка (как памяти так и процессорных мощностей) может возрасти, вопрос только в каких пределах.
                        • 0
                          А почему именно wget'ом хотите забирать, а не, например, curl'ом?
                          • 0
                            а) Функционала больше.
                            Иногда необходимо сдёрнуть всю страницу + уровни лежащие ниже, или же отзеркалить структуру в уже сущевствующую папку для проверки произошедших изменений.

                            б) привык =)

                            Хотя иногда проще парсить страницы на лету perl + regex'ы
                      • 0
                        При выводе скаченной страницы без парсинга все выклядит нормально, после парсинга бьются русские буквы? как решить эту проблему?
                        • 0
                          возможно сам фаил скрипта не в той кодировке или кодировка сайта отлична от UTF-8
                          • 0
                            в том то и дело, что сайт который паршу — утф, скрипт — утф, все утф
                            • 0
                              Проверяйте настройки DOM расширения для php или попробуйте на другой установке Php
                              • 0
                                в локале (MacOSX Lion) и на сервере (Debian) результат одинаков
                          • –1
                            echo iconv("windows-1251", "utf-8", $content);
                            • 0
                              Да вы не просто капитан, вы подполковник очевидность. Проблема была с разной разрядностью символов.
                          • 0
                            уберите указание кодировки в коде

                            $doc = phpQuery::newDocumentHTML($data['document']);
                            

                            Мне помогло

                            	public static function newDocumentHTML($markup = null, $charset = null) {
                            		$contentType = $charset
                            			? ";charset=$charset"
                            			: '';
                            		return self::newDocument($markup, "text/html{$contentType}");
                            	}
                            

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.