11 апреля 2010 в 22:19

Парсинг (разбор) XML документов с помощью CSS селекторов перевод

Привет. Заметил что постов посвященных Symfony 2.0 все еще не много. Постараюсь это исправить в ближайшее время топиками и переводами про компоненты фреймворка. Сейчас же представляю вашему вниманию перевод статьи с блога Фабьена (Fabien Potencier) который всегда интересно читать. Перевод, возможно не всегда дословный, но смысл я старался передавать четко. Итак начнем.

— HTML и XML документы это как хлеб и масло для веб-разработчиков. День за днем вы, скорее всего, создаете множество HTML документов. И наверняка вам приходится парсить некоторые из них время от времени: потому что вы используете веб службы и хотите извлечь некоторую информацию, или потому, что вы хотите получить данные с нужных веб страниц, или просто потому, что хотите написать функциональные тесты для веб сайта. Получить содержимое достаточно просто, но как его разобрать, чтобы выделить нужную информацию?

PHP уже поставляется с большим количеством инструментов для парсинга XML документов: например, SimpleXML, DOM и XMLReader. Но как только вам нужно извлечь информацию глубоко зашитую в структуру документа, все не так легко, как и должно быть. Конечно же, XPath ваш лучший друг если вам нужно выбрать элементы, но кривая изучения очень крутая. Даже выражения, которые должны быть простыми, оказываются громоздкими. Для примера, вот XPath выражение для нахождения всех тегов h1 с классом «foo»:
h1[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]

Выражение получилось сложным, так как у тега может быть несколько классов:

<h1 class="foo">Foo</h1>
<h1 class="foo bar">Foo</h1>
<h1 class="foobar bar">Foo</h1>

Выражение должно выбирать первые два тега h1, но не третий.

Конечно же, все знают что сделать то же на css проще простого:
h1.foo

Для функциональных тестов в Symfony 2, я искал путь увеличения мощи и выразительности CSS селекторов при помощи средств, которые уже есть в PHP. Первая идея которая пришла мне на ум, это конвертировать CSS селектор в его XPath эквивалент. Но возможно ли это? Ответ скорее «Да».

John Resig написал в своем посте почти на ту же тему: «Самое главное понять, что CSS селекторы, часто, очень коротки, но крайне неэффективны по сравнению с XPath».

Написание «токенайзера», парсера, и компоновщика, способного преобразовывать CSS селекторы в XPath эквиваленты — не тривиальная задача. Поэтому, вместо изобретения колеса, я просмотрел существующие библиотеки. Уже очень скоро я наткнулся на lxml, библиотеку Python. Модуль lxml.cssselect библиотеки lxml делает то что нужно. Поэтому я потратил время на трансляцию кода с Python на язык PHP, добавил некоторые модульные тесты, и вуаля — родился компонент CSS селекторов для Symfony 2.

Для справки: в symfony 1 есть класс sfDomCssSelector, но он не конвертирует CSS селекторы в XPath. Он делает роботу хорошо, но ограничен очень простыми CSS селекторами и не может быть использован совместно со стандартными XML инструментами.

Компонент Symfony 2 CSS Selector делает только одну вещь, и пытается делать ее хорошо: конвертировать CSS селекторы в выражения XPath. Его использование очень простое:

use Symfony\Components\CssSelector\Parser;

$xpath = Parser::cssToXpath('h1.foo');


Теперь переменная $xpath содержит «h1[contains(concat(' ', normalize-space(@class), ' '), ' foo ')]».

Давайте приведем пример, как вы можете использовать компонент. Допустим вы хотите получить все названия постов и URL-ы моего блога (информация доступна по адресу fabien.potencier.org/articles).

use Symfony\Components\CssSelector\Parser;

$document = new \DOMDocument();
$document->loadHTMLFile('http://fabien.potencier.org/articles');

$xpath = new \DOMXPath($document);
foreach ($xpath->query(Parser::cssToXpath('div.item > h4 > a')) as $node)
{
 printf("%s (%s)\n", $node->nodeValue, $node->getAttribute('href'));
}

Код очень простой, и вместо использования выражения XPath, мы позволим классу парсера преобразовать для нас CSS селекторы в выражение XPath.

$xpath->query(Parser::cssToXpath('div.item > h4 > a'))

Помните, что если вы работаете с XML документами, вам нужно объявить используемые пространства имен. Давайте используем SimpleXMLElement, который понимает только правильно сформированные XML документы:

$document = new \SimpleXMLElement('http://fabien.potencier.org/articles', 0, true);
$document->registerXPathNamespace('xhtml', 'http://www.w3.org/1999/xhtml');
foreach ($document->xpath(Parser::cssToXpath('xhtml|div.item > xhtml|h4 > xhtml|a')) as $node)
{
 printf("%s (%s)\n", $node, $node['href']);
}

Как вы могли заметить, CSS селекторы поддерживают пространства имен (xhtml|div).

Этот новый CSS Selector компонент будет использоваться в Symfony 2 для функциональных тестов (но как вы увидите в ближайшие несколько недель, совсем иначе чем было с symfony 1).

Код компонента модульно протестирован с неплохим тестовым покрытием кода (code coverage), поэтому будьте свободны в его использовании (код есть на Github: github.com/fabpot/symfony в пространстве имен Symfony\Components\CssSelector) и оставляйте отзывы.
Автор оригинала: Fabien Potencier
skorney @skorney
карма
33,0
рейтинг 0,0
Похожие публикации

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

  • +3
    пользователям ZF 1.10 на заметку: обратите внимание на Zend_Dom. Внутреннее css-серектори идут через трансформацию их в xpath.
  • +1
    Правильно:
    h1[@class~='foo']
    • 0
      Согласен, h1[contains(concat(' ', normalize-space(@class), ' '), ' foo ')] можно свести к h1[@class='foo']. Хотя css-селекторы всё равно удобней.
      • 0
        Смотря для чего))
      • +1
        ну как сказать… XPath h1[@class='foo'] не выберет <h1 class=«foo bar»/>
        Из приведенного в статье выражения мне не понятно только зачем вставили normalize-space в данном выражении
        • 0
          Видимо для внушительности выражения, чтобы больше стимул был переходить на селекторы)
  • +1
    тоже подписан на обновления github/symfony.

    А еще есть code.google.com/p/phpquery/

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