w999d @w999d read-only
Пользователь
16 декабря 2010 в 01:36

Разработка → Нокогири: парсинг HTML в одну строку

PHP*
Мучительный опыт парсинга кучи невалидных html и «xml» документов и удивительная простота dapper (сервис) и nokogiri (ruby) заставили потратить 5 часов на написание своей собственной пилы.

image

Примеры использования:


<?php
$html = gzdecode(file_get_contents('http://habrahabr.ru/'));

$saw = new nokogiri($html);
var_dump($saw->get('a.habracut')->toArray());
// На выходе: Array(6) { [0]=> array(3) { ["class"]=> string(8) "habracut" ["href"]=> string(56) "http://habrahabr.ru/blogs/google_chrome/110099/#habracut" ["#text"]=> string(29) "Читать дальше →" } [1]=> ....
var_dump($saw->get('ul.panel-nav-top li.current')->toArray());
// На выходе: array(2) { ["class"]=> string(7) "current" ["a"]=> array(3) { ["href"]=> string(20) "http://habrahabr.ru/" ["class"]=> string(8) "disabled" ["#text"]=> string(10) "Посты" } }
var_dump($saw->get('#sidebar dl.air-comment a.topic')->toArray());
// На выходе: array(50) { [0]=> array(3) { ["class"]=> string(5) "topic" ["href"]=> string(36) "http://habrahabr.ru/blogs/os/110045/" ["#text"]=> string(63) "ФБР внедряло backdoor'ы в IPSec код OpenBSD (?)" } [1]=> array(3) { ["cl
var_dump($saw->get('a[rel=bookmark]')->toArray());
// На выходе: array(10) { [0]=> array(4) { ["rel"]=> string(8) "bookmark" ["href"]=> string(47) "http://habrahabr.ru/blogs/google_chrome/110099/" ["class"]=> string(5) "topic" ["#text"]=> string(100) "Google объявил Chrome готовым к использованию в бизнес-среде" } [1]=> array(4) { ["rel"]=


Ошибки html игнорируются.
Распознаются вложеные теги (через пробел), а также конструкции вида .class, #id и [attr=value]
Создание из строки: nokogiri::fromString($htmlString); или new nokogiri($htmlString);
Создание из DomDocument: nokogiri::fromDom($dom);

Сам парсер


(он небольшой, поэтому копию оставлю здесь):
<?php

/**
 * Description of nokogiri
 *
 * @author olamedia
 */
class nokogiri implements IteratorAggregate{
    protected $_source = '';
    /**
     * @var DOMDocument
     */
    protected $_dom = null;
    /**
     * @var DOMXpath
     * */
    protected $_xpath = null;
    public function __construct($htmlString = ''){
        $this->loadHtml($htmlString);
    }
    public static function fromHtml($htmlString){
        $me = new self();
        $me->loadHtml($htmlString);
        return $me;
    }
    public static function fromDom($dom){
        $me = new self();
        $me->loadDom($dom);
        return $me;
    }
    public function loadDom($dom){
        $this->_dom = $dom;
        $this->_xpath = new DOMXpath($this->_dom);
    }
    public function loadHtml($htmlString = ''){
        $dom = new DOMDocument('1.0', 'UTF-8');
        $dom->preserveWhiteSpace = false;
        if (strlen($htmlString)){
            libxml_use_internal_errors(TRUE);
            $dom->loadHTML($htmlString);
            libxml_clear_errors();
        }
        $this->loadDom($dom);
    }
    function __invoke($expression){
        return $this->get($expression);
    }
    public function get($expression){
        if (strpos($expression, ' ') !== false){
            $a = explode(' ', $expression);
            foreach ($a as $k => $sub){
                $a[$k] = $this->getXpathSubquery($sub);
            }
            return $this->getElements(implode('', $a));
        }
        return $this->getElements($this->getXpathSubquery($expression));
    }
    protected function getXpathSubquery($expression){
        $query = '';
        if (preg_match("/(?P<tag>[a-z0-9]+)?(\[(?P<attr>\S+)=(?P<value>\S+)\])?(#(?P<id>\S+))?(\.(?P<class>\S+))?/ims", $expression, $subs)){
            $tag = $subs['tag'];
            $id = $subs['id'];
            $attr = $subs['attr'];
            $attrValue = $subs['value'];
            $class = $subs['class'];
            if (!strlen($tag))
                $tag = '*';
            $query = '//'.$tag;
            if (strlen($id)){
                $query .= "[@id='".$id."']";
            }
            if (strlen($attr)){
                $query .= "[@".$attr."='".$attrValue."']";
            }
            if (strlen($class)){
                //$query .= "[@class='".$class."']";
                $query .= '[contains(concat(" ", normalize-space(@class), " "), " '.$class.' ")]';
            }
        }
        return $query;
    }
    protected function getElements($xpathQuery){
        $newDom = new DOMDocument('1.0', 'UTF-8');
        $root = $newDom->createElement('root');
        $newDom->appendChild($root);
        if (strlen($xpathQuery)){
            $nodeList = $this->_xpath->query($xpathQuery);
            if ($nodeList === false){
                throw new Exception('Malformed xpath');
            }
            foreach ($nodeList as $domElement){
                $domNode = $newDom->importNode($domElement, true);
                $root->appendChild($domNode);
            }
            return self::fromDom($newDom);
        }
    }
    public function toXml(){
        return $this->_dom->saveXML();
    }
    public function toArray($xnode = null){
        $array = array();
        if ($xnode === null){
            $node = $this->_dom;
        }else{
            $node = $xnode;
        }
        if ($node->nodeType == XML_TEXT_NODE){
            return $node->nodeValue;
        }
        if ($node->hasAttributes()){
            foreach ($node->attributes as $attr){
                $array[$attr->nodeName] = $attr->nodeValue;
            }
        }
        if ($node->hasChildNodes()){
            if ($node->childNodes->length == 1){
                $array[$node->firstChild->nodeName] = $this->toArray($node->firstChild);
            }else{
                foreach ($node->childNodes as $childNode){
                    if ($childNode->nodeType != XML_TEXT_NODE){
                        $array[$childNode->nodeName][] = $this->toArray($childNode);
                    }
                }
            }
        }
        if ($xnode === null){
            return reset(reset($array)); // first child
        }
        return $array;
    }
    public function getIterator(){
        $a = $this->toArray();
        return new ArrayIterator($a);
    }
}



Требования:
  DOM, libxml, php 5.3 (вероятно работает и со многими старыми версиями)
  HTML на входе должен быть в кодировке UTF-8
Отдаю код под MIT ака AS IS.
Если код окажется вам полезен и есть пожелания, высказывайте, возможно выполню и выложу уже svn.
P.S. Обновил код — добавил поддержку __invoke, IteratorAggregate, упрощен поиск вложенных тегов
Пример с итератором:
foreach ($saw->get('#sidebar a.topic') as $link){
    var_dump($link['#text']);
}

Предыдущие версии можно найти здесь.

UPD 07/11/2011
Последняя версия на github
w999d @w999d
карма
64,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • +2
    Интересно, конечно. А не пробовали сравнивать производительность с phpQuery или аналогичными парсерами?
    • +2
      производительность в данном случае меня не интересовала, так как обычно ответа от удаленного сервера приходится ждать на порядки дольше.
      • +1
        Вообще-то при парсинге больших объемов данных время обработки очень даже ощутимое.
        • +1
          тогда сразу скажу — для парсинга больших объемов (которые все-таки умещаются в рамках одного DomDocument) надо переписать момент с обработкой вложенных тегов (сейчас теоретически могут быть проблемы с памятью, в зависимости от уровня вложенности).
      • 0
        А парсить можно и HTML-файлы, лежащие на диске…
        • 0
          можно, конечно, я ведь уточнил, что это в «данном случае» — писалось под конкретную задачу.
          а производительность сейчас должна быть выше, чем у phpQuery ввиду простоты своей конструкции (читал phpQuery, там разбор сложнее и уже нашел недоработки :) )
      • 0
        Производительность ладно, а чем функционал от того же phpQuery отличается?
  • +1
    Раз уж только php5.3, то почему бы не использовать __invoke?
    $saw->get('a[rel=bookmark]')->toArray()
    // =>
    $saw('a[rel=bookmark]')->toArray()
    
    • 0
      не только 5.3, просто на предыдущих версиях не проверялся, комментарий «вероятно работает и со многими старыми версиями» не могу уже сделать жирнее :/
      • 0
        а зря. давно пора отказаться от устаревших версий програмного обеспечения)
        • +1
          это уже хостеров надо просить, да и не все клиенты соглашаются менять обжитые годами сервера.
          • +1
            в любом случае, такой код не сломает совместимости со старыми версиями:
            function __invoke ($expression) {
                return $this->get($expression);
            }
            
            • 0
              … если его не использовать.
              • +1
                кто хочет — может забить на поддержку устаревших версий пхп, кто хочет — может не забивать. библиотека поддерживает и то и то
        • +5
          Давно пора перестать ломать совместимость в пределах одной major-версии
      • 0
        Если не только, то можно сделать

        $saw->{"'a[rel=bookmark]"}->toArray();

        через __get

        Или даже вернуть на вызов $saw->{"'a[rel=bookmark]"} объект, который будет вести себя как массив.
    • +4
      Тогда уж стоит также имплементировать хотя бы ArrayIterator. Можно было бы писать:
      foreach($saw('a[rel=bookmark]') as $item)
      {
      ...
      }

      • 0
        вообще да, работы много еще
      • 0
        хорошая идея ) возьму на заметку
    • 0
      за такой код с работы надо гнать, зачем превращать код во write only, который без копания в коде библиотеки нельзя понять
      • 0
        а чем $saw('a[rel=bookmark]') отличается от $saw->get('a[rel=bookmark]') для программиста кроме более короткой записи?
        • +2
          отсутствием магии, автокомплитом с подсказкой значений, типом возвращаемого значения, phpdoc-ом для метода позволяющий задокуметировать его.
          точно так же можно сделать через [] и arrayaccess, но это экономия на буквах приводит
          к ухудшению читаемости, понятности и прозрачности кода, а главное — проблем с поддержкой, потому что чёрный щик превращается в шкатулку пандоры.
          • 0
            отсутствием магии

            Это не аргумент

            автокомплитом с подсказкой значений, типом возвращаемого значения, phpdoc-ом для метода позволяющий задокуметировать его.

            __invoke ничем не отличается от обычного метода. в нормальных IDE все будет работать отлично. phpdoc — тоже. так что эти аргументы никаким боком не привязываются к данной ситуации.

            к ухудшению читаемости, понятности и прозрачности кода, а главное — проблем с поддержкой,

            Личное мнение, которое ошибочно и выплывает в боязни всего, чего нету в Java, а следовательно не написано в любимой книге. У людей, у которых нету своего мнения, кроме книжного часто так бывает.
            • +1
              какая ide для php поддерживает подсказки для $var('method_name'), переход по ctrl+click ???
              у меня netbeans 6.9.1 не умеет так в режиме php 5.3.

              у меня боязнь неявного синтаксиса выработался не столько книгами, сколько собственным горьким ОПЫТОМ работы с ДОЛГОИГРАЮЩИМИ проектами. если ide позволяет удобно работать с подобной магией (@property для __get __set), то — только рад использовать фичи, а иначе — согласно Макконнеллу «Пишите код так, как будто человек, который будет его поддерживать — серийный маньяк, который знает, где вы живете».

              Вот я привёл список минусов, приведите плюсы вашего подхода и можно ли их сравнивать с минусами?

              p.s. Извиняюсь, что долго отвечаю, дотроллился, приходите на phpclub, мы там как раз обсуждали недавно тему поддержки ruby проектов и как бороться с проблемами синтаксического сахара.
              • 0
                Ничего, что долго.

                В отличии от такой магии как __get и __set подход с __invoke совершенно логичен и ожидаем. Для программиста он не будет отличатся от просто вызова метода. вот __get, __set — да, там можно намутить, но все-равно используют

                Руби — очень сладкий, согласен. Но ПХП слишком несладкий. Совсем чуть чуть сахара ему точно не помешает)
  • 0
    Кстати, я так понял, вы создаете отдельное дерево поиска. А почему бы не попробовать сразу превращать css-селекторы в xpath?
    • 0
      они и преобразуются в запрос xpath: ...$nodeList = $this->_xpath->query($query);…
      где $this->_xpath — это объект DOMXpath
    • 0
      или это про вложенность?
      • +1
        Это про то, чтоб не создавать новое дерево, а писать xpath, которыми можно искать элементы прямо в загруженном.
          • 0
            а зачем нужны строчки
            if ($node->childNodes->length == 1){
                  $array[$node->firstChild->nodeName] = $this->toArray($node->firstChild);
            }else{
            

            разве цикл в else не делает ту же самую работу?
            • 0
              этот кусок кода из другого класса, с преобразованием в массив вообще не знаю, что делать — ['#text'] это, конечно, неплохо… но подумываю добавить куда-нибудь класс с методом __toString, чтобы проще получать текст внутри тега.
              • 0
                или отдельный метод для получения массива нод
          • 0
            Смотрел я код. Видимо, вы не поняли, что я имею ввиду. Я предлагаю с помощью небольших изменений избавиться от этого:
             $newDom = new DOMDocument('1.0', 'UTF-8');
             $root = $newDom->createElement('root');
             $newDom->appendChild($root);
            

            • 0
              на nodeList просто не навешать xPath, а значит незначительными изменениями не обойтись :/ — вижу вариант писать еще метод fromNodeList, в котором отключать создание DOMXpath и метода get или делать дополнительные проверки и создание DOMXpath непосредственно перед использованием.
            • 0
              попробовал второй вариант, с созданием xpath перед использованием
              code.google.com/p/kanon-framework/source/browse/trunk/src/parse/nokogiri.php?spec=svn1373&r=1373
  • +1
    выложу уже svn
    Лучше на GitHub или BitBucket :)
    • 0
      поддерживаю, однозначно GitHub. Многие разработчики переходят с svn на git и hg, но я никогда не видел обратного, что характерно
      и да, GitHub — лучшее, что я видел для совместной разработки. Переношу свои проекты с Google Code потихоньку туда
      • –3
        я перешёл на свн после того как заебался создавать по репозиторию для каждой библиотеки.
        • 0
          Заебались делать
          git init
          ? Похоже, вы написали несметное количество библиотек. Хотя, возможно, логика истории в SVN помогает.
          • –3
            на гитхабе. и вообще, эта возьня с ключами и протоколами попортила мне немало серого вещества. а если учесть, что гит мержит ничем не лучше свн-а, то становится вообще не понятно кто в здравом уме будет им пользоваться. этакий гиковский распределённый свн.

            порядка сорока
            • 0
              Ну, у всех свои сложности, у меня было наоборот :)
            • 0
              эээ. какая возня с ключами? о чём вы?
              git init
              git add .
              git commit -m 'first commit'
              git remote add origin git@github.com:theshock/{PROJECT_NAME}.git
              git push origin master
              [ввести пароль, который сам придумал, он один для всех проектов]


              репозитарий готов. более того, 4 из 5 строчек вставляются копипастом.
              • 0
                Permission denied (publickey).
                fatal: The remote end hung up unexpectedly
                • 0
                  Настройки неправильные где-то, видимо.
                  shock@localhost:~/test> git init
                  Initialized empty Git repository in /home/shock/test/.git/
                  shock@localhost:~/test> git add .
                  shock@localhost:~/test> git commit -m 'first commit'
                  [master (root-commit) fd0015b] first commit
                  1 files changed, 1 insertions(+), 0 deletions(-)
                  create mode 100644 README
                  shock@localhost:~/test> git remote add origin git@github.com:theshock/test.git
                  shock@localhost:~/test> git push origin master
                  Enter passphrase for key '/home/shock/.ssh/id_rsa':
                  Counting objects: 3, done.
                  Writing objects: 100% (3/3), 219 bytes, done.
                  Total 3 (delta 0), reused 0 (delta 0)
                  To git@github.com:theshock/test.git
                  * [new branch] master -> master
                  shock@localhost:~/test>

                  Результат: github.com/theshock/test
                  • 0
                    > Enter passphrase for key '/home/shock/.ssh/id_rsa'
                    • 0
                      да, я так и говорил:
                      [ввести пароль, который сам придумал, он один для всех проектов]
                      • 0
                        а перед этим сгенерировать ключи, один дать гитхабу, другой ссш клиенту, скопипастить на все машины с которых работаешь.
                        • 0
                          10 минут потратить, чтобы была удобная работа?
                          • 0
                            она может быть удобной и без этого геморроя.
                  • 0
                    Конечно, нужно же ввести public-key в настройках аккаунта на ГитХабе.
      • 0
        Кстати, недавно попробовал контрибьютить в проект, который разрабатывается на Launchpad-е… Знаете, мое мнение — Launchpad гораздо мощнее collaborative платформа чем GitHub. Конечно, bazaar сосет по сравнению с Git но сам Launchpad реально мощнее.

        Вот скажите — на GitHub много кто пользуется чем-то еще кроме самого хостинга репозитория и README файла? Имею в виду тикеты, Wiki пр?
        В то же время на Launchpad есть Blueprints, Q&A, командные (групповые) аккаунты, лента новостей. Есть возможность перевода приложения пользователями на различные языки из веб-интерфейса. Есть сборка DEB пакетов для Ubuntu + PPA (ну это не всем нужно правда). И, как я вижу, большинство проектов всеми этими возможностями довольно активно пользуются.
        • 0
          из того, что я помню — это отличные Pull Request, возможность скачать все одним архивом. на самом деле я буквально месяц как перешёл на него с Google Code, так что я не эксперт)
        • 0
          Launchpad больше заточен под deb-пакеты изначально, потому просто другой. Тотже Google code тоже умеет пости всё из этого, но ГихХабом банально проще и приятнее пользоваться.

          Насчёт того, кто что и как часто использует — это решение отдельных пользователей. кто-то хостит там HTML и .dot файлы для синхронизации между компьютерами. А кто-то пользуется на полную катушку всеми возможностями сайта :) Тикеты используют все крупные проекты. С вики та же история. Командные аккаунты есть, см. github.com/nodejitsu. Возможности перевода нет, просто потому что он не заточен под пакеты.

          А для сборки пакетов есть более функциональные (имхо) сервисы, вроде openSuSe BuildService. Умеет собирать и RPM и делает пакеты под различные дистрибутивы.
  • –8
    Блиииин. HTML парсеры пишутся так: закачиваем, прогоняем через tidy, чтобы получить валидный XML, используем XPath, XSLT и т.п. Если очень хочется CSS выражений, то их очень легко преобразовать к XPath.

    Ваш же вариант рано или поздно обязательно порушится от невалидного HTML'a. Атрибуты без кавычек лишние символы ", < и что-нить в этом вроде.
    • +12
      1. зачем нужен tidy, если можно просто игнорировать ошибки?
      2. XPath и используется, и именно преобразование
      3. вариант с tidy точно так же может вырезать часть невалидного кода

      код не читаем, комментарии не читаем, а высказаться хочется )
  • +5
    Есть ещё PHP Simple HTML DOM Parser. Крутая штука.
    • 0
      Забыл указать, его фишка в том, что можно использовать jQuery-селекторы.
    • 0
      Да вешь полезная однозначно, но помоему автор уже давно забил на нее, так что проект не завивается.
      • +2
        Как раз-таки завивается:))
        • 0
          Взгляните на релиз последней версии — 2008-12-18
          И да проектный сыроват при это, бугов достатачно, при том что оперативку съедает за раз.
          • +4
            поэтому и «за-», а не «раз-»
  • +2
    А чем phpQuery не подошёл? Сравнения с ним действительно не хватает.
  • 0
    Автор молодец!

    Но я так понимаю, что работать парсер будет только с валидными XHTML документами? Или я ошибаюсь?
    • 0
      > Ошибки html игнорируются.
  • 0
    Наверное не правильно выразился, хотел написать «работать правильно». Т.е. если на входе даем ошибочный html — то ответ не всегда будет ожидаемым или же его вовсе не будет, так?
  • +3
    PHP Simple HTML DOM Parser давно не обновляется, к тому-же судя по исходнику там нормальным написанием под PHP 5 особо не пахнет. К тому же там чудовищьное потребление памяти. Но, чёрт возьми, удобно.

    ИМХО, надо бы проект в опен сорц и развивать. Нормальных парсеров HTML'a на самом деле тупо нету в дикой природе, вообще… Сидишь каждый раз изобретаешь велосипед.
  • 0
    картинка классная :)
  • 0
    А как у вас обстоят дела с закомментированными кусками HTML? Например
    <!-- <div id="someId">?</div> -->
    • 0
      парсингом занимается libxml, в массиве это выглядит как ["#comment"]
    • +1
      это выглядит как ['#comment'], в svn последняя версия возвращает и значение комментариев
  • +3
    Вставлю еще свои 5 копеек сюда.

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

    Столкнулся с такой проблемой при обработке через DOMDocument кириллических страниц и страниц в UTF-8. По идее в этом парсере тоже должны быть такие проблемы — буду признателен, если кто-нибудь проверит. У меня, к сожалению, не под рукой PHP сейчас, чтобы самому проверить.

    1) кириллический текст например в title до meta с указанием кодировки — вся кириллица превращается в htmlentities. Если она была в UTF-8, то обратно ее уже нельзя будет вернуть через html_entity_decode()
    2) отсутствие тега meta с указанием кодировки — то же самое, что и п.1
    3) битый UTF-8 — например, если кто-то сделал у себя на сайте substr($title, 250), чтобы сильно длинный тайтл не показывать ) — решается через iconv('UTF-8', 'UTF-8//IGNORE', $html);
    4) на невалидном HTML ведет себя сильно отлично от большинства браузеров. Например, если встретит td внутри div — все дерево DOM будет перекошено ого-го как. Выдернуть из него почти ничего не реально.

    Собственно, алгоритм у меня такой был перез загрузкой в DOMDocument:
    1) находим meta content-type. Если не нашли — определяем кодировку сами и генерируем нужный meta content-type.
    2) вставляем его в самое начало head.
    3) если работаем с UTF-8 — удаляем битые символы.

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

    Была идея выдернуть парсер HTML из WebKit, например, но я подумал, что это уже слишком жестко для такой задачи )
    • 0
      по первым двум пунктам есть хороший комментарий.
      третий пункт зависит от задачи — где-то его надо делать, где-то можно обойтись без него.
      по четвертому надо проверять опять же, пока ничего не смогу сказать
      • 0
        спасибо за ссылку на хак. Как-нибудь надо посмотреть, как она работать будет на невалидном HTML.

        Но. Все-таки задачи парсинга HTML не ограничиваются парсингом идельных UTF-8 страничек, поэтому с вами не могу согласиться, что все зависит от задачи. Как я сам в свое время пришел к такому выводу: никогда не было проблем с парсингом — до того момента, как не сделали одну систему, в которую заказчик вводит грубо говоря URL и XPath — и получает в базе у себя результат парсинга. Таких ужасов насмотрелись, когда сайтов много и разных появилось… И на KOI8-R, и с битым UTF-8, и без meta… Чего только не сделают вебмастера. Я к чему это — сделать более или менее работающий парсер, который все это учитывает (браузеры же учитывают как-то) — цены бы ему не было.
        • 0
          если требуется парсер под конкретный сайт или формат данных, тогда можно обойтись — я это имел в виду
    • 0
      Чуть ранее писал про работу с кодировками: habrahabr.ru/blogs/webdev/70903/#comment_2028492
  • +1
    Перечитал еще раз статью. Увидел «страницы должны быть в UTF-8».

    Cтраницы не обязаны быть в UTF-8. Насколько я помню свои опыты с DOMDocument — ваш код будет работать и на других кодировках входного HTML с условиями, описанными в моем комментарии выше. DOMDocument, насколько я помню (поправьте меня, если я не прав) сам узнает кодировку из тега meta и преобразует уже к UTF-8 все на выходе.
  • +1
    Я правильно понял, что это обертка над DomDocument и XPath которая позволяет использовать CSS селекторы?
    Т.е. полезна тем, кто XPath не осилил или еще кому-то?
    • 0
      да, и это тоже ) зачем знать в деталях другой формат запроса, когда уже знаешь один.
      а помимо — автоматическое создание domDocument, domXpath, отключение ошибок. без этого придется каждый раз все писать самому
  • +1
    Спасибо. Поупражнялся с библиотекой, и сделал за 5 минут парсер подкастов Эха Москвы, с выводом их в OPML фид echo.den-rad.ru
    • 0
      А можно узнать как?
  • –1
    Хороший парсер, это когда, он работает в ритм со скоростью жесткого диска как минимум :-)
    За проделанную работу спасибо в любом случае, также было-бы классно если-бы оба пожелания выполнялись однозначно.

    Думаю найдутся умельцы которые сделают это :-), по крайней мере я очень на это надеюсь.
  • 0
    Если у кого проблемы с кодировками (после вывода текста он превращается в месиво из &#XX; ) — надо сначала его прогнать через mb_convert_encoding.

    $html = mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8");

    И только потом отдавать в nokogiri
  • 0
    Отличная штука. только обнаружился небольшой баг:
    например, надо распарсить такую структуру:
    <table><tr><td>some text<a href='#'>some link</a></td></tr></table>


    $html = "<table><tr><td>some text<a>some link</a><a>some link</a></td></tr></table>";
    
    $saw = new nokogiri($html);
    
    $tmp = $saw->get('table')->toArray();
    print_r($tmp);
    


    И получим такой дамп:

        [tr] => Array
            (
                [td] => Array
                    (
                        [a] => Array
                            (
                                [0] => Array
                                    (
                                        [href] => #
                                        [#text] => some link
                                    )
     
                            )
     
                    )
     
            )


    А куда делся 'some text'?

    Вскрыие показало, что проблема в этом коде:

    if ($node->hasChildNodes()){
                if ($node->childNodes->length == 1){
                    $array[$node->firstChild->nodeName] = $this->toArray($node->firstChild);
                }else{
                    foreach ($node->childNodes as $childNode){
                        //if ($childNode->nodeType != XML_TEXT_NODE){
                            $array[$childNode->nodeName][] = $this->toArray($childNode);
                       // }
                    }
                }
            }
    


    Пофиксил комментированием условия:
    //if ($childNode->nodeType != XML_TEXT_NODE){


    Проблема вроде как решилась, только начались проблемы со скоростью. Что ж, исследуем дальше.
  • 0
    А чем регулярки плохи для парсинга?
    • 0
      сложнее составить, легко ломаются при изменении структуры
  • 0
    Автору громадный респект за библиотеку. На больших объемах Simple HTML DOM Parser съедает всю оперативку, за что и был выброшен в утиль. Библиотека автора была использована взамен вышеупомянутого парсера. В итоге при меньшем потреблении ресурсов — больше производительность.

    Были правда небольшие баги в библиотеке. Например были вот такие предупреждения:


    PHP Notice: Undefined index: id in ../parser/html_parser.php on line 64
    PHP Notice: Undefined index: attr in ../parser/html_parser.php on line 65
    PHP Notice: Undefined index: value in ../parser/html_parser.php on line 66
    PHP Notice: Undefined index: class in ../parser/html_parser.php on line 67


    Была пофиксена одна функция. Надеюсь будет полезно и автор внесет в следующий релиз.

    Стало
    protected function getXpathSubquery($expression){
    $query = '';
    if (preg_match("/(?P[a-z0-9]+)?(\[(?P\S+)=(?P\S+)\])?(#(?P\S+))?(\.(?P\S+))?/ims", $expression, $subs)){
    if (array_key_exists ('tag', $subs) ) {
    $tag = $subs['tag'];
    if (!strlen($tag))
    $tag = '*';
    }
    $query = '//'.$tag;

    if (array_key_exists ('id', $subs) ){
    $id = $subs['id'];
    if (strlen($id)){
    $query .= "[@id='".$id."']";
    }
    }
    if (array_key_exists ('attr', $subs) ){
    $attr = $subs['attr'];
    if (strlen($attr)){
    $query .= "[@".$attr."='".$attrValue."']";
    }
    }
    if (array_key_exists ('value', $subs) ) $attrValue = $subs['value'];
    if (array_key_exists ('class', $subs) ){
    $class = $subs['class'];
    if (strlen($class)){
    //$query .= "[@class='".$class."']";
    $query .= '[contains(concat(" ", normalize-space(@class), " "), " '.$class.' ")]';
    }
    }
    }
    return $query;
    }


    Было

    protected function getXpathSubquery($expression){
    $query = '';
    if (preg_match("/(?P[a-z0-9]+)?(\[(?P\S+)=(?P\S+)\])?(#(?P\S+))?(\.(?P\S+))?/ims", $expression, $subs)){
    $tag = $subs['tag'];
    $id = $subs['id'];
    $attr = $subs['attr'];
    $attrValue = $subs['value'];
    $class = $subs['class'];
    if (!strlen($tag))
    $tag = '*';
    $query = '//'.$tag;
    if (strlen($id)){
    $query .= "[@id='".$id."']";
    }
    if (strlen($attr)){
    $query .= "[@".$attr."='".$attrValue."']";
    }
    if (strlen($class)){
    //$query .= "[@class='".$class."']";
    $query .= '[contains(concat(" ", normalize-space(@class), " "), " '.$class.' ")]';
    }
    }
    return $query;
    }
    • 0
      спасибо за замечание, пожалуйста проверьте версию на гитхабе (обновление внизу топика)
      • 0
        Сдается мне что ссылка должна быть чтото вроде github.com/olamedia/nokogiri
        А не такой длинной на какой то другой проект и сразу на файл.
  • 0
    Подскажите пожалуйста, как в Nokogiri получить родительскую ноду внутри foreach? Что не пробовал только массив получаю.

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