Здравствуйте.
Представляю вам заключительную главу цикла. В ней пойдет речь о реализации самого парсера, его модулей, вроде функции анализа, построения стека и dom дерева. Помимо этого поговорим и об обработке комментариев. Как оказалось, комментарии могут обрабатываться по разному.
Напишем свой поиск элементов, подробнее поговорим о поиске по классам и идентификаторам. И многое другое!
Введение
Если вы не знакомы с устройством html парсеров и не прочитали предыдущею главу, рекомендую почитать. Я обновил в ней картинки, так что теперь воспринимать информацию там приятнее.
В этой статье будет введен новый ряд терминов:
- Цепочка — последовательность любых символов.
- Состояние — условие, от которого будет зависеть дальнейшая обработка цепочки парсером.
- Токен — массив, содержащий информацию о теге, комментарии или тексте. Также токен часто называют “Словом”.
- Стек — массив, содержащий токены.
- Разграничительный символ — символ, после которого следующая цепочка обрабатывается как новая цепочка.
- Алфавит — список символов, которые имеют первостепенную важность для парсера.
Если термины из списка вам не понятны, не волнуйтесь: в контексте все станет ясно.
Исходный код доступен на github.
Главные переменные парсера
Перед тем как писать парсер, следует определится с главными переменными, чтобы потом отталкиваться от них при дальнейшем написании.
А их будет четыре:
- $__SOURCE_TEXT — Содержит в себе текст исходного документа.
- $__DOM — Содержит в себе полученный в результате парсинга массив с dom деревом.
- $__ENABLE_COMMENTS — Означает, включена ли функция отображения комментариев или нет.
- $__ESCAPE_SYMBOLS — Массив со специальными символами и пробелом.
- $__MANDATORY_OPEN_ELEMENTS — Массив из четырех ячеек, обозначающие наличие обязательных открывающих тегов в документе.
- $__MANDATORY_CLOSE_ELEMENTS — Массив из трёх ячеек, обозначающие наличие обязательных закрывающих тегов в документе.
Класс парсера и его конструктор
Для того, чтобы потом было легче добавлять функционал, напишем класс парсера. В начале там будет функция отправки запросов, но постепенно туда будет записываться новый функционал.
Давайте кратко вспомним, как работает анализ
Как вы помните, в первой части статьи, работа парсера была разделена на два этапа. Так вот, в начале я буду говорить о первом этапе, об отделении текста от тегов.
Чтобы дальше было проще, первый этап я буду называть «Анализом».
Итак, как работает анализ в парсере. Сначала, он берет символ. Если этот символ равен "<", то он вызывает функцию, которая обработает следующую цепочку за этим символом как тег, в случаи если после "<" идет англоязычный символ. В ином случаи парсер обработает эту цепочку как текст.
После работы функция выдает токен, который отправляется в стек. Токен содержит в себе информацию о теге: его название, классы, идентификаторы и другие атрибуты.
Обобщим: анализатор видит "<", определяет что дальше идет тег, обрабатывает его создавая токен, и дальше отдает токен стеку.
Работа анализа «Изнутри»
Чтобы написать функцию анализа, стоит обратиться к теории конечных автоматов. Если говорить просто, то во время работы нашего алгоритма у него будет состояние. В зависимости от состояния, после того как алгоритм дошел до разграничительного символа, будет зависеть то, что он будет делать с полученной информацией. Например, если до пробела была цепочка, где в начале был символ "<", то после разграничительного символа вся цепочка кроме "<" будет обработана как название тега. Когда алгоритм нашел название тега, он начнет искать атрибуты. Значит нужно изменить состояние на поиск и обработку атрибута. И так далее. На этом и основывается алгоритм анализа.
Работа с кавычками в значении атрибута
Стоит об этом поговорить. Скорее всего вы знаете, что в html можно написать, например, такое:
<somebody class = '"main"'>
<somebody class = main>
И нельзя написать такое:
<somebody class = "main"">
<somebody class = m"ain">
И все эти ошибки нужно правильно обрабатывать. Для этого будет введено три переменных: счетчик кавычек, тип открывающей кавычки и переменная, означающая, находится ли значение атрибута в кавычках или оно было написано без них.
Счетчик кавычек, как понятно из названия, будет их считать. Пока количество кавычек не будет четным, парсер будет считать все символы значением атрибута. Если все правильно, значит значение атрибута будет записано в токен.
Тип открывающей кавычки будет означать, какая изначально кавычка была использована при написании. Значение атрибута не будет записано, пока не будет найдена закрывающая кавычка, тип которой совпадает с изначальной.
Обязательные и необязательные теги в документе
Скорее всего вы знаете, что на сайте, написанном на html всегда должны быть определенные теги. Если быть точнее, то всего три: <html>, <head> и <body>. Если таковых нет в документе, либо присутствуют только закрывающие или открывающие обязательные теги, они ставятся в определенных местах. Давайте посмотрим, в каких:
- <html> — Этот тег ставиться в конце и начале документа(после <!DOCTYPE html>), если после него не идет комментарий. В ином случаи <html> поставиться ровно после комментария.
- <head> — Ставится в месте, где есть теги <link>, <meta>, <style>, <script> и <title>. Если потом в документе будут встречаться такие же теги, в <head> они занесены не будут.
- <body> — ставится после head и до закрывающего <html>.
Также стоит упомянуть, что помимо обязательных тегов в документе еще должна быть декларация типа документа — <!DOCTYPE html>. В принципе, в нашем скрипте она практически никакой роли играть не будет, но в любом случаи стоит предусмотреть её вставку при необходимости.
Помимо обязательных открывающий тегов в html присутствуют еще и необязательные открывающие теги. Их несколько:
- <tbody> — Ставится после закрывающего <thead>, либо если такового тега нет перед контентом таблицы.
- <colgroup> — ставиться перед <col>.
Теперь давайте посмотрим на реализацию. Для начала, нужно как-то узнать, есть ли нужные теги в документе, чтобы потом не добавлять лишние теги.
Эта часть кода взята из функции анализа, дальше в статье вы увидите, где она будет находиться.
Вы могли заметить, что в проверке не участвуют теги <tbody> и <colgroup>. Они не являются обязательными, более того, их вставка может понадобится несколько раз в документе, поэтому их наличие и вставка будут проверяться динамически, во время выполнения функции вставки тегов.
После того, как мы разобрались со способом нахождения обязательных тегов в документе, нужно написать функцию, которая будет их вставлять по мере надобности. Так как с условиями вставки обязательных тегов мы разобрались, давайте поговорим об алгоритме вставки <tbody> и <colgroup>. Все будет работать очень просто: так как эти теги встречаются в документе только в теге <table>, то сначала функция будет проверять, есть ли такой тег в документе или нет. Если да, то она начинает цикл, в котором будут происходить следующие действия: сначала, функция должна будет проверить наличие тега <colgroup> в таблице. Если он есть, то добавлять еще один такой же тег бессмысленно. Если тег <colgroup> закрыт, то алгоритм начинает заново проверять наличие этого тега где-либо еще в таблице. Параллельно с этим функция также будет проверять наличие контента в таблице. Если контент в таблице присутствует и находится в тегах <tbody> или <thead>, то вставлять <tbody> смысла нет. Если же нет, то алгоритм вставит этот тег в нужное место в документе. Вот и все, давайте приступать к коду.
Необязательные закрывающие теги
В спецификации html говорится, что некоторым тегам при определенных условиях закрывающие теги не нужны. Давайте реализуем данный механизм и в нашем парсере.
Но сначала давайте определим, при каких условиях можно не писать закрывающий тег. Эти условия в основном одни и те же для большинства тегов, так что будет руководствоваться исходя из них. Есть еще пара тегов с иными условиями, о них поговорим отдельно.
Давайте взглянем на эти условия:
Закрывающий тег elem может быть опущен, если следующий тег за ним elem или если в родительском элементе нет больше элементов.
Elem в данном контексте обозначает тег, закрывающей тег которого необязателен. Он может быть один, а может быть и целая группа тегов(<dd>,<dt> и т.п).
Также стоит помнить, что у некоторый тегов есть, как я его назвал, подуровень. Например, тег <li> может находиться в тегах <ul> или <ol>, и это нужно иметь в виду. Чтобы все стало ясно, давайте посмотрим на пример:
<li>
<ul>
<li>
</ul>
</li>
Теперь давайте посмотрим что будет, если считать что все теги li находятся на “одном” уровне:
<li>
<ul>
</li>
<li></li>
</ul>
Это, очевидно, неправильное поведение. А вот что будет если допустить существование «подуровней»:
<li>
<ul>
<li></li>
</ul>
</li>
Мне кажется, этот наглядный пример должен был донести смысл моих слов.
Теперь давайте напишем простой скрипт для подобной обработки только для тега li и ul.
Как вы видите, кода слишком много, и если мы задумаемся написать под каждый такой тег такие условия(а их больше десятка), то конечная функция по размерам будет как сам парсер. Так что с этим нужно разобраться. Вместо написания одних и тех же условий для каждого такого тега, лучше написать одну гибкую функцию сразу для всех. Она значительно облегчит навигацию по коду, но будет гораздо сложнее метода разработки условий под каждый тег отдельно. Перед тем как показать код, давайте спроектируем алгоритм ее работы. Для начала, нужно определится с тем, где именно функция будет выполняться. Будет лучше, если функция будет выполнятся перед началом работы рекурсии, чтобы потом не было проблем с зависимостями в массиве dom дерева. Поэтому функция будет выполняться сразу после работы функции построения стека. Так как тегов с необязательными закрывающими тегами много, стоит сделать массив со всеми такими тегами, а также с тегами «подуровня»(<ul>, <ol>), и с тегами, перед которыми нужно ставить закрывающий тег(<li>, <dd>, <dt> и д.р).
После того, как мы разобрались с основными моментами, стоит поговорит о самом алгоритме работы функции. Сначала функция будет брать токен. Далее она проверяет, была ли функция вызвана собой же, или она выполняется впервые. Если она вызвана собой же, значит токен в данный момент находится в подуровне и дальнейшие элементы стоит обрабатывать только в контексте подуровня. Если нет, значит это первый вызов функции, и токены нужно обрабатывать уже без контекста.
Далее, давайте посмотрим, как происходит обработка токенов. После того, как функция поняла причину своего вызова, она берет токен и пытается понять, относится ли название тега в токене к названиям тегов в массиве функции. Если какое-либо название из массива подошло, начинается цикл, в котором будут искаться другие теги, которые могут быть связаны с найденным изначально. Они могут быть тегами подуровня, либо тегами, перед которыми нужно ставить закрывающий тег. Если найденный во время цикла тег является тегом подуровня, функция вызывает саму себя, задавая точку, где она остановилась а также говорит вызываемой функции, что та ее вызвала. Дальше вызванная функция обрабатывает теги в контексте подуровня, в результате выдавая точку завершения работы а также обработанный стек. После всего этого, функция дальше ищет подходящие теги на обработку. Если же был найден тег, перед которым нужно поставить закрывающий тег, функция сначала проверяет, не является ли этот тег закрывающим а также были ли такие же теги до этого. Благодаря таким проверкам можно избавиться от лишних закрывающих тегов.
В принципе, это все. Остались только некоторые различия в обработке в контексте подуровня и в бесконтекстной обработке. Но они не слишком сильно отличаются, так что понять различия можно будет при просмотре кода.
Caption и colgroup
Теперь давайте поговорим про теги с иными условиями, благо их всего два — <caption> и <colgroup>. В спецификации сказано, что закрывающий тег <caption> можно опустить только если за ним следует пробельный символ. Но так как в нашем парсере пробельные символы опускаются, мы условимся, что в теге <caption> не допускается наличие любых других тегов. С <colgroup> несколько иначе. Спецификация говорит, что закрывающий тег <colgroup> может быть опущен если после него нету пробельного символа или комментария. Но тут ситуация неоднозначная, так как наш парсер пропускает все пробельные символы после тегов. Так что мы условимся, что в <colgroup> могут быть только теги <col> и как только парсер найдет другой тег, он сразу же поставит перед ним закрывающий тег.
Раз закончили, давайте смотреть на код.
Дополнительные функции анализа
Эта функция проверяет, является ли тег одиночным. Если да, то выводит true.
Эта функция необходима, чтобы в случаи если перед нужным символом стоит пробел, он пропускался. Понадобится в случаи, если между нужными цепочками будет много пробелов.
Функция анализа
Для начала создадим алфавит с символами, имеющими первостепенную важность для анализа. А этих символов будет семь:
- "<" — Говорит о том, что дальше идет название тега.
- "/" — Говорит о том, что тег является закрывающим.
- ">" — Говорит о том, что дальше смысла в обработке нет, тег закрыт.
- "=" — Говорит о том, что дальше будет идти значение атрибута.
- "'" — Говорит о том, что дальше идет значение атрибута.
- """ — Имеет то же значение, что и прошлый вариант.
- " " — Разграничительный символ.
Стоит учитывать то, что любой из этих символов может встречаться в значении атрибута. Любой другой символ, не попадающий в этот список будет обрабатываться в зависимости от состояния.
Теперь давайте определимся, сколько состояний будет у алгоритма анализа. А их будет всего четыре:
- «attribute» — означает, что цепочка обрабатывается как атрибут.
- «attribute_value» — означает, что цепочка обрабатывается как значение атрибута.
- «tag» — означает, что цепочка обрабатывается как название тег.
- «attribute_value_starting» — означает, что следующая цепочка будет обрабатываться как значение атрибута.
Хорошо, со всем разобрались. Можно приступать к коду.
Но что там по комментариям? Возможно вы спросите меня. Я не стал включать комментарии в эту функцию, банально потому, что это функция обработки тегов, когда у комментариев свой собственный синтаксис, и их нужно обрабатывать отдельной функцией. Об этом пойдет речь дальше.
Так, а теперь построение стека
Как строится стек я уже говорил ранее в первой главе. Так что нам осталось только реализовать этот алгоритм.
Но давайте немного отвлечемся. Комментарии. Казалось бы, чего сложного? Но не все так просто.
Посмотрите на комментарий ниже. Как, по вашему, должен такой комментарий обрабатываться?
<!--<-->
Если подумали, то посмотрите, как этот комментарий обрабатывает браузер и редактор.
Вот как видит комментарий браузер:
<!---->
Как будто там и не было ничего. При этом, "-->" выносится как текст, а не комментарий.
А вот как видит его редактор:
<!--<-->
Результат выглядит так же, как исходный пример. И назревает вопрос: как это обрабатывать? Я решил обрабатывать так же, как и редактор.
А как будем обрабатывать теги <script> и <style>? Все просто. У нас будет переменная, которая в случаи, если был найден любой из этих тегов, будет игнорировать последующие цепочки ровно до того момента, пока не будет найден закрывающий тег. При этом, любые ошибки, найденные внутри тега, будут также игнорироваться.
Теперь стоит поговорить о дополнительных функциях к этому алгоритму. Их будет немного.
Нужна, как понятно из названия, для обработки комментариев.
Пропускает все символы, указанные в параметрах функции в виде массива и выводит в результате строку без этих символов.
С дополнительными функциями разобрались. Давайте писать код.
Касательно «Костыля». Дело в том, что в алгоритме есть проверка, является ли текст пустым. Это сделано для того, чтобы после каждого токена не было пустых текстовых токенов с одними табами или другими специальными символами. Но '0' в строке php считает как пустую строку. Поэтому я добавил это дополнительно условие, чтобы если в тексте есть только нуль(не считая дополнительных символов вроде табов и пробелов), алгоритм его правильно обрабатывал.
Построение dom дерева
Половина парсера позади, что радует. Теперь давайте углубимся во вторую часть парсера. Для удобства, её я буду называть «рекурсией», так как, по сути, это одна большая рекурсия.
Как говорилось в первой части, проблемы две. Первая с переизбытком тегов, вторая — их недостаток. Для решения этих проблем будет массив, в котором будут ячейки. В этих ячейках будет всего два параметра: Название тега и количество открытых тегов с таким именем. Также у нас будет переменная, которая будет хранить название родительского тега. Смотрите как это работает: если название закрывающего тега не совпадает с родительским тегом, значит где-то ошибка. Чтобы определить, какая именно ошибка, алгоритм будет смотреть массив со всеми открытыми тегами. Если один из тегов в массиве не был закрыт и он совпадает с текущим закрывающим тегом, значит нужно перед этим тегом поставить закрывающий тег родителя. В ином случаи значит что этот закрывающий тег лишний.
Перед написанием кода следует поговорить о построении массива с открытыми тегами. Строится он очень просто: в массиве будет несколько ячеек, в каждой из которой будет по два параметра — название тега и количество открытых таких тегов. Для поиска определенного тега с его параметрами в массиве будет функция, которая будет возвращать количество открытых тегов с названием, указанным в аргументе функции. Если ячейки с таким тегом не будет, тогда парсер создаст новую ячейку с этим тегом.
Что ж, раз со всем разобрались, давайте приступать к коду.
Итак, парсер готов. Как выглядит результат я думаю вы уже знаете, если читали прошлую главу. Остался только поиск. Давайте смотреть.
Поиск элементов
Как вы узнали из предыдущей главы, с поиском есть определенные сложности. Во-первых, поиск по классу и идентификатору отличаются от поиска по тегу. Во-вторых, нам нужно реализовать функцию поиска определенного элемента. Ну а третья причина состоит в том, что если мы захотим найти любой элемент в большом документе, то массив с результатами поиска будет огромен. Это происходит потому, что все дочерние токены находятся в подмассиве родительского токена, при этом во время поиска все неподходящие элементы будут пропускаться, но подходящие элементы из массива убираться не будут, сохраняя при этом вложенность. Таким образом, у нас может получиться такая картина, когда чтобы дойти до любого токена нам потребуется перелопатить кучу вложенных массивов.
Поиск текста
Давайте поговорим про поиск текста.
Поиск будет работать очень просто: если дать функции отдельный токен текста, то он его выведет. Если же функция получит dom дерево, то она пройдется по все дочерним токенам и, если найдет в дереве один либо больше токенов текста, выведет их в виде массива.
Реализацию функции поиска текста вы сможете увидеть далее во время реализации классов поиска и дочернего элемента.
Работа поиска «Изнутри»
Как поверхностно работает поиск я объяснял в первой части цикла, так что давайте приступим непосредственно к реализации. Но перед этим стоит ввести некоторые переменные, с которыми будет работать алгоритм. А их будет пять:
- $__DOM — Содержит в себе массив с dom деревом, в котором впоследствии будет происходить поиск
- $__ELEMENT — Содержит в себе наименование элемента, который будет искать алгоритм(body,div и др).
- $__ELEMENT_TYPE — Означает тип элемента, который нужно искать(class, id, tag).
- $__ELEMENT_DOM — Содержит результаты поиска, чтобы в последующим, например, искать там дочерние элементы.
- $__ELEMENT_NUMBER — Означает номер элемента, который нужно найти.
Для начала напишем класс, в котором в будущем реализуем алгоритм поиска. В нем будет конструктор.
Отлично, класс написали. Теперь давайте поговорим про тонкости поиска.
Сначала давайте поговорим про то, как найти определенный элемент по счету. Он может быть первый, второй, или пятый. Для этого добавим дополнительную переменную
$point
. Она отображает, на каком найденном элементе по счету находится поиск. Если мы указали в аргументах функции после элемента, который нам нужно найти число n, то поиск будет искать ровно до того момента, пока найденный элемент не будет по счету n-ым. При этом счетчик будет увеличиваться когда будет найден указанный в аргументах функции элемент. Ну и конечно, в результате поиска будет только этот элемент и его подмассив, если он присутствует.
Раз со всем разобрались, давайте напишем поиск только по тегу, а потом уже по классу и идентификатору.
Окей, поиск по тегу написали. Теперь давайте посмотрим, как будет работать поиск по классу и идентификатору.
Как вы видите, алгоритм поиска по классу слабо отличается от поиска по тегу. С поиском идентификатора все тоже самое — просто измените все слова «Класс» на «Тег».
Теперь приступим к реализации. Эта функция будет вмещать в себе сразу поиск по трем параметрам: классу, тегу и идентификатору.
Поиск дочерних элементов
Сразу стоит сказать, что все найденные элементы для парсера являются дочерний элементами. То есть, если вы хотите найти в определенном элементе определенный дочерний элемент, сначала вам нужно сказать парсеру, в каком элементе вы хотите начать поиск. Если вы не хотите этого делать, вам стоит воспользоваться аргументом функции поиска для нахождения определенного элемента, тогда вам не придется использовать дополнительную функцию поиска дочернего элемента для указания элемента, где именно нужно вести поиск.
В целом, это все. Ничего сложного в написании класса нет.
Тесты
Вот мы и написали парсер! Теперь давайте проверим его в тестах. Тесты будут не только на производительность, но и на правильность выполнения. Давайте смотреть!
Правильность выполнение
Мы сделаем три теста на правильность, а также один дополнительный на «хорошего верстальщика». Первый будет для проверки того, как парсер добавляет недостающие теги, второй будет на правильность удаления лишних, а третий на правильную обработку тегов, закрывающий тег которым не обязательно нужен. Последний тест можно считать бесполезным, он скорее просто интересен сам по себе. Начнем с первого теста.
Тест на недостающие теги
Для теста возьмем вот такой html:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="1">
<link rel="stylesheet" href="2">
<link rel="stylesheet" href="3">
<link rel="stylesheet' href="4>
</head>
<body>
<div>
0
<div id = 'somebody'>
Привет!
<style>once told me, the world is gonna roll me</style>
<!-- А где </div>?-->
</div>
</body>
</html>
Как видно в примере, одного тега не хватает. Давайте посмотрим, какой результат выведет наш парсер:
Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 15
[tag] => !DOCTYPE
[html] => 1
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 23
[tag] => html
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 32
[tag] => head
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 68
[tag] => link
[rel] => stylesheet
[href] => 1
)
[1] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 104
[tag] => link
[rel] => stylesheet
[href] => 2
)
[2] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 140
[tag] => link
[rel] => stylesheet
[href] => 3
)
[3] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 167
[tag] => link
[rel] => stylesheet
)
[4] => Array
(
[is_closing] => 1
[is_singleton] =>
[pointer] => 177
[tag] => head
)
)
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 186
[tag] => body
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 195
[tag] => div
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] => 0
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 227
[tag] => div
[id] => Array
(
[0] => somebody
)
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] => Привет!
)
[1] => Array
(
[tag] => __COMMENT
[0] => <!-- А гле </div>?-->
)
[2] => Array
(
[is_closing] => 1
[is_singleton] =>
[pointer] => 348
[tag] => div
)
)
)
[2] => Array
(
[tag] => div //А вот и он
[is_closing] => 1
)
)
)
[1] => Array
(
[is_closing] => 1
[is_singleton] =>
[pointer] => 358
[tag] => body
)
)
)
[2] => Array
(
[is_closing] => 1
[is_singleton] =>
[pointer] => 367
[tag] => html
)
)
)
)
Отлично, парсер правильно обработал страницу.
Теперь давайте посмотрим на работу парсера, когда тегов больше, чем нужно.
Возьмем вот такой html:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="1">
<link rel="stylesheet" href="2">
<link rel="stylesheet" href="3">
<link rel="stylesheet">
</head>
<body>
<div>
</span>
</span>
</span>
</span>
</span>
</span>
</span>
</span>
<div id = 'somebody'>
Привет!
<style>once told me, the world is gonna roll me</style>
</span>
</span>
</span>
</span>
</span>
</span>
</span>
</span>
</div>
</body>
</html>
В этом html, как видно, верстальщик немного переборщил с элементами. Впрочем, неважно. Давайте посмотрим, как парсер обработает это:
Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 15
[tag] => !DOCTYPE
[html] => 1
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 23
[tag] => html
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 32
[tag] => head
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 68
[tag] => link
[rel] => stylesheet
[href] => 1
)
[1] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 104
[tag] => link
[rel] => stylesheet
[href] => 2
)
[2] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 140
[tag] => link
[rel] => stylesheet
[href] => 3
)
[3] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 167
[tag] => link
[rel] => stylesheet
)
[4] => Array
(
[is_closing] => 1
[is_singleton] =>
[pointer] => 177
[tag] => head
)
)
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 186
[tag] => body
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 195
[tag] => div
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 481
[tag] => div
[id] => Array
(
[0] => somebody
)
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] => Привет!
)
[1] => Array
(
[is_closing] => 1
[is_singleton] =>
[pointer] => 831
[tag] => div
)
)
)
[1] => Array
(
[tag] => div
[is_closing] => 1
)
)
)
[1] => Array
(
[is_closing] => 1
[is_singleton] =>
[pointer] => 841
[tag] => body
)
)
)
[2] => Array
(
[is_closing] => 1
[is_singleton] =>
[pointer] => 850
[tag] => html
)
)
)
)
Чисто, аккуратно и по красоте. Что ж, раз так, давайте приступим к следующему тесту.
Тест на хорошего верстальщика
Наверное, самый интересный тест. Навряд ли конечно кто-нибудь будет так писать, но проверить нужно.
Для этого теста возьмем вот такой html:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="1">
<link rel="stylesheet" href="2">
<link rel="stylesheet" href="3">
<link rel="stylesheet">
</head>
<body>
<div>
<div><span></div><p></span></p>
<div id = 'somebody'>
Привет!
<style>once told me, the world is gonna roll me</style>
</div>
</body>
</html>
А теперь давайте посмотрим на вывод парсера:
Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 15
[tag] => !DOCTYPE
[html] => 1
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 23
[tag] => html
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 33
[tag] => head
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 71
[tag] => link
[rel] => stylesheet
[href] => 1
)
[1] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 109
[tag] => link
[rel] => stylesheet
[href] => 2
)
[2] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 147
[tag] => link
[rel] => stylesheet
[href] => 3
)
[3] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 176
[tag] => link
[rel] => stylesheet
)
[4] => Array
(
[is_closing] => 1
[is_singleton] =>
[pointer] => 187
[tag] => head
)
)
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 197
[tag] => body
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 208
[tag] => div
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 221
[tag] => div
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 227
[tag] => span
[0] => Array
(
[0] => Array
(
[tag] => span
[is_closing] => 1
)
)
)
[1] => Array
(
[is_closing] => 1
[is_singleton] =>
[pointer] => 233
[tag] => div
)
)
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 236
[tag] => p
[0] => Array
(
[0] => Array
(
[is_closing] => 1
[is_singleton] =>
[pointer] => 247
[tag] => p
)
)
)
[2] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 276
[tag] => div
[id] => Array
(
[0] => somebody
)
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] => Привет!
)
[1] => Array
(
[is_closing] => 1
[is_singleton] =>
[pointer] => 376
[tag] => div
)
)
)
[3] => Array
(
[tag] => div
[is_closing] => 1
)
)
)
[1] => Array
(
[is_closing] => 1
[is_singleton] =>
[pointer] => 387
[tag] => body
)
)
)
[2] => Array
(
[is_closing] => 1
[is_singleton] =>
[pointer] => 396
[tag] => html
)
)
)
)
Наверное, это правильный результат выполнение. Наверное.
Тесты на необязательные теги
После того, как разобрались с основным механизмом исправления ошибок в парсере, давайте поговорим про дополнительный. Для этого я возьму такой код:
<table>
<caption>37547 TEE Electric Powered Rail Car Train Functions (Abbreviated)
<colgroup><col><col><col>
<thead>
<tr>
<th>Function
<th>Control Unit
<th>Central Station
<tbody>
<tr>
<td>Headlights
<td>
<td>
<tr>
<td>Interior Lights
<td>
<td>
<tr>
<td>Electric locomotive operating sounds
<td>
<td>
<tr>
<td>Engineer’s cab lighting
<td>
<td>
<tr>
<td>Station Announcements - Swiss
<td>
<td>
</table>
Давайте посмотрим на результат:
[0] => Array
(
[tag] => !DOCTYPE
[html] => 1
[is_singleton] => 1
)
[1] => Array
(
[tag] => html
[is_singleton] =>
[is_closing] =>
[0] => Array
(
[0] => Array
(
[tag] => head
[is_singleton] =>
[is_closing] =>
[0] => Array
(
[0] => Array
(
[tag] => head
[is_singleton] =>
[is_closing] => 1
)
)
)
[1] => Array
(
[tag] => body
[is_singleton] =>
[is_closing] =>
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 7
[tag] => table
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 18
[tag] => caption
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] => 37547 TEE Electric Powered Rail Car Train Functions (Abbreviated)
)
[1] => Array
(
[tag] => caption
[is_closing] => 1
)
)
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 95
[tag] => colgroup
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 100
[tag] => col
)
[1] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 105
[tag] => col
)
[2] => Array
(
[is_closing] =>
[is_singleton] => 1
[pointer] => 110
[tag] => col
)
[3] => Array
(
[tag] => colgroup
[is_closing] => 1
)
)
)
[2] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 119
[tag] => thead
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 125
[tag] => tr
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 133
[tag] => th
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] => Function
)
[1] => Array
(
[tag] => th
[is_closing] => 1
)
)
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 149
[tag] => th
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] => Control Unit
)
[1] => Array
(
[tag] => th
[is_closing] => 1
)
)
)
[2] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 169
[tag] => th
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] => Central Station
)
[1] => Array
(
[tag] => th
[is_closing] => 1
)
)
)
[3] => Array
(
[tag] => tr
[is_closing] => 1
)
)
)
[1] => Array
(
[tag] => thead
[is_closing] => 1
)
)
)
[3] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 193
[tag] => tbody
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 199
[tag] => tr
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 207
[tag] => td
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] => Headlights
)
[1] => Array
(
[tag] => td
[is_closing] => 1
)
)
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 225
[tag] => td
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] =>
)
[1] => Array
(
[tag] => td
[is_closing] => 1
)
)
)
[2] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 236
[tag] => td
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] =>
)
[1] => Array
(
[tag] => td
[is_closing] => 1
)
)
)
[3] => Array
(
[tag] => tr
[is_closing] => 1
)
)
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 245
[tag] => tr
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 253
[tag] => td
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] => Interior Lights
)
[1] => Array
(
[tag] => td
[is_closing] => 1
)
)
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 276
[tag] => td
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] =>
)
[1] => Array
(
[tag] => td
[is_closing] => 1
)
)
)
[2] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 287
[tag] => td
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] =>
)
[1] => Array
(
[tag] => td
[is_closing] => 1
)
)
)
[3] => Array
(
[tag] => tr
[is_closing] => 1
)
)
)
[2] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 296
[tag] => tr
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 304
[tag] => td
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] => Electric locomotive operating sounds
)
[1] => Array
(
[tag] => td
[is_closing] => 1
)
)
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 348
[tag] => td
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] =>
)
[1] => Array
(
[tag] => td
[is_closing] => 1
)
)
)
[2] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 359
[tag] => td
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] =>
)
[1] => Array
(
[tag] => td
[is_closing] => 1
)
)
)
[3] => Array
(
[tag] => tr
[is_closing] => 1
)
)
)
[3] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 368
[tag] => tr
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 376
[tag] => td
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] => Engineer’s cab lighting
)
[1] => Array
(
[tag] => td
[is_closing] => 1
)
)
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 409
[tag] => td
[0] => Array
(
[0] => Array
(
[tag] => td
[is_closing] => 1
)
)
)
[2] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 417
[tag] => td
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] =>
)
[1] => Array
(
[tag] => td
[is_closing] => 1
)
)
)
[3] => Array
(
[tag] => tr
[is_closing] => 1
)
)
)
[4] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 426
[tag] => tr
[0] => Array
(
[0] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 434
[tag] => td
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] => Station Announcements - Swiss
)
[1] => Array
(
[tag] => td
[is_closing] => 1
)
)
)
[1] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 471
[tag] => td
[0] => Array
(
[0] => Array
(
[tag] => td
[is_closing] => 1
)
)
)
[2] => Array
(
[is_closing] =>
[is_singleton] =>
[pointer] => 479
[tag] => td
[0] => Array
(
[0] => Array
(
[tag] => __TEXT
[0] =>
)
[1] => Array
(
[tag] => td
[is_closing] => 1
)
)
)
[3] => Array
(
[tag] => tr
[is_closing] => 1
)
)
)
[5] => Array
(
[tag] => tbody
[is_closing] => 1
)
)
)
[4] => Array
(
[is_closing] => 1
[is_singleton] =>
[pointer] => 492
[tag] => table
)
)
)
[1] => Array
(
[tag] => body
[is_closing] => 1
)
)
)
[2] => Array
(
[tag] => html
[is_closing] => 1
)
)
)
)
Добавились как и закрывающие теги, вроде <colgroup> или <caption>, так и обязательные теги <html>, <head> и <body>.
Тесты производительности
Внимание! Все тесты производительности будут происходить с заранее скачанными документами.
Что ж, раз с этим разобрались, давайте приступать. Для теста возьмем мою первую публикацию, «Как я html-парсер на php писал, и что из этого вышло». Будем искать название публикации, а также показатель плюсиков. Начнем сначала со второго.
Для того, чтобы найти этот показатель, я буду использовать такой код:
$some->find('.voting-wjt__counter')->children(0)->children(0)->plainText();
Такой код выводит результат выполнения в браузере за ~660ms.
Теперь давайте найдем название публикации. Оно находится почти в самом верху, так что это будет быстрее. Для поиска я буду использовать такой код:
$some->find('.post__title-text')->children(0)->plainText();
Такой текст находится тоже за ~660ms.
Заключение
Странно, но иногда исходный код из paste.bin не показывается. Если у вас такая же проблема, то обновите страницу.
Спасибо Denai и vdem за указания на теги с необязательными закрывающими тегами.
Спасибо за внимание!
Автор: Арсений Романовский