Чому неможливо використовувати регулярний вираз для розбору HTML / XML: формальне пояснення в умовах непростої людини


117

Жоден день на SO не проходить без запитання про розбір (X) HTML або XML з запитаннями регулярних виразів.

Хоча порівняно легко придумати приклади, які демонструють нежиттєздатність регулярних виразів для цього завдання або з набором виразів для представлення концепції, я все ще не міг знайти в ТА офіційного пояснення того, чому це неможливо зробити в непрофесійних умови.

Єдині формальні пояснення, які я могла знайти на цьому веб-сайті, напевно, надзвичайно точні, але також досить скептичні для програміста-самоучки:

недолік тут полягає в тому, що HTML - це граматика Chomsky Type 2 (граматика без контексту), а RegEx - граматика Chomsky Type 3 (регулярний вираз)

або:

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

або:

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

або:

Лімма накачки для звичайних мов є причиною того, чому ви не можете цього зробити.

[Справедливо кажучи: більшість наведених пояснень посилаються на сторінки вікіпедій, але це не набагато простіше зрозуміти, ніж самі відповіді].

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

ЕДИТ: Після прочитання першої відповіді я подумав, що повинен уточнити: шукаю «переклад», який також бурхливо пояснює поняття, які він намагається перекласти: наприкінці відповіді читач повинен мати грубе уявлення - наприклад - що означає "звичайна мова" та "без контексту граматика" ...


19
Будьте в курсі того, що, з точки зору інформатики, "регулярні вирази" сильно відрізняються від сучасних "регекс-реалізацій" (інструменти / програми, які ви використовуєте мовою програмування). Останні можуть "запам'ятати" речі, з якими вони стикалися, і навіть можуть відповідати рекурсивно визначеним (під) шаблонам, змушуючи їх збігати / розбирати / розпізнавати набагато більше, ніж теоретичні "регулярні вирази".
Барт Кіерс

1
@Bart: Це дійсно стосується лише мов, які зловживають терміном "регулярне вираження. POSIX ERE є суто регулярним.
R .. GitHub СТОП ДОПОМОГАТИ ДВІ

2
@R .., отже, ви називаєте POSIX "реалізацією сучасного дня": P. З усією серйозністю , хоча: так, ви маєте рацію ті , дійсно є регулярними. Я повинен був сказати "... багато сучасних реалізацій регулярних виразів ..." або "... реалізації регексу PCRE ..." .
Барт Кіерс

4
Мені важко сприймати серйозні мови програмування, які принципово зловживають суворою мовою заради маркетингу самих неосвічених програмістів ...
R .. GitHub ЗАСТАНІТЬ ДОПОМОГУ ДВІ

3
@R .., прикро, що реалізацію PCRE називають "регулярними виразами", але якщо не сприймати мову серйозно, це робить її на крок занадто далеко, IMO. Я маю на увазі, ти не сприймаєш Perl, Java, Python, Ruby, JavaScript, .NET і т. Д. Несерйозно через це?
Барт Кіерс

Відповіді:


117

Концентруйтесь на цьому:

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

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

HTML, з іншого боку, має структури, які можуть гніздитися довільно глибоко. Щоб визначити, чи правильний файл HTML чи ні, потрібно перевірити, чи всі теги закриття відповідають попередньому тегу відкриття. Щоб зрозуміти це, потрібно знати, який елемент закривається. Без жодних засобів "запам'ятати" те, що відкривали теги ви бачили, жодного шансу.

Зауважимо, однак, що більшість "регулярних" бібліотек насправді дозволяють більше, ніж просто чітке визначення регулярних виразів. Якщо вони можуть відповідати зворотним посиланням, вони вийшли за рамки звичайної мови. Тож причина, чому не слід використовувати бібліотеку регулярних виразів на HTML, є трохи складнішою, ніж простий факт, що HTML не є регулярним.


Тут також є досить хороше пояснення автоматизованих державних автоматів: youtube.com/watch?v=vhiiia1_hC4
GDP2

55

Те, що 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 можуть бути корисними у блозі Джеффа Етвуда: Розбір Html The Cthulhu Way .

Коли краще використовувати регулярний вираз для розбору HTML?

У більшості випадків краще використовувати XPath на структурі DOM, яку може надати вам бібліотека. Проте, проти поширеної думки, є кілька випадків, коли я настійно рекомендую використовувати регулярний вираз, а не бібліотеку розбору:

Враховуючи кілька таких умов:

  • Коли вам потрібно одноразове оновлення ваших HTML-файлів, і ви знаєте, що структура є послідовною.
  • Коли у вас дуже маленький фрагмент HTML.
  • Якщо ви не маєте справу з файлом HTML, але подібним механізмом шаблонування (в цьому випадку знайти синтаксичний розбір може бути дуже важко).
  • Коли ви хочете змінити частини HTML, але не всі - парсер, наскільки мені відомо, не зможете відповісти на цей запит: він розбере весь документ і збереже весь документ, змінивши частини, які ви ніколи не хотіли змінювати.

4
Це дуже чіткий і добре написаний фрагмент про те, коли (не потрібно) використовувати регулярний вираз для розбору HTML, але навряд чи це відповідь на моє запитання. Чи можу я запропонувати вам замість цього перейти до цього питання ? Думаю, це отримає вам більше репутації там, але, перш за все, - я думаю, що це було б місце, де майбутні відвідувачі вважають його більш релевантним (є коментар @Bart Kiers до мого запитання, який нагадує відвідувачам про "додаткову силу" сучасних двигунів регексу).
мак

1
@mac - Дякую велике Власне, я і подумав над цим. Я знаю, що я не відповів на ваше запитання, але я не думаю, що це питання в основному правильне - ви просите пояснити неправильну причину ... Хоча у вас є гарна ідея, можливо, інше питання підходить більше ...
Кобі

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, тому що вони не можуть розпізнати завершальний тег для цього діла із закривального тегу для bardiv. Це тому, що у них немає способу сказати "добре, я зараз зійшов у другу з двох дівок, тож наступний дів, який я бачу, повертає мене назад, а той, що знаходиться після цього, - це тег для першого". . Програмісти, як правило, реагують на розробку спеціальних регекерів для конкретної ситуації, які потім ламаються, як тільки все більше тегів буде введено всередину, fooі їх потрібно неприховати з величезними витратами в часі та розчаруванні. Ось чому люди озлоблені від усієї справи.


1
Вдячний за відповідь, але моє запитання не в тому, "чому я не можу використовувати регулярний вираз ...". Моє запитання - про "переклад" наданих мені формальних пояснень! :)
mac

5
Це переклад усіх з них у певному сенсі, найчастіше "Регулярні вирази можуть відповідати лише звичайним мовам, але HTML - це контекстна мова" і мова про кінцеві автомати. Це справді все та сама причина.
Ianus Chiaroscuro

Вибачте, можливо, я не зрозумів свого запитання (пропозиції щодо його вдосконалення вітаються!). Але я шукаю відповідь, яка також пояснює "переклад". Ваша відповідь не пояснює ні поняття «звичайна мова», ні «контекстна мова без контексту» ...
mac

5
Пояснення цих термінів було б таким же технічним, як і сам жаргон, і відволікання від фактичного значення, яке отримує вся мова про точність, і це те, що я розміщував.
Ianus Chiaroscuro

4
<(\w+)(?:\s+\w+="[^"]*")*>(?R)*</\1>|[\w\s!']+відповідає вашому зразку коду.
Кобі

9

Звичайна мова - це мова, яка може відповідати машині з кінцевим станом.

(Розуміння машин кінцевого стану, машин, що віджимаються, та машин Тюрінга - це, головним чином, навчальна програма четвертого курсу коледжу курсу CS.)

Розглянемо наступну машину, яка розпізнає рядок "привіт".

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

Це проста машина для розпізнавання звичайної мови; Кожен вираз у дужках - це стан, а кожна стрілка - перехід. Побудова такої машини дозволить перевірити будь-який рядок введення на звичайній мові - отже, регулярний вираз.

HTML вимагає, щоб ви знали більше, ніж просто стан, в якому ви перебуваєте - він вимагає історії того, що ви бачили раніше, щоб відповідати вкладенню тегів. Ви можете досягти цього, якщо додати стек до машини, але він більше не є "регулярним". Це називається машиною Push-down і розпізнає граматику.


2
"Розуміння машин кінцевих держав, машин, що віджимаються, і машин Тьюрінга - це, в основному, навчальний план курсу CS на 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"");
    }
    ";
}

Це зрозуміла відповідь
Людина

Але без контексту не означає регулярності. Мова відповідних парантезів є безконтекстною, але не регулярною.
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], що відповідатиме будь-чому, включаючи повернення перевезення та нові рядки, навіть в однорядковій режим, продовжуючи, поки не досягне a -->. Отже, він просто ставиться до всього як до дійсного, поки не досягне чогось значимого.

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

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


3
Заява про те, що цей регулярний вираз буде відповідати лише в тому випадку, якщо вхід добре сформований, є невірним. Він не перевіряє, чи є імена дійсними іменами XML, не перевіряє атрибути, не перевіряє посилання сутності та символів, не обробляє CDATA чи інструкції з обробки. Коли ви говорите, що він був протестований, я дуже сумніваюся, що він був перевірений на що-небудь схоже на набір тестів на відповідність XML. У цьому полягає проблема всіх спроб обробити XML регексами, які я коли-небудь бачив: вони працюють з невеликою кількістю входів, але не з будь-яким XML, який легально може бути переданий вашій програмі.
Майкл Кей

2
Також є добре сформовані входи, яким регулярний вираз не відповідає. Наприклад, це не дозволяє пробіл після імені в кінцевому тезі. Більшість цих глюків легко виправити, але після виправлення ВСІХ глюків у вас виходить щось абсолютно непридатне. І звичайно справжній прийом полягає в тому, що ви не просто хочете, щоб парсер дав вам відповідь "так / ні", ви хочете, щоб він передав інформацію в додаток, який робить щось корисне з цим.
Майкл Кей

0

Не розбирайте XML / HTML з регулярним виразом, використовуйте правильний XML / HTML-аналізатор та потужний запит.

теорія:

Згідно з теорією компіляції, XML / HTML не може бути розроблений за допомогою регулярного вираження на основі машини з кінцевим станом . У зв'язку з ієрархічною побудовою XML / HTML вам потрібно використовувати автоматичний вимикач та керувати граматикою LALR, використовуючи такий інструмент, як YACC .

realLife © ® ™ повсякденний інструмент в a :

Ви можете скористатися одним із наступних:

xmllint часто встановлюється за замовчуванням з libxml2, xpath1 (перевіряйте мій обгортник, щоб вихідні рядки були обмеженими

xmlstarlet може редагувати, вибирати, трансформувати ... Не встановлено за замовчуванням, xpath1

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

xidel xpath3

саксон-лент мій власний проект, обгортка над бібліотекою Java Саксон-HE HE @Michael Kay, xpath3

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

's lxml( from lxml import etree)

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

, перевірте цей приклад

DOMXpath, перевірте цей приклад


Перевірка: Використання регулярних виразів з тегами HTML

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.