Почему невозможно использовать регулярное выражение для разбора HTML/XML: формальное объяснение в условиях неспециалиста

На SO нет дня, который проходит без вопроса о разборе (X) HTML или XML с запросами регулярных выражений.

Хотя относительно легко придумать примеры, демонстрирующие нежизнеспособность регулярных выражений для этой задачи или с помощью набор выражений, чтобы представить концепцию, я все еще не мог найти на SO a формальное объяснение того, почему это невозможно сделать в условиях непрофессионала.

Единственные формальные объяснения, которые я мог найти до сих пор на этом сайте, вероятно, очень точны, но также довольно загадочны для программиста-самоучки:

недостаток заключается в том, что HTML - это грамматика типа Хомского типа 2 (контекст бесплатно грамматика), а RegEx - грамматика Хомского типа 3 (регулярное выражение)

или

Регулярные выражения могут соответствовать только обычным языкам, но HTML - это контекстно-свободный язык.

или

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

или

Лемма прокачки для правильных языков - причина, по которой вы не можете что.

[Справедливости ради: большинство приведенных выше ссылок ссылаются на страницы Википедии, но это не намного легче понять, чем сами ответы).

Итак, мой вопрос: может ли кто-нибудь предоставить перевод в неспециалистических терминах формальных объяснений, приведенных выше, почему нельзя использовать регулярное выражение для синтаксического анализа (X) HTML/XML?

EDIT:. Прочитав первый ответ, я подумал, что должен уточнить: я ищу "перевод", который также кратко объясняет концепции, которые он пытается перевести: в конце ответа, у читателя должна быть приблизительная идея - например, о том, что означает "обычный язык" и "контекстно-свободная грамматика"...

Ответы

Ответ 1

Сосредоточьтесь на этом:

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

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

HTML, с другой стороны, имеет структуры, которые могут гнездиться сколь угодно глубоко. Чтобы определить, является ли файл допустимым HTML или нет, вам нужно проверить, соответствуют ли все закрывающие теги предыдущему открывающему тегу. Чтобы понять это, вам нужно знать, какой элемент закрывается. Без каких-либо средств "помнить", какие открытые теги вы видели, нет шансов.

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

Ответ 2

Тот факт, что HTML не представляет собой обычный язык, - это красная селедка. Регулярное выражение и обычные языки кажутся похожими, но не являются - они имеют одинаковое происхождение, но там заметное расстояние между академическими "обычными языками" и текущей степенью согласованности движков. Фактически, почти все современные механизмы регулярного выражения поддерживают нерегулярные функции - простой пример - (.*)\1. который использует обратную привязку для соответствия повторяющейся последовательности символов - например, 123123 или bonbon. Согласование рекурсивных/сбалансированных структур делает их еще более увлекательными.

Википедия помещает это красиво в цитату Ларри Уолл:

'Регулярные выражения' [...] лишь незначительно связаны с реальными регулярными выражениями. Тем не менее, этот термин вырос благодаря возможностям наших механизмов сопоставления шаблонов, поэтому я не буду пытаться бороться с лингвистической необходимостью здесь. Я, однако, обычно называю их "регулярными выражениями" (или "regexen", когда я нахожусь в англосаксонском настроении).

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

Итак, почему бы не тогда?

Хорошая причина не соответствовать HTML с регулярным выражением заключается в том, что "просто потому, что вы можете это не значит, что вам нужно". Пока возможно - есть просто лучшие инструменты для работы. Принимая во внимание:

  • Допустимый HTML сложнее/сложнее, чем вы думаете.
  • Существует много типов "допустимых" HTML - то, что допустимо в HTML, например, недопустимо в XHTML.
  • Большая часть HTML свободной формы, найденная в Интернете, в любом случае недействительна. Библиотеки HTML хорошо справляются с этими проблемами, и были протестированы для многих из этих распространенных случаев.
  • Очень часто невозможно сопоставить часть данных без его синтаксического анализа в целом. Например, вы можете искать все заголовки и в итоге совпадать внутри комментария или строкового литерала. <h1>.*?</h1> может быть смелой попыткой найти главный заголовок, но он может найти:

    <!-- <h1>not the title!</h1> -->
    

    Или даже:

    <script>
    var s = "Certainly <h1>not the title!</h1>";
    </script>
    

Последний вопрос является самым важным:

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

Хорошее резюме темы и важный комментарий о том, когда смешивание Regex и HTML может быть уместным, можно найти в блоге Джеффа Атвуда: Parsing Html Cthulhu Way.

Когда лучше использовать регулярное выражение для синтаксического анализа HTML?

В большинстве случаев лучше использовать XPath в структуре DOM, которую может вам предоставить библиотека. Тем не менее, против популярного мнения, есть несколько случаев, когда я настоятельно рекомендую использовать регулярное выражение, а не библиотеку парсера:

Учитывая несколько из этих условий:

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

Ответ 3

Поскольку HTML может иметь неограниченное вложенное расположение <tags><inside><tags and="<things><that><look></like></tags>"></inside></each></other>, и регулярное выражение не может справиться с этим, потому что оно не может отслеживать историю того, из чего он спустился и вышел из него.

Простая конструкция, иллюстрирующая трудности:

<body><div id="foo">Hi there!  <div id="bar">Bye!</div></div></body>

99,9% обобщенных процедур извлечения на основе регулярных выражений не смогут правильно передать мне все внутри div с идентификатором foo, потому что они не могут сказать закрывающий тег для этого div из закрывающего тега для bar div. Это потому, что у них нет никакого способа сказать "хорошо, я теперь опустился во второй из двух div, поэтому следующий div close, который я вижу, возвращает мне один, а один после этого является тегом закрытия для первого", Программисты обычно отвечают, создавая специальные выражения для конкретной ситуации, которые затем ломаются, как только в теге foo вводится больше тегов, и они должны быть нерасчленены с огромными затратами времени и разочарования. Вот почему люди злится на все это.

Ответ 4

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

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

Рассмотрим следующую машину, которая распознает строку "hi".

(Start) --Read h-->(A)--Read i-->(Succeed)
  \                  \
   \                  -- read any other value-->(Fail) 
    -- read any other value-->(Fail)

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

HTML требует, чтобы вы знали больше, чем просто то, в каком состоянии вы находитесь, - для этого требуется история того, что вы видели раньше, для соответствия вложенности тегов. Вы можете выполнить это, если вы добавите стек к машине, но затем он перестает быть "обычным". Это называется Push-Down Machine и распознает грамматику.

Ответ 5

Регулярное выражение представляет собой машину с конечным (и обычно довольно небольшим) числом дискретных состояний.

Чтобы анализировать XML, C или любой другой язык с произвольным вложением языковых элементов, вам нужно помнить, насколько вы глубоки. То есть вы должны иметь возможность подсчитывать фигурные скобки/скобки/теги.

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

Ответ 6

Грамматика - это формальное определение того, куда слова могут идти. Например, прилагательные преследуют существительные in English grammar, но следуют за существительными en la gramática española. Контекстно-свободный означает, что грамматик универсален во всех контекстах. Контекстно-зависимое означает, что в определенных контекстах есть дополнительные правила.

В С#, например, using означает что-то другое в using System; в верхней части файлов, чем using (var sw = new StringWriter (...)). Более подходящим примером является следующий код в коде:

void Start ()
{
    string myCode = @"
    void Start()
    {
       Console.WriteLine (""x"");
    }
    ";
}

Ответ 7

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

Например, все это очень хорошо записывает регулярное выражение для соответствия

<price>10.65</price>

Но если ваш код будет правильным, тогда:

  • Он должен разрешать пробелы после имени элемента в обоих начальных и конечных тегах

  • Если документ находится в пространстве имен, то он должен разрешить использование любого префикса пространства имен

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

  • Возможно, потребуется разрешить пробелы до и после десятичного значения (опять же, в зависимости от подробных правил конкретного словаря XML).

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

  • Возможно, потребуется диагностика, если вход недействителен.

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

Ответ 8

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

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

Рассмотрим, например,

(?:
    <!\-\-[\S\s]*?\-\->
    |
    <([\w\-\.]+)[^>]*?
    (?:
        \/>
        |
        >
        (?:
            [^<]
            |
            (?R)
        )*
        <\/\1>
    )
)

Это найдет следующий правильно сформированный XML-тег или комментарий, и он найдет его только в том случае, если оно полностью сформировано. (Это выражение было проверено с помощью Notepad++, в котором используется библиотека регулярных выражений Boost C++, которая близко аппроксимирует PCRE.)

Вот как это работает:

  1. Первый фрагмент соответствует комментарию. Это необходимо для первого, чтобы он имел дело с любым прокомментированным кодом, который в противном случае мог бы вызвать зависание.
  2. Если это не соответствует, оно будет искать начало тега. Обратите внимание, что он использует круглые скобки для захвата имени.
  3. Этот тег будет либо завершен в />, таким образом завершив тег, или он закончится с помощью >, и в этом случае он продолжит изучение содержимого тега.
  4. Он будет продолжать синтаксический анализ до тех пор, пока он не достигнет <, после чего он вернется к началу выражения, позволяя ему иметь дело либо с комментарием, либо с новым тегом.
  5. Он будет продолжаться через цикл до тех пор, пока он не достигнет конца текста или не будет < a, который он не может проанализировать. Неспособность совладать, конечно, заставит его начать процесс. В противном случае, < предположительно, является началом закрывающего тега для этой итерации. Используя обратную ссылку внутри закрывающего тега <\/\1>, он будет соответствовать открытому тегу для текущей итерации (глубина). Там только одна группа захвата, поэтому этот матч - это простой вопрос. Это делает его независимым от имен используемых тегов, хотя вы можете изменить группу захвата для захвата только определенных тегов, если вам нужно.
  6. В этот момент он либо выйдет из текущей рекурсии, либо на следующий уровень, либо закончит совпадение.

В этом примере решаются проблемы, связанные с пробелами или определяющие релевантный контент, с использованием групп символов, которые просто отрицают < или > или в случае комментариев с помощью [\S\s], что будет соответствовать чему угодно, включая возврат каретки и новые линии, даже в однострочном режиме, продолжаются до тех пор, пока не достигнут -->. Следовательно, он просто рассматривает все как действительные, пока не достигнет чего-то значимого.

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

Короче говоря: используйте синтаксический анализатор XML для реальной работы и используйте это, если хотите поиграть с регулярными выражениями.

Ответ 9

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

теория:

Согласно теории компиляции, XML/HTML не может быть проанализирован с помощью регулярных выражений на основе конечного автомата. Из-за иерархического построения XML/HTML вам нужно использовать автомат с нажатием кнопки и манипулировать грамматикой LALR с помощью такого инструмента, как YACC.

RealLife © ® ™ повседневный инструмент в :

Вы можете использовать один из следующих:

xmllint часто устанавливается по умолчанию с использованием libxml2, xpath1 (проверьте, что у меня есть оболочка, чтобы выводился символ новой строки

xmlstarlet может редактировать, выбирать, преобразовывать... Не установлен по умолчанию, xpath1

xpath устанавливается через модуль perl XML :: XPath, xpath1

xidel xpath3

saxon-lint мой собственный проект, обертка над Java-библиотекой @Michael Kay Saxon-HE, xpath3

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

[ lxml ] lxml (from lxml import etree)

XML::LibXML, XML::XPath, XML::Twig::XPath, HTML::TreeBuilder::XPath

, проверьте этот пример

DOMXpath, проверьте этот пример


Проверка: использование регулярных выражений с тегами HTML