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

117

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

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

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

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

или:

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

или:

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

или:

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

[Честно говоря: большинство приведенных выше объяснений ссылаются на страницы википедии, но их не намного легче понять, чем сами ответы].

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

РЕДАКТИРОВАТЬ: После прочтения первого ответа я подумал, что должен уточнить: я ищу «перевод», который также кратко объясняет концепции, которые он пытается перевести: в конце ответа читатель должен иметь приблизительное представление - например - что означают "регулярный язык" и "контекстно-свободная грамматика" ...

макинтош
источник
19
Имейте в виду, что в терминах информатики «регулярные выражения» сильно отличаются от современных «реализаций регулярных выражений» (инструменты / api, которые вы используете в языке программирования). Последние могут «запоминать» вещи, с которыми они столкнулись, и даже могут сопоставлять рекурсивно определенные (под) шаблоны, заставляя их сопоставлять / анализировать / распознавать гораздо больше, чем теоретические «регулярные выражения».
Барт Кирс,
1
@Bart: На самом деле это относится только к языкам, которые злоупотребляют термином «регулярное выражение. POSIX ERE является чисто регулярным.
R .. GitHub ОСТАНОВИТЬ ПОМОЩЬ ICE
2
@R .., значит, вы называете POSIX «современной реализацией»: P. Со всей серьезностью , хотя: да, вы правы те , действительно являются регулярными. Я должен был сказать «... многие современные реализации регулярных выражений ...» или «... реализации регулярных выражений PCRE ...» .
Bart Kiers
4
Мне трудно серьезно относиться к языкам программирования, которые в корне неверно используют строгий язык ради того, чтобы
продать
3
@R .., очень жаль, что реализации PCRE называют "регулярными выражениями", но, если не принимать во внимание язык серьезно, это слишком далеко, ИМО. Я имею в виду, разве вы не воспринимаете Perl, Java, Python, Ruby, JavaScript, .NET и т.д. несерьезно из-за этого?
Bart Kiers

Ответы:

117

Сконцентрируйтесь на этом:

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

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

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

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

Стив Джессоп
источник
Здесь также есть довольно хорошее объяснение конечных автоматов: youtube.com/watch?v=vhiiia1_hC4
GDP2
55

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

Википедия прекрасно описывает это в цитате Ларри Уолла :

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

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

Так почему бы и нет?

Хорошая причина не сопоставлять 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 The Cthulhu Way .

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

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

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

  • Когда вам нужно однократное обновление ваших HTML-файлов, и вы знаете, что структура согласована.
  • Когда у вас очень маленький фрагмент HTML.
  • Когда вы имеете дело не с файлом HTML, а с похожей системой шаблонов (в этом случае может быть очень сложно найти парсер).
  • Когда вы хотите изменить части HTML, но не весь HTML - парсер, насколько мне известно, не может ответить на этот запрос: он проанализирует весь документ и сохранит весь документ, изменив части, которые вы никогда не хотели изменять.
Коби
источник
4
Это очень четкая и красиво написанная статья о том, когда (не) использовать регулярное выражение для анализа HTML, но это вряд ли ответ на мой вопрос. Могу я предложить вам вместо этого переместить его к этому вопросу ? Я думаю, это принесет вам больше репутации, но - прежде всего - я думаю, что это будет место, где будущие посетители сочтут это более актуальным (есть комментарий @Bart Kiers к моему вопросу, который напоминает посетителям о «дополнительной власти» современных движков регулярных выражений).
mac,
1
@mac - Большое спасибо. Собственно, я немного подумал. Я знаю, что не ответил на ваш вопрос, но я не думаю, что вопрос в основном правильный - вы просите объяснить неправильную причину ... Хотя у вас есть хорошая идея, может быть, другой вопрос более подходит ...
Kobi
19

Потому что 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 от закрывающего тега для bardiv. Это потому, что у них нет возможности сказать: «Хорошо, я сейчас спустился во второй из двух div, поэтому следующий закрытый div, который я вижу, возвращает меня к одному, а следующий за ним - закрывающий тег для первого» , Программисты обычно отвечают, разрабатывая регулярные выражения для особых случаев для конкретной ситуации, которые затем ломаются, как только в них вводятся новые теги, fooи их приходится распутывать, что требует огромных затрат времени и разочарований. Вот почему люди злятся на все это.

Янус Чиароскуро
источник
1
Оцените ответ, но мой вопрос не в том, «почему я не могу использовать регулярное выражение ...». Мой вопрос касается «перевода» предоставленных мною официальных объяснений! :)
mac
5
Это в некотором смысле перевод всех из них, наиболее приближенно «Регулярные выражения могут соответствовать только регулярным языкам, но HTML - это контекстно-свободный язык», а также язык конечных автоматов. На самом деле причина одна и та же.
Ianus Chiaroscuro
Извините, возможно, я не совсем ясно сформулировал свой вопрос (приветствуются предложения по его улучшению!). Но я ищу ответ, который также объясняет «перевод». Ваш ответ не проясняет ни понятия «обычный язык», ни «контекстно-свободный язык» ...
Mac
5
Объяснение этих терминов было бы таким же техническим, как и сам жаргон, и отвлекало бы от фактического значения, до которого доходит весь точный язык, это то, что я опубликовал.
Ianus Chiaroscuro
4
<(\w+)(?:\s+\w+="[^"]*")*>(?R)*</\1>|[\w\s!']+соответствует вашему образцу кода.
Коби
9

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

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

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

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

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

HTML требует, чтобы вы знали больше, чем просто, в каком состоянии вы находитесь - он требует истории того, что вы видели раньше, чтобы соответствовать вложенности тегов. Вы можете добиться этого, если добавите в машину стек, но тогда он больше не будет «обычным». Это называется выталкивающей машиной и распознает грамматику.

Шон Макмиллан
источник
2
«Понимание машин с конечным числом состояний, машин выталкивания вниз и машин Тьюринга - это, по сути, учебная программа 300-уровневого курса компьютерной науки». Я понимаю, что это попытка указать, насколько сложна / продвинута тема, но я не знаком с школьной системой, о которой вы говорите, не могли бы вы уточнить, не относясь к конкретной стране? Спасибо! :)
mac
1
Я его обновил. Я не знаю, что это слишком сложно понять, просто чтобы объяснить в сообщении о переполнении стека.
Шон Макмиллан,
6

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

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

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

п. 'местоимения' м.
источник
6

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

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

void Start ()
{
    string myCode = @"
    void Start()
    {
       Console.WriteLine (""x"");
    }
    ";
}
Агент-J
источник
Это понятный ответ
Человек
Но контекстно-зависимый не означает регулярный. Язык сопоставленного парантеза контекстно-зависимый, но не регулярный.
Taemyr
Следует добавить, что регулярные выражения (если вы не добавляете такие расширения, которые присутствуют в Perl) эквивалентны обычным грамматикам , что означает, что они не могут описывать произвольно глубоко вложенные структуры, такие как произвольно сбалансированные круглые скобки или открывающие и закрывающие теги HTML-элементов.
Reinierpost
4

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

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

<price>10.65</price>

Но если ваш код верен, то:

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

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

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

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

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

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

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

Майкл Кей
источник
2

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

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

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

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

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

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

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

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

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

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

buchWyrm
источник
3
Утверждение, что это регулярное выражение будет соответствовать только в случае правильного ввода, неверно. Он не проверяет, являются ли имена допустимыми именами XML, он не проверяет атрибуты, он не проверяет ссылки на сущности и символы, он не обрабатывает CDATA или инструкции обработки. Когда вы говорите, что он был протестирован, я очень сомневаюсь, что он был протестирован на чем-либо, напоминающем набор тестов на соответствие XML. Это проблема всех попыток обработать XML с помощью регулярных выражений, которые я когда-либо видел: они работают с небольшим количеством входных данных, но не с любым XML, который можно законно передать вашему приложению.
Майкл Кей
2
Кроме того, есть правильно сформированные входные данные, которым не соответствует регулярное выражение. Например, он не позволяет использовать пробелы после имени в закрывающем теге. Большинство этих глюков легко исправить, но как только вы исправите ВСЕ глюки, вы получите что-то совершенно непригодное для использования. И, конечно, настоящая проблема в том, что вы не просто хотите, чтобы парсер давал вам ответ «да / нет», вы хотите, чтобы он передавал информацию приложению, которое делает с ним что-то полезное.
Майкл Кей
0

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

теория:

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

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

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

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

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

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

xidel xpath3

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

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

's lxml( from lxml import etree)

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

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

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


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

Жиль Кено
источник