Русский
Русский
English
Статистика
Реклама

Парсер

Как я html-парсер на php писал, и что из этого вышло. Заключительная часть

15.08.2020 18:06:03 | Автор: admin
Здравствуйте.
Представляю вам заключительную главу цикла. В ней пойдет речь о реализации самого парсера, его модулей, вроде функции анализа, построения стека и dom дерева. Помимо этого поговорим и об обработке комментариев. Как оказалось, комментарии могут обрабатываться по разному. Напишем свой поиск элементов, подробнее поговорим о поиске по классам и идентификаторам. И многое другое!

Введение


Если вы не знакомы с устройством html парсеров и не прочитали предыдущею главу, рекомендую почитать.Я обновил в ней картинки, так что теперь воспринимать информацию там приятнее.

В этой статье будет введен новый ряд терминов:

  • Цепочка последовательность любых символов.
  • Состояние условие, от которого будет зависеть дальнейшая обработка цепочки парсером.
  • Токен массив, содержащий информацию о теге, комментарии или тексте. Также токен часто называют Словом.
  • Стек массив, содержащий токены.
  • Разграничительный символ символ, после которого следующая цепочка обрабатывается как новая цепочка.
  • Алфавит список символов, которые имеют первостепенную важность для парсера.

Если термины из списка вам не понятны, не волнуйтесь: в контексте все станет ясно.

Исходный код доступен на github.

Главные переменные парсера


Перед тем как писать парсер, следует определится с главными переменными, чтобы потом отталкиваться от них при дальнейшем написании.
А их будет четыре:

  1. $__SOURCE_TEXT Содержит в себе текст исходного документа.
  2. $__DOM Содержит в себе полученный в результате парсинга массив с dom деревом.
  3. $__ENABLE_COMMENTS Означает, включена ли функция отображения комментариев или нет.
  4. $__ESCAPE_SYMBOLS Массив со специальными символами и пробелом.
  5. $__MANDATORY_OPEN_ELEMENTS Массив из четырех ячеек, обозначающие наличие обязательных открывающих тегов в документе.
  6. $__MANDATORY_CLOSE_ELEMENTS Массив из трёх ячеек, обозначающие наличие обязательных закрывающих тегов в документе.

Класс парсера и его конструктор


Для того, чтобы потом было легче добавлять функционал, напишем класс парсера. В начале там будет функция отправки запросов, но постепенно туда будет записываться новый функционал.

Класс парсера


Эта часть кода взята из функции анализа, дальше в статье вы увидите, где она будет находиться.

Вы могли заметить, что в проверке не участвуют теги <tbody> и <colgroup>. Они не являются обязательными, более того, их вставка может понадобится несколько раз в документе, поэтому их наличие и вставка будут проверяться динамически, во время выполнения функции вставки тегов.

После того, как мы разобрались со способом нахождения обязательных тегов в документе, нужно написать функцию, которая будет их вставлять по мере надобности. Так как с условиями вставки обязательных тегов мы разобрались, давайте поговорим об алгоритме вставки <tbody> и <colgroup>. Все будет работать очень просто: так как эти теги встречаются в документе только в теге <table>, то сначала функция будет проверять, есть ли такой тег в документе или нет. Если да, то она начинает цикл, в котором будут происходить следующие действия: сначала, функция должна будет проверить наличие тега <colgroup> в таблице. Если он есть, то добавлять еще один такой же тег бессмысленно. Если тег <colgroup> закрыт, то алгоритм начинает заново проверять наличие этого тега где-либо еще в таблице. Параллельно с этим функция также будет проверять наличие контента в таблице. Если контент в таблице присутствует и находится в тегах <tbody> или <thead>, то вставлять <tbody> смысла нет. Если же нет, то алгоритм вставит этот тег в нужное место в документе. Вот и все, давайте приступать к коду.

Функция вставки тегов


Как вы видите, кода слишком много, и если мы задумаемся написать под каждый такой тег такие условия(а их больше десятка), то конечная функция по размерам будет как сам парсер. Так что с этим нужно разобраться. Вместо написания одних и тех же условий для каждого такого тега, лучше написать одну гибкую функцию сразу для всех. Она значительно облегчит навигацию по коду, но будет гораздо сложнее метода разработки условий под каждый тег отдельно. Перед тем как показать код, давайте спроектируем алгоритм ее работы. Для начала, нужно определится с тем, где именно функция будет выполняться. Будет лучше, если функция будет выполнятся перед началом работы рекурсии, чтобы потом не было проблем с зависимостями в массиве dom дерева. Поэтому функция будет выполняться сразу после работы функции построения стека. Так как тегов с необязательными закрывающими тегами много, стоит сделать массив со всеми такими тегами, а также с тегами подуровня(<ul>, <ol>), и с тегами, перед которыми нужно ставить закрывающий тег(<li>, <dd>, <dt> и д.р).

После того, как мы разобрались с основными моментами, стоит поговорит о самом алгоритме работы функции. Сначала функция будет брать токен. Далее она проверяет, была ли функция вызвана собой же, или она выполняется впервые. Если она вызвана собой же, значит токен в данный момент находится в подуровне и дальнейшие элементы стоит обрабатывать только в контексте подуровня. Если нет, значит это первый вызов функции, и токены нужно обрабатывать уже без контекста.

Далее, давайте посмотрим, как происходит обработка токенов. После того, как функция поняла причину своего вызова, она берет токен и пытается понять, относится ли название тега в токене к названиям тегов в массиве функции. Если какое-либо название из массива подошло, начинается цикл, в котором будут искаться другие теги, которые могут быть связаны с найденным изначально. Они могут быть тегами подуровня, либо тегами, перед которыми нужно ставить закрывающий тег. Если найденный во время цикла тег является тегом подуровня, функция вызывает саму себя, задавая точку, где она остановилась а также говорит вызываемой функции, что та ее вызвала. Дальше вызванная функция обрабатывает теги в контексте подуровня, в результате выдавая точку завершения работы а также обработанный стек. После всего этого, функция дальше ищет подходящие теги на обработку. Если же был найден тег, перед которым нужно поставить закрывающий тег, функция сначала проверяет, не является ли этот тег закрывающим а также были ли такие же теги до этого. Благодаря таким проверкам можно избавиться от лишних закрывающих тегов.

В принципе, это все. Остались только некоторые различия в обработке в контексте подуровня и в бесконтекстной обработке. Но они не слишком сильно отличаются, так что понять различия можно будет при просмотре кода.

Caption и colgroup


Теперь давайте поговорим про теги с иными условиями, благо их всего два <caption> и <colgroup>. В спецификации сказано, что закрывающий тег <caption> можно опустить только если за ним следует пробельный символ. Но так как в нашем парсере пробельные символы опускаются, мы условимся, что в теге <caption> не допускается наличие любых других тегов. С <colgroup> несколько иначе. Спецификация говорит, что закрывающий тег <colgroup> может быть опущен если после него нету пробельного символа или комментария. Но тут ситуация неоднозначная, так как наш парсер пропускает все пробельные символы после тегов. Так что мы условимся, что в <colgroup> могут быть только теги <col> и как только парсер найдет другой тег, он сразу же поставит перед ним закрывающий тег.

Раз закончили, давайте смотреть на код.

Функция вставки закрывающий тегов


Эта функция проверяет, является ли тег одиночным. Если да, то выводит true.

Функция для пропуска пробелов


Но что там по комментариям? Возможно вы спросите меня. Я не стал включать комментарии в эту функцию, банально потому, что это функция обработки тегов, когда у комментариев свой собственный синтаксис, и их нужно обрабатывать отдельной функцией. Об этом пойдет речь дальше.

Так, а теперь построение стека


Как строится стек я уже говорил ранее в первой главе. Так что нам осталось только реализовать этот алгоритм.

Но давайте немного отвлечемся. Комментарии. Казалось бы, чего сложного? Но не все так просто.
Посмотрите на комментарий ниже. Как, по вашему, должен такой комментарий обрабатываться?

<!--<-->

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

<!---->

Как будто там и не было ничего. При этом, "-->" выносится как текст, а не комментарий.
А вот как видит его редактор:

<!--<-->

Результат выглядит так же, как исходный пример. И назревает вопрос: как это обрабатывать? Я решил обрабатывать так же, как и редактор.

А как будем обрабатывать теги <script> и <style>? Все просто. У нас будет переменная, которая в случаи, если был найден любой из этих тегов, будет игнорировать последующие цепочки ровно до того момента, пока не будет найден закрывающий тег. При этом, любые ошибки, найденные внутри тега, будут также игнорироваться.

Теперь стоит поговорить о дополнительных функциях к этому алгоритму. Их будет немного.

Функция для обработки комментариев


Пропускает все символы, указанные в параметрах функции в виде массива и выводит в результате строку без этих символов.

С дополнительными функциями разобрались. Давайте писать код.

Функция построения стека


Что ж, раз со всем разобрались, давайте приступать к коду.

Функция построения DOM дерева


Отлично, класс написали. Теперь давайте поговорим про тонкости поиска.

Сначала давайте поговорим про то, как найти определенный элемент по счету. Он может быть первый, второй, или пятый. Для этого добавим дополнительную переменную
$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>Engineers 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] => Engineers 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 за указания на теги с необязательными закрывающими тегами.
Спасибо за внимание!
Подробнее..

Перевод Используйте парсинг вместо контроля типов

15.01.2021 12:16:15 | Автор: admin


В 2019 году была написана потрясающая статья Parse, dont validate. Я крайне рекомендую изучить её всем программистам (а также недавнее дополнение к ней Names are not type safety). Её основная идея заключается в том, что существует два способа проверки валидности входящих данных функции:

  • Валидатор проверяет входящие данные на правильность и в случае их неправильности выдаёт ошибку. Он ничего не возвращает. Например, он может проверять, не пуст ли список.
  • Парсер делает то же самое, что и валидатор, но возвращает более конкретное представление входящих данных, обеспечивающее соответствие требуемого свойства. Например, он проверяет, не пуст ли список, и возвращает тип NonEmptyList.

Главное утверждение, сделанное в этой статье что парсеры предпочтительнее, чем валидаторы. Её основной посыл нужно сделать недопустимые состояния непредставимыми (unrepresentable). В статье это реализовано с помощью использования системы типов. Я полностью согласен с такой философией, но хотел бы выделить и более подробно обсудить один из ироничных аспектов аргументации:

Инструмент контроля типов является хрестоматийным примером валидатора!

Ведь в конечном итоге инструмент контроля типов получает на входе уже подвергнутое парсингу представление программы и бракует его, если не удаётся выполнить контроль типов. Он не возвращает более конкретного представления программы. (Не стоит путать это с выводом типа, который возвращает больше информации, но только касательно типов).

Какой же может быть альтернатива инструменту контроля типов в виде парсера для языка программирования?


Одним из решений может стать создание более конкретной грамматики, устраняющей возможность недопустимых состояний. Например, если определённая функция требует ввода числа в интервале от 1 до 5, то вместо такого обобщённого синтаксиса вызова функций:

fun_call ::= fun_name ( integer )

можно определить для функций конкретные правила грамматики:

one_to_five ::= 1 | 2 | 3 | 4 | 5fun_call ::= lil_fac( one_to_five )         | ... other function definitions ...

Придумывание таких конкретных грамматик один из способов создания предметно-ориентированных языков (domain-specific languages, DSL). И в самом деле, DSL являются отличным способом обеспечения непредставимости недопустимых состояний. Разумеется, это решение не масштабируется, если вы хотите создать язык общего назначения с пользовательскими функциями.

Альтернативой создания очень конкретной грамматики является повышение уровня абстракции для усложнения возникновения недопустимых состояний. Например, частым источником программных ошибок является индексация вне границ массива. Такая ситуация возникает, потому что язык программирования предоставляет разработчику только примитивную операцию индексирования: a[x]. Здесь x является integer, но его значение может выйти за границы, что приведёт к исключению или вылету программы (если вам повезёт). Один из способов предотвращения такой ситуации заключается в определении более конкретного типа числа integer от нуля до 12, чтобы система типов отклоняла все потенциально недопустимые операции индексирования, а затем находила более точный тип для каждого массива мы снова встретились с валидацией.

Ещё одно решение можно заметить, что обычно массивы используются лишь ограниченным количеством способов. Например, очень часто производится итерация по массиву с выполнением каких-то вычислений. Вместо того, чтобы заставлять каждого программиста вручную писать для этого циклы for (Массивы в этом языке начинаются с 0 или с 1? Они заканчиваются на array.length or array.length 1? Индексы массива имеют определённый тип?), мы можем создать общую операцию fold (редукции). Аналогично, вместо того, чтобы заставлять кодеров писать собственные реализации хэш-таблиц, можно предоставить встроенную в язык реализацию. Предоставляя разработчикам более качественные абстракции, вы снижаете вероятность возникновения недопустимых состояний.

Можно пойти ещё дальше и устранить наиболее примитивные операции, обеспечив доступ только к абстракциям более высокого уровня. Надеюсь, что в наше время большинство программистов уже согласно с тем, что устранение оператора goto в пользу более высокоуровневых структурированных абстракций программирования было большой победой. То же самое может быть истинно и для других низкоуровневых конструкций: null, обычных указателей памяти и т.д.

В мире компьютерной безопасности у подобных обсуждений есть непосредственный аналог. Модель защиты большинства компьютерных систем позволяет обеспечивать разделение между операциями, которые я могу (пытаюсь) выполнить и операциями, которые мне разрешено выполнять: я могу попытаться удалить чей-то веб-сайт, однако этот запрос будет отклонён как неавторизованный (по крайней мере, на это стоит надеяться). Для сравнения, в системах на основе моделей возможностей объекта допустимость вызова таких операций зависит от того, есть ли у меня возможность (которую невозможно подделать), предоставляющая мне разрешение на их выполнение. В таких системах нельзя даже попробовать выполнить операцию, которую мне не разрешено выполнять. Например, в REST API, использующем мандатные URI я не могу даже отправить запрос DELETE для /users/alice, а должен вместо этого отправлять его на какой-то случайный неподбираемый URI если у меня нет этого URI, то я даже не могу начать отправлять запрос. Следовательно, цель безопасности на основе возможностей объекта заключается в непредставимости неавторизованных состояний.

Вероятно, наиболее известным воплощением парадигмы возможностей объекта является язык программирования E: объектно-ориентированный язык с динамической типизацией. Хотя он уже почти забыт, это потрясающий язык со множеством отличных идей. Для обеспечения безопасности E использует строгие границы абстракций. Предупреждаю: веб-сайт E это кроличья нора невероятной глубины!

В молодости я много программировал на Tcl. Большинство читателей согласится, что этот язык максимально далёк от современного языка программирования со статической типизацией (часто об этом говоря с искренним отвращением). Тем не менее, мысль о реализации непредставимости недопустимых состояний была совершенно естественной для меня и других программистов на Tcl. Я часто начинал проект с создания интерпретатора с чистого листа, избавляясь от всех встроенных конструкций языка (циклов, процедур, даже арифметики), а затем добавляя тщательно отобранное множество высокоуровневых предметно-ориентированных примитивов. Этот DSL потом становился конфигурационным файлом приложения, защищённого тем, что недопустимые конфигурации выразить невозможно.

Подведу итог: моё мнение заключается не в том, что системы типов плохи или представленное по ссылке эссе имеет недостатки, я считаю Parse, dont validate превосходным материалом. Но за последние два десятилетия произошёл такой прогресс систем типов и использования полнотиповых шаблонов программирования, что возникла опасность восприятия систем типов как единственного способа достижения корректности при создании ПО. Разумеется, это чрезвычайно мощный инструмент, но столь же мощными могут быть и более простые техники абстрагирования и сокрытия информации. Задача обеспечения непредставимости недопустимых значений должна быть одной из основных целей при проектировании ПО, но для её реализации существует много возможных способов.



На правах рекламы


Эпичные серверы для разработчиков и не только! Дешёвые VDS на базе новейших процессоров AMD EPYC и хранилища на основе NVMe дисков от Intel для размещения проектов любой сложности, от корпоративных сетей и игровых проектов до лендингов и VPN. Вы можете создать собственную конфигурацию сервера в пару кликов!

Подробнее..

Сложности работы с ANTLR пишем грамматику Ruby

04.08.2020 10:19:03 | Автор: admin
image В Ростелеком-Солар мы разрабатываем статический анализатор кода на уязвимости и НДВ, который работает в том числе на деревьях разбора. Для их построения мы пользуемся оптимизированной версией ANTLR4 инструмента для разработки компиляторов, интерпретаторов и трансляторов.

В репозитории можно найти грамматики множества языков программирования. Однако в нем отсутствует грамматика Ruby, которую, по всей видимости, никто так и не реализовал. Есть только грамматика похожего самодельного языка, парсящая лишь простейшие случаи. Это неудивительно, ведь грамматику Ruby сложно реализовать, так как язык обладает нетривиальным синтаксисом. Но она очень пригодилась бы тем, кто, например, захочет написать свой язык и задумается, как это сделать. Или тем, кому нужно решить технические сложности, рассмотренные в нашей статье. Ну что же придется писать новую грамматику, чем прямо здесь и займемся.


В ANTLR предлагается разбивать анализ языка на лексер и парсер.

Лексер занимается тем, что формирует токены на основе заданных последовательностей символов из алфавита языка. Если последовательность символов подходит под определение нескольких токенов, выбирается наидлиннейший, а среди таких первый по приоритету (который задается порядком записи).

Парсер формирует предложения (законченные команды) языка с помощью токенов (также называемых терминальными символами), получаемых из лексера.

В целом написание грамматики не отличается от других языков. Научиться можно с помощью авторской книги, документации или по многочисленным туториалам вроде этого.

В данной статье будет опущена базовая реализация и будут рассмотрены лишь технические сложности написания.

Проблемы лексера


В общем и целом, единственная возможная проблема, возникающая в лексере, это выдача валидного токена по последовательности символов и сопутствующие этому препятствия.

Интерполяция


Некоторые строки в Ruby допускают интерполяцию вставку произвольного кода внутрь с помощью синтаксиса #{code}. Например:

a = 3"Hello #{if a%2==1 then "Habr!" else "World!" end}"

Справляться будем с помощью режимов специфичных маленьких лексеров, предназначенных под определенную задачу, вроде нашего парсинга строки:

DoubleQuote: '"' {++nestedStringLevel;}    -> pushMode(InterpolationString);

На каждом уровне вложенности нужно поддерживать число открытых скобок из-за ситуаций вида (выйти из интерполяции нужно на 2-й закрывающей фигурной скобке):

"Kappa #{    buf = ''    [1, 2, 3].each { |x| buf += x.to_s }    buf}"

Для этого заведем стек openedCurlyBracesInsideString. Итого внутри мода имеем токен:

StartInterpolation: '#{' {openedCurlyBracesInsideString.push(0);}    -> pushMode(DEFAULT_MODE);

Теперь же нужно вовремя выйти из обычного режима (DEFAULT_MODE), для этого добавим код в токены:

OpenCurlyBracket:             '{' {    if (nestedStringLevel > 0 && !openedCurlyBracesInsideString.empty()) {        final int buf = openedCurlyBracesInsideString.pop();        openedCurlyBracesInsideString.push(buf + 1);    }};CloseCurlyBracket:            '}' {    if (nestedStringLevel > 0 && openedCurlyBracesInsideString.peek() <= 0) {       popMode();       openedCurlyBracesInsideString.pop();    } else {        if (!openedCurlyBracesInsideString.empty()) {            final int buf = openedCurlyBracesInsideString.pop();            openedCurlyBracesInsideString.push(buf - 1);        }    }};

%-нотации


В Ruby существует вдохновленный Perl дополнительный синтаксис написания строк, массивов строк и символов (который в Ruby не является символом в обычном понимании), регулярных выражений и шелл-команд. Синтаксис таких команд таков: %, следующий за ним опциональный идентификатор типа и символ-разделитель. Например: %w|a b c| массив из трех строк. Однако, также можно использовать в качестве разделителя парные скобки: {} [] () <>. Просто задать все возможные идентификаторы не выйдет тогда, например, последовательность

q = 35%q

будет распознаваться некорректно. Лексер просто съест самую длинную цепочку символов, создав токен %q.

Создав проверку на отсутствие явно невалидного разделителя вроде пробельного символа, цифры и буквы и добавив её в токен в качестве предиката (условие, которое обязано выполняться в определенном месте для продолжения конструирования токена),

StringArrayConstructorwToken: '%w' {canBeDelimiter()}?;

получаем защиту, работающую почти всегда (об этом далее). Так же добавим ожидание соответствующего разделителя в зависимости от альтернативы.

StartArrayConstructorNotInterpolated    : StringArrayConstructorwToken ( Brackets {setPairDelimiter();} | ~[(<{[] {setCharDelimiter();} ) {startStringMode(ArrayConstructorw);}fragment Brackets: '(' | '[' | '{' | '<';

где startStringMode utility-функция для переключения режима и поддержки вложенности.

public void startStringMode(final int mode) {    pushMode(mode);    ++nestedStringLevel;}

Контрпример: 5%q|1 парсящийся корректно лишь в контексте парсера, когда известно, что после 5-ти задания строки быть не может.

Можно было бы подумать, что достаточно найти парный разделитель с помощью заглядывания вперед, однако и на такой случай есть пример 5%q|1|1. Откуда становится ясно, что при разделении на лексер и парсер подобный случай распарсить невозможно.

Однако такое случается очень редко, так что сойдет \_()_/. Внутри режима же просто ждем разделитель.

ArraywWhitespace: WhitespaceAll                           -> skip;ArraywText:       ({!isDelimiter()}? ArraywTextFragment)+ -> type(StringPart);ArraywEnd:        . {nestedStringLevel--;}                -> type(HereDocEnd), popMode;

где type изменяет тип создаваемых токенов для удобства.

Деление или начало регулярного выражения


Синтаксис регулярного выражения таков /regexp/ (а также вышеупомянутая нотация с процентом). Возникает такая же проблема контекста парсера, как и в предыдущем пункте, для её смягчения создаем проверку

public boolean canBeRegex() {    return isPrevWS && " \t\r\u000B\f\b\n".indexOf((char) _input.LA(1)) == -1 || isPrevNL || isOp || prevNonWsType == StartInterpolation;}

и добавляем в токен

Divide:                       '/' {    if (canBeRegex()) {        startHereDoc(RegExp);    }};

Для пересчета переменных isOp, isPrevNL, isPrevWS также переопределяем emit-функцию итогового создания токена

@Overridepublic void emit(final Token token) {    final String txt = token.getText();    final int type = token.getType();    isPrevNL = type == NL;    isPrevWS = type == WS;    if (!isPrevWS && !isPrevNL && type != SingleLineComment && type != MultiLineComment) {        isOp = OPERATORS.contains(type);        prevNonWsChar = txt.charAt(txt.length() - 1);        prevNonWsType = type;    }    super.emit(token);}

где OPERATORS hashmap всех операторов.

Проблемы парсера


Пробельные символы


Довольно неприятным сюрпризом стало влияние пробелов на парсинг. Обычно они никак не сказываются на грамматике и просто-напросто удаляются из потока с помощью -> skip или перевода в другой канал. Однако ряд языков все же различает некоторые конструкции с их помощью.

Так, например, a+3 и a + 3 не могут быть вызовом функции без скобок, а а +3 может. Поэтому все правила парсера выглядят так (NL newline, WS whitespace):

    | expression WS* op=('+' | '-') (NL | WS)* expression

Для смягчения проблемы, напишем listener , который будет чистить нам дерево разбора от подобного мусора.

public class RemovingTokensListener implements ParseTreeListener {        private List<Integer> unwantedTokens;        ...        @Override        public void visitTerminal(final TerminalNode node) {            if (this.unwantedTokens.contains(node.getSymbol().getType())) {                ((ParserRuleContext) node.getParent().getRuleContext()).removeLastChild();            }        }}

Heredoc Лексер и парсер


Специальный синтаксис задания многострочных строк. Может быть таким

<<STRcontent line 1content line 2STR

или даже таким (интересно, что markdown не распознает синтаксис корректно).

value = 123print <<STR_WITH_INTERPOLATION, <<'STR_WITHOUT_INTERPOLATION'.stripcontent 1 and #{value}STR_WITH_INTERPOLATION     content 2 and #{value}STR_WITHOUT_INTERPOLATION

Проблема заключается в том, что нужно понимать, где заканчивается строка, а также было бы удобно знать, какой контент относится к какой строке. Для этого сначала создадим лист ожидающих парсинга heredoc-ов (парсер в зависимости от контекста и режима может подгружать произвольное число токенов вперед)

public final HeredocsHolder HEREDOCS = new HeredocsHolder();public static final class HereDocEntry {    public final String name;    public final HereDocType type;    public final boolean isInterpolated;    public int partsNumber;    public HereDocEntry(final String name, final HereDocType type, final boolean isInterpolated) {        this.name = name;        this.type = type;        this.isInterpolated = isInterpolated;        this.partsNumber = 0;    }}public static final class HeredocsHolder {    public final List<HereDocEntry> entries;    public int toProcess;    HeredocsHolder() {        this.entries = new ArrayList<>();        this.toProcess = 0;    }}

и будем добавлять их по мере поступления

StartHereDoc    : HereDocToken HereDocName {        heredocIdentifier = getHeredocIdentifier('\'');        setText(getText().trim());        keepHereDoc(HereDoc, false);    }    ;

public void keepHereDoc(final int mode, final boolean isInterpolated) {    HEREDOCS.entries.add(new HereDocEntry(heredocIdentifier, getHereDocType(), isInterpolated));    ++HEREDOCS.toProcess;    isFirstNL = true;}


Далее, увидев конец строки при ожидающих обработки heredoc-ах, вызовем функцию обработки.

NL:                           '\n' {    final int next = _input.LA(1);    if (HEREDOCS.toProcess > 0 && isFirstNL) {        startHereDocRoutine();        isFirstNL = false;        insideHeredoc = true;    }};

Сама функция обработки приведена ниже: здесь обрабатываем лишь последние heredoc-и (лексер мог уйти вперед, однако парсер в таком случае еще не поглотил токены, так что сохраняем информацию)

public void startHereDocRoutine() {    final int stopIdx = HEREDOCS.entries.size() - HEREDOCS.toProcess;    for (int i = HEREDOCS.entries.size() - 1; i >= stopIdx; --i) {        if (HEREDOCS.entries.get(i).isInterpolated) {            pushMode(HereDocInterpolated);        } else {            pushMode(HereDoc);        }    }    nestedStringLevel += HEREDOCS.toProcess;    currentHeredocIt = HEREDOCS.entries.listIterator(HEREDOCS.entries.size() - HEREDOCS.toProcess);    currentHeredoc = currentHeredocIt.next();}

Нужно перезаписать nextToken для выхода из режима и подсчета числа токенов каждого heredoc-а

@Overridepublic Token nextToken(){    final CommonToken token = (CommonToken)super.nextToken();    final int ttype = token.getType();    if (insideHeredoc && ttype == StartInterpolation) {        ++heredocTokensCount;    }    if (_mode == HereDoc || _mode == HereDocInterpolated) {        if (ttype == VarName) {            ++heredocTokensCount;        } else if (ttype == StringPart) {            ++heredocTokensCount;            final String txt = token.getText();            if (CheckHeredocEnd(txt)) {                token.setText(txt.trim());                token.setType(HereDocEnd);                nestedStringLevel--;                popMode();                currentHeredoc.partsNumber = heredocTokensCount;                if (currentHeredocIt.hasNext()) {                    currentHeredoc = currentHeredocIt.next();                }                heredocTokensCount = 0;                --HEREDOCS.toProcess;                if (_mode == DEFAULT_MODE) {                    insideHeredoc = false;                }            }        }    }    return token;}

Теперь займемся парсером.

С помощью @parser::members добавим в парсер: ссылку на лексер, узлы строк, куда будем переносить их контент, число узлов интерполяции (по аналогии с heredocTokensCount лексера), а также стек statement-ов с указанием необходимости обработки.

    private final RubyLexer lexer = (RubyLexer)_input.getTokenSource();    private final List<ParserRuleContext> CONTEXTS = new ArrayList<>();    private final List<Integer> heredocRulesCount = new ArrayList<>();    private final Stack<StatementEntry> statements = new Stack<>();    private static final class StatementEntry {        public boolean needProcess;        public int currentHeredoc;        StatementEntry() {            this.needProcess = false;            this.currentHeredoc = 0;        }    }

Модифицируем непосредственно единицу кода:

statement@init {    statements.push(new StatementEntry());}@after {    if (statements.peek().needProcess) {        processHeredocs($ctx);    }    statements.pop();}    : statementWithoutHeredocTail ({statements.peek().needProcess}? interpolatedStringPart* HereDocEnd {++statements.peek().currentHeredoc;})*    ;

@init код, который исполняется при входе парсера в правило, @after при выходе.

Стек необходим для отнесения heredoc-ов к нужному statement-у, т.к. внутри интерполяции могут быть новые.

Для того, чтобы правильно распознать случаи, где heredoc может быть обычным expression и где строку можно считать токенами подряд, а также сложные кейсы, когда конец строки будет лежать за текущим выражением, пишем такой вот код парсера:

string:...    | StartInterpolatedHereDoc (memberAccess* WS* NL interpolatedStringPart* HereDocEnd)? {        if ($ctx.HereDocEnd() == null) {            CONTEXTS.add($ctx);            heredocRulesCount.add(0);            statements.peek().needProcess = true;        } else {             lexer.HEREDOCS.entries.remove(0);        }    }...

Для самого же подсчета узлов интерполяции модифицируем код правила с контентом строки (+ 2 здесь нужно для подсчета токенов, открывающих и закрывающих интерполяцию)

interpolatedStringPartlocals[int nodesCount = 0]    : StringPart    | VarName    | StartInterpolation (WS* statement {++$nodesCount;})* (WS* rawStatement {++$nodesCount;})? WS* '}' {        final int cur = statements.peek().currentHeredoc;        if (cur < heredocRulesCount.size()) {            heredocRulesCount.set(cur, heredocRulesCount.get(cur) + $nodesCount + 2);        }    }    ;

где locals список локальных переменных (ссылаться на них нужно через $), а пробельные токены не считаются, т.к. удаляются во время построения дерева нашим listener-ом.

Теперь напишем саму функцию processHeredocs. Посчитаем, сколько узлов предстоит забрать

int heredocNodesCount = 0;for (int i = 0; i < CONTEXTS.size(); ++i) {    heredocNodesCount += lexer.HEREDOCS.entries.get(i).partsNumber;    heredocNodesCount += heredocRulesCount.get(i);}

Начиная с какого ребенка, начнем перекидывать контент строк по своим местам

int currentChild = ctx.getChildCount() - heredocNodesCount;

Подвесим контент к соответствующему узлу

for (int i = 0; i < CONTEXTS.size(); ++i) {    final RubyLexer.HereDocEntry entry = lexer.HEREDOCS.entries.remove(0);    final ParserRuleContext currentContext = CONTEXTS.get(i);    final int currentNodesCount = entry.partsNumber + heredocRulesCount.get(i);    for (int j = 0; j < currentNodesCount; ++j, ++currentChild) {        final ParseTree child = ctx.getChild(currentChild);        if (child instanceof TerminalNode) {            ((TerminalNodeImpl) child).setParent(currentContext);            currentContext.addChild((TerminalNodeImpl) child);        } else if (child instanceof ParserRuleContext) {            ((ParserRuleContext) child).setParent(currentContext);            currentContext.addChild((ParserRuleContext) child);        } else {            // parser failed            clear();            return;        }    }}

Очищаем сам узел, контексты heredoc-ов и список числа узлов интерполяции

for (int i = 0; i < heredocNodesCount; ++i) {    ctx.removeLastChild();}clear();

private void clear() {    CONTEXTS.clear();    heredocRulesCount.clear();}

Последним штрихом можно удалить ненужное промежуточное правило для обработки heredoc-ов statementWithoutHeredocTail, переподвешивая детей узла к его предку, с помощью того же listener-а

public class RemovingRulesListener implements ParseTreeListener {    private List<Integer> unwantedRules;    ...    @Override    public void exitEveryRule(final ParserRuleContext ctx) {        if (this.unwantedRules.contains(ctx.getRuleIndex())) {            final ParserRuleContext parentCtx =                    (ParserRuleContext) ctx.getParent().getRuleContext();            parentCtx.children.remove(ctx);            ctx.children.forEach(                    child -> {                        if (child instanceof RuleContext) {                            ((RuleContext) child).setParent(parentCtx);                            parentCtx.addChild((RuleContext) child);                        } else if (child instanceof TerminalNode) {                            ((TerminalNodeImpl) child).setParent(parentCtx);                            parentCtx.addChild((TerminalNodeImpl) child);                        }                    }            );        }    }}

Ambiguity


Нерешенной проблемой стало различение вызовов функций и, например, обычного сложения (а также символа любой одновременно унарной и бинарной операции), что может быть решено лишь с помощью символьных таблиц в рантайме.

Суть заключается в том, что при входе a +a +a +a... на каждом шаге может быть как обычное сложение, так и вызов функции без аргументов (хотя и в таком случае Ruby требует отсутствия пробела после знака у первого аргумента), отчего по всей видимости возникает экспоненциальный рост хождений по графу предсказаний.

Однако просто-напросто запретить ANTLR пробел после унарного оператора в контексте не выйдет придется вручную переписывать леворекурсивный expression, разрешаемый автоматически для случая без аргументов. Полагаясь на то, что никто не пишет более тридцати слагаемых без причины, данная проблема отпадает.

Заключение


Экспериментально данная грамматика может распарсить 99% файлов.

Так, aws-sdk-ruby, содержащий 3024 ruby-файла, падает лишь на семи, fastlane, содержащий 1028, на 2-x, а Ruby on Rails c 2081, на 19-ти.

Однако все же есть принципиально бОльные моменты вроде heredoc-ов, входящих в expression

option(:sts_regional_endpoints,  default: 'legacy',  doc_type: String,  docstring: <<-DOCS) do |cfg|Passing in 'regional' to enable regional endpoint for STS for all supportedregions (except 'aws-global'), defaults to 'legacy' mode, using global endpointfor legacy regions.  DOCS  resolve_sts_regional_endpoints(cfg)end

или используемых как выражения, любых типов блоков

def test_group_by_with_order_by_virtual_count_attribute    expected = { "SpecialPost" => 1, "StiPost" => 2 }    actual = Post.group(:type).order(:count).limit(2).maximum(:comments_count)    assert_equal expected, actualend if current_adapter?(:PostgreSQLAdapter)

Надеюсь, приемы, описанные в статье, помогут вам справиться с трудностями вашей грамматики. Если считаете, что какую-то из проблем можно решить лучше добро пожаловать в комментарии.

Автор: Федор Усов, разработчик Solar appScreener
Подробнее..

Облегчаем себе жизнь с помощью BeautifulSoup4

01.03.2021 16:18:41 | Автор: admin
Приветствую всех. В этой статье мы сделаем жизнь чуточку легче, написав легкий парсер сайта на python, разберемся с возникшими проблемами и узнаем все муки пайтона что-то новое.

Статья ориентирована на новичков, таких же как и я.

Начало


Для начала разберем задачу. Взял я малоизвестный сайт новостей об Израиле, так как сам проживаю в этой стране, и хочется читать новости без рекламы и не интересных новостей. И так, имеется сайт, на котором постятся новости: есть новости помеченные красным, а есть обычные. Те что обычные не представляют собой ничего интересного, а отмеченные красным являются самым соком. Рассмотрим наш сайт.



Как видно сайт достаточно большой и есть много ненужной информации, а ведь нам нужно использовать лишь контейнер новостей. Давайте использовать мобильную версию сайта,
чтобы сэкономить себе же время и силы.



Как видите, сервер отдал нам красивый контейнер новостей (которых, кстати, больше чем на основном сайте, что нам на руку) без рекламы и мусора.

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



Как видим каждая новость лежит по-отдельности в тэге 'a' и имеет класс 'lenta'. Если мы откроем тэг 'a', то заметим, что внутри есть тэг 'span', в котором находится класс 'time2', либо 'time2 time3', а также время публикации и после закрытия тэга мы наблюдаем сам текст новости.

Что отличает важную новость от неважной? Тот самый класс 'time2' или 'time2 time3'. Новости помеченые 'time2 time3' и являются нашими красными новостями. Раз уж суть задачи понятна, перейдем к практике.

Практика


Для работы с парсерами умные люди придумали библиотеку BeautifulSoup4, в которой есть еще очень много крутых и полезных функций, но об этом в следующий раз. Нам также понадобиться библиотека Requests позволяющая отправлять различные http-запросы. Идем их скачивать.

(убедитесь, что стоит последняя версия pip)

pip install beautifulsoup4 

pip install requests

Переходим в редактор кода и импортируем наши библиотеки:

from bs4 import BeautifulSoupimport requests

Для начала сохраним наш URL в переменную:

url = 'http://mignews.com/mobile'

Теперь отправим GET()-запрос на сайт и сохраним полученное в переменную 'page':

page = requests.get(url)

Проверим подключение:

print(page.status_code)

Код вернул нам статус код '200', значит это, что мы успешно подключены и все в полном порядке.

Теперь создадим два списка (позже я объясню для чего они нужны):

new_news = []news = []

Самое время воспользоваться BeautifulSoup4 и скормить ему наш page, указав в кавычках как он нам поможет 'html.parcer':

soup = BeautifulSoup(page.text, "html.parser")

Если попросить его показать, что он там сохранил:

print(soup)

Нам вылезет весь html-код нашей страницы.

Теперь воспользуемся функцией поиска в BeautifulSoup4:

news = soup.findAll('a', class_='lenta')

Давайте разберём поподробнее, что мы тут написали.

В ранее созданный список 'news' (к которому я обещал вернуться), сохраняем все с тэгом 'а' и классом 'news'. Если попросим вывести в консоль все, что он нашел, он покажет нам все новости, что были на странице:



Как видите, вместе с текстом новостей вывелись теги 'a', 'span', классы 'lenta' и 'time2', а также 'time2 time3', в общем все, что он нашел по нашим пожеланиям.

Продолжим:

for i in range(len(news)):    if news[i].find('span', class_='time2 time3') is not None:        new_news.append(news[i].text)

Тут мы в цикле for перебираем весь наш список новостей. Если в новости под индексом [i] мы находим тэг 'span' и класc 'time2 time3', то сохраняем текст из этой новости в новый список 'new_news'.

Обратите внимание, что мы используем '.text', чтобы переформатировать строки в нашем списке из 'bs4.element.ResultSet', который использует BeautifulSoup для своих поисков, в обычный текст.

Однажды я застрял на этой проблеме надолго в силу недопонимания работы форматов данных и неумения использовать debug, будьте осторожны. Таким образом теперь мы можем сохранять эти данные в новый список и использовать все методы списков, ведь теперь это обычный текст и, в общем, делать с ним, что нам захочется.

Выведем наши данные:

for i in range(len(new_news)):    print(new_news[i])

Вот что мы получаем:



Мы получаем время публикации и лишь интересные новости.

Дальше можно построить бот в Телеге и выгружать туда эти новости, либо создать виджет на рабочий стол с актуальными новостями. В общем, можно придумать удобный для себя способ узнавать о новостях.

Надеюсь эта статья поможет новичкам понять, что можно делать с помощью парсеров и поможет им немного продвинуться вперед с обучением.

Спасибо за внимание, был рад поделиться опытом.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru