Simple and fast HTML and XML parser

Muhammad Imangazaliev 8945208ab2 Fix warning "PHP Deprecated: mb_convert_encoding(): Handling HTML entities via mbstring is deprecated; use htmlspecialchars, htmlentities, or mb_encode_numericentity/mb_decode_numericentity instead" 1 year ago
src 8945208ab2 Fix warning "PHP Deprecated: mb_convert_encoding(): Handling HTML entities via mbstring is deprecated; use htmlspecialchars, htmlentities, or mb_encode_numericentity/mb_decode_numericentity instead" 1 year ago
tests 18ed017330 Rename Element::getDocument() to ownerDocument() 2 years ago
.gitattributes 0b2be7e8e7 Add a .gitattributes file with export-ignore rules 4 years ago
.gitignore f0de7229e5 Fix tests 2 years ago
.php_cs 2def7aedfc Update PHP CS Fixer config and run CS fix 7 years ago
.travis.yml 8945208ab2 Fix warning "PHP Deprecated: mb_convert_encoding(): Handling HTML entities via mbstring is deprecated; use htmlspecialchars, htmlentities, or mb_encode_numericentity/mb_decode_numericentity instead" 1 year ago
CHANGELOG.md cc8ec6ec92 Minor fixes in the changelog 2 years ago
LICENSE 5c082a9a69 Initial commit 9 years ago
README-RU.md acb5f1fdc7 Update the documentation 2 years ago
README.md acb5f1fdc7 Update the documentation 2 years ago
composer.json fa3855ddb5 Bump required PHP version to 7.2 2 years ago
composer.lock fa3855ddb5 Bump required PHP version to 7.2 2 years ago
phpunit.xml f02d343bf7 Fix Trvis CI issues 5 years ago

README-RU.md

DiDOM

Build Status Total Downloads Latest Stable Version License

DiDOM - простая и быстрая библиотека для парсинга HTML.

Содержание

Установка

Для установки DiDOM выполните команду:

composer require imangazaliev/didom

Быстрый старт

use DiDom\Document;

$document = new Document('http://www.news.com/', true);

$posts = $document->find('.post');

foreach($posts as $post) {
    echo $post->text(), "\n";
}

Создание нового документа

DiDom позволяет загрузить HTML несколькими способами:

Через конструктор
// в первом параметре передается строка с HTML
$document = new Document($html);

// путь к файлу
$document = new Document('page.html', true);

// или URL
$document = new Document('http://www.example.com/', true);

// также можно создать документ из DOMDocument
$domDocument = new DOMDocument();
$document = new Document($domDocument);

Сигнатура:

__construct($string = null, $isFile = false, $encoding = 'UTF-8', $type = Document::TYPE_HTML)

$isFile - указывает, что загружается файл. По умолчанию - false.

$encoding - кодировка документа. По умолчанию - UTF-8.

$type - тип документа (HTML - Document::TYPE_HTML, XML - Document::TYPE_XML). По умолчанию - Document::TYPE_HTML.

Через отдельные методы
$document = new Document();

$document->loadHtml($html);

$document->loadHtmlFile('page.html');

$document->loadHtmlFile('http://www.example.com/');

Для загрузки XML есть соответствующие методы loadXml и loadXmlFile.

При загрузке документа через эти методы, парсеру можно передать дополнительные опции:

$document->loadHtml($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$document->loadHtmlFile($url, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);

$document->loadXml($xml, LIBXML_PARSEHUGE);
$document->loadXmlFile($url, LIBXML_PARSEHUGE);

Поиск элементов

В качестве выражения для поиска можно передать CSS-селектор или XPath. Для этого в первом параметре нужно передать само выражение, а во втором - его тип (по умолчанию - Query::TYPE_CSS):

Через метод find():
use DiDom\Document;
use DiDom\Query;

...

// CSS-селектор
$posts = $document->find('.post');

// эквивалентно
$posts = $document->find('.post', Query::TYPE_CSS);

// XPath-выражение
$posts = $document->find("//div[contains(@class, 'post')]", Query::TYPE_XPATH);

Метод вернет массив с элементами (экземпляры класса DiDom\Element) или пустой массив, если не найден ни один элемент, соответствующий выражению.

При желании можно получить массив узлов без преобразования в Element или текст (DOMElement/DOMText/DOMComment/DOMAttr, в зависимости от выражения), для этого необходимо передать в качестве третьего параметра false.

Через метод first():

Возвращает первый найденный элемент или null, если не найдено ни одного элемента.

Принимает те же параметры, что и метод find().

Через магический метод __invoke():
$posts = $document('.post');

Принимает те же параметры, что и метод find().

Внимание: использование данного метода нежелательно, т.к. в будущем он может быть удален.

Через метод xpath():
$posts = $document->xpath("//*[contains(concat(' ', normalize-space(@class), ' '), ' post ')]");

Проверка наличия элемента

Проверить наличие элемента можно с помощью метода has():

if ($document->has('.post')) {
    // код
}

Если нужно проверить наличие элемента, а затем получить его, то можно сделать так:

if ($document->has('.post')) {
    $elements = $document->find('.post');

    // код
}

но быстрее так:

$elements = $document->find('.post');

if (count($elements) > 0) {
    // код
}

т.к. в первом случае выполняется два запроса.

Подсчет количества элементов

Метод count() позволяет подсчитать количество дочерних элементов, соотвествующих селектору:

// выведет количество ссылок в документе
echo $document->count('a');
// выведет количество пунктов в списке
echo $document->first('ul')->count('> li');

Поиск в элементе

Методы find(), first(), xpath(), has(), count() доступны также и для элемента.

Пример:

echo $document->find('nav')[0]->first('ul.menu')->xpath('//li')[0]->text();

Метод findInDocument()

При изменении, замене или удалении элемента, найденного в другом элементе, документ не будет изменен. Данное поведение связано с тем, что в методе find() класса Element (а, соответственно, и в методах first() и xpath) создается новый документ, в котором и производится поиск.

Для поиска элементов в исходном документе необходимо использовать методы findInDocument() и firstInDocument():

// ничего не выйдет
$document->first('head')->first('title')->remove();

// а вот так да
$document->first('head')->firstInDocument('title')->remove();

Внимание: методы findInDocument() и firstInDocument() работают только для элементов, которые принадлежат какому-либо документу, либо созданых через new Element(...). Если элемент не принадлежит к какому-либо документу, будет выброшено исключение LogicException;

Поддерживамые селекторы

DiDom поддерживает поиск по:

  • тэгу
  • классу, идентификатору, имени и значению атрибута
  • псевдоклассам:

    • first-, last-, nth-child
    • empty и not-empty
    • contains
    • has

      // все ссылки
      $document->find('a');
      
      // любой элемент с id = "foo" и классом "bar"
      $document->find('#foo.bar');
      
      // любой элемент, у которого есть атрибут "name"
      $document->find('[name]');
      
      // эквивалентно
      $document->find('*[name]');
      
      // поле ввода с именем "foo"
      $document->find('input[name=foo]');
      $document->find('input[name=\'foo\']');
      $document->find('input[name="foo"]');
      
      // поле ввода с именем "foo" и значением "bar"
      $document->find('input[name="foo"][value="bar"]');
      
      // поле ввода, название которого НЕ равно "foo"
      $document->find('input[name!="foo"]');
      
      // любой элемент, у которого есть атрибут,
      // начинающийся с "data-" и равный "foo"
      $document->find('*[^data-=foo]');
      
      // все ссылки, у которых адрес начинается с https
      $document->find('a[href^=https]');
      
      // все изображения с расширением png
      $document->find('img[src$=png]');
      
      // все ссылки, содержащие в своем адресе строку "example.com"
      $document->find('a[href*=example.com]');
      
      // все ссылки, содержащие в атрибуте data-foo значение bar отделенное пробелом
      $document->find('a[data-foo~=bar]');
      
      // текст всех ссылок с классом "foo" (массив строк)
      $document->find('a.foo::text');
      
      // эквивалентно
      $document->find('a.foo::text()');
      
      // адрес и текст подсказки всех полей с классом "bar"
      $document->find('a.bar::attr(href|title)');
      
      // все ссылки, которые являются прямыми потомками текущего элемента
      $element->find('> a');
      

Изменение содержимого

Изменение HTML

$element->setInnerHtml('<a href="#">Foo</a>');

Изменение XML

$element->setInnerXml(' Foo <span>Bar</span><!-- Baz --><![CDATA[
    <root>Hello world!</root>
]]>');

Изменение значения (как простой текст)

$element->setValue('Foo');
// будет закодирован в HTML-сущность как при вызове htmlentities()
$element->setValue('<a href="#">Foo</a>');

Вывод содержимого

Получение HTML

Через метод html():
// HTML-код документа
echo $document->html();

// HTML-код элемента
echo $document->first('.post')->html();
Приведение к строке:
// HTML-код документа
$html = (string) $document;

// HTML-код элемента
$html = (string) $document->first('.post');

Внимание: использование данного способа нежелательно, т.к. в будущем он может быть удален.

Форматирование HTML при выводе
echo $document->format()->html();

Метод format() отсутствует у элемента, поэтому, если нужно получить отформатированный HTML-код элемента, необходимо сначала преобразовать его в документ:

$html = $element->toDocument()->format()->html();

Внутренний HTML

$innerHtml = $element->innerHtml();

Метод innerHtml() отсутствует у документа, поэтому, если нужно получить внутренний HTML-код документа, необходимо сначала преобразовать его в элемент:

$innerHtml = $document->toElement()->innerHtml();

Получение XML

// XML-код документа
echo $document->xml();

// XML-код элемента
echo $document->first('book')->xml();

Получение содержимого

Возвращает текстовое содержимое узла и его потомков:

echo $element->text();

Создание нового элемента

Создание экземпляра класса

use DiDom\Element;

$element = new Element('span', 'Hello');

// выведет "<span>Hello</span>"
echo $element->html();

Первым параметром передается название элемента, вторым - его значение (необязательно), третьим - атрибуты элемента (необязательно).

Пример создания элемента с атрибутами:

$attributes = ['name' => 'description', 'placeholder' => 'Enter description of item'];

$element = new Element('textarea', 'Text', $attributes);

Элемент можно создать и из экземпляра класса DOMElement:

use DiDom\Element;
use DOMElement;

$domElement = new DOMElement('span', 'Hello');
$element = new Element($domElement);

Изменение элемента, созданного из DOMElement

Экземпляры класса DOMElement, созданные через конструктор (new DOMElement(...)), являются неизменяемыми, поэтому и элементы (экземпляры класса DiDom\Element), созданные из таких объектов, так же являются неизменяемыми.

Пример:

$element = new Element('span', 'Hello');

// добавит атрибут "id" со значением "greeting"
$element->attr('id', 'greeting');

$domElement = new DOMElement('span', 'Hello');
$element = new Element($domElement);

// будет выброшено исключение
// DOMException with message 'No Modification Allowed Error'
$element->attr('id', 'greeting');

С помощью метода Document::createElement()

$document = new Document($html);

$element = $document->createElement('span', 'Hello');

С помощью CSS-селектора

Первый параметр - селектор, второй - значение, третий - массив с атрибутами.

Атрибуты элемента могут быть указаны как в селекторе, так и переданы отдельно в третьем параметре.

Если название атрибута в массиве совпадает с названием атрибута из селектора, будет использовано значение, указанное в селекторе.

$document = new Document($html);

$element = $document->createElementBySelector('div.block', 'Foo', [
    'id' => '#content',
    'class' => '.container',
]);

Можно так же использовать статический метод createBySelector класса Element:

$element = Element::createBySelector('div.block', 'Foo', [
     'id' => '#content',
     'class' => '.container',
 ]);

Получение названия элемента

$element->tagName();

Получение родительского элемента

$element->parent();

Так же можно получить родительский элемент, соответствующий селектору:

$element->closest('.foo');

Вернет родительский элемент, у которого есть класс foo. Если подходящий элемент не найден, метод вернет null.

Получение соседних элементов

Первый аргумент - CSS-селектор, второй - тип узла (DOMElement, DOMText или DOMComment).

Если оба аргумента опущены, будет осуществлен поиск узлов любого типа.

Если селектор указан, а тип узла нет, будет использован тип DOMElement.

Внимание: Селектор можно использовать только с типом DOMElement.

// предыдущий элемент
$item->previousSibling();

// предыдущий элемент, соответствующий селектору
$item->previousSibling('span');

// предыдущий элемент типа DOMElement
$item->previousSibling(null, 'DOMElement');

// предыдущий элемент типа DOMComment
$item->previousSibling(null, 'DOMComment');
// все предыдущие элементы
$item->previousSiblings();

// все предыдущие элементы, соответствующие селектору
$item->previousSiblings('span');

// все предыдущие элементы типа DOMElement
$item->previousSiblings(null, 'DOMElement');

// все предыдущие элементы типа DOMComment
$item->previousSiblings(null, 'DOMComment');
// следующий элемент
$item->nextSibling();

// следующий элемент, соответствующий селектору
$item->nextSibling('span');

// следующий элемент типа DOMElement
$item->nextSibling(null, 'DOMElement');

// следующий элемент типа DOMComment
$item->nextSibling(null, 'DOMComment');
// все последующие элементы
$item->nextSiblings();

// все последующие элементы, соответствующие селектору
$item->nextSiblings('span');

// все последующие элементы типа DOMElement
$item->nextSiblings(null, 'DOMElement');

// все последующие элементы типа DOMComment
$item->nextSiblings(null, 'DOMComment');

Получение дочерних элементов

$html = '<div>Foo<span>Bar</span><!--Baz--></div>';

$document = new Document($html);

$div = $document->first('div');

// элемент (DOMElement)
// string(3) "Bar"
var_dump($div->child(1)->text());

// текстовый узел (DOMText)
// string(3) "Foo"
var_dump($div->firstChild()->text());

// комментарий (DOMComment)
// string(3) "Baz"
var_dump($div->lastChild()->text());

// array(3) { ... }
var_dump($div->children());

Получение документа

$document = new Document($html);

$element = $document->first('input[name=email]');

$document2 = $element->ownerDocument();

// bool(true)
var_dump($document->is($document2));

Работа с атрибутами элемента

Создание/изменение атрибута

Через метод setAttribute:
$element->setAttribute('name', 'username');
Через метод attr:
$element->attr('name', 'username');
Через магический метод __set:
$element->name = 'username';

Получение значения атрибута

Через метод getAttribute:
$username = $element->getAttribute('value');
Через метод attr:
$username = $element->attr('value');
Через магический метод __get:
$username = $element->name;

Если атрибут не найден, вернет null.

Проверка наличия атрибута

Через метод hasAttribute:
if ($element->hasAttribute('name')) {
    // код
}
Через магический метод __isset:
if (isset($element->name)) {
    // код
}

Удаление атрибута:

Через метод removeAttribute:
$element->removeAttribute('name');
Через магический метод __unset:
unset($element->name);

Получение всех атрибутов:

var_dump($element->attributes());

Получение определенных атрибутов:

var_dump($element->attributes(['name', 'type']));

Удаление всех атрибутов:

$element->removeAllAttributes();

Удаление всех атрибутов, за исключением указанных:

$element->removeAllAttributes(['name', 'type']);

Сравнение элементов

$element  = new Element('span', 'hello');
$element2 = new Element('span', 'hello');

// bool(true)
var_dump($element->is($element));

// bool(false)
var_dump($element->is($element2));

Добавление дочерних элементов

$list = new Element('ul');

$item = new Element('li', 'Item 1');

$list->appendChild($item);

$items = [
    new Element('li', 'Item 2'),
    new Element('li', 'Item 3'),
];

$list->appendChild($items);

Замена элемента

$title = new Element('title', 'foo');

$document->first('title')->replace($title);

Внимание: заменить можно только те элементы, которые были найдены непосредственно в документе:

// ничего не выйдет
$document->first('head')->first('title')->replace($title);

// а вот так да
$document->first('head title')->replace($title);

Подробнее об этом в разделе Поиск в элементе.

Удаление элемента

$document->first('title')->remove();

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

// ничего не выйдет
$document->first('head')->first('title')->remove();

// а вот так да
$document->first('head title')->remove();

Подробнее об этом в разделе Поиск в элементе.

Работа с кэшем

Кэш - массив XPath-выражений, полученных из CSS.

Получение кэша

use DiDom\Query;

...

$xpath    = Query::compile('h2');
$compiled = Query::getCompiled();

// array('h2' => '//h2')
var_dump($compiled);

Установка кэша

Query::setCompiled(['h2' => '//h2']);

Прочее

preserveWhiteSpace

По умолчанию сохранение пробелов между тегами отключено.

Включать опцию preserveWhiteSpace следует до загрузки документа:

$document = new Document();

$document->preserveWhiteSpace();

$document->loadXml($xml);

matches

Возвращает true, если элемент соответсвует селектору:

// вернет true, если элемент это div с идентификатором content
$element->matches('div#content');

// строгое соответствие
// вернет true, если элемент это div с идентификатором content и ничего более
// если у элемента будут какие-либо другие атрибуты, метод вернет false
$element->matches('div#content', true);

isElementNode

Проверяет, является ли элемент узлом типа DOMElement:

$element->isElementNode();

isTextNode

Проверяет, является ли элемент текстовым узлом (DOMText):

$element->isTextNode();

isCommentNode

Проверяет, является ли элемент комментарием (DOMComment):

$element->isCommentNode();

Сравнение с другими парсерами

Сравнение с другими парсерами