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

Кажется, что на каждый вопрос о stackoverflow, когда задающий вопрос использует регулярное выражение для получения некоторой информации из HTML, неизбежно будет «ответ», в котором говорится не использовать регулярное выражение для анализа HTML.

Почему нет? Я знаю, что существуют "настоящие" HTML-парсеры без кавычек, такие как Beautiful Soup , и я уверен, что они мощные и полезные, но если вы просто делаете что-то простое, быстрое или грязное, тогда почему беспокоиться об использовании чего-то настолько сложного, когда несколько операторов регулярных выражений будут работать нормально?

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

Ответов (18)

Решение

Полный анализ HTML невозможен с регулярными выражениями, так как он зависит от соответствия открывающего и закрывающего тегов, что невозможно с регулярными выражениями.

Регулярные выражения могут соответствовать только регулярным языкам, но HTML - это контекстно-свободный язык, а не обычный язык (как отметил @StefanPochmann, регулярные языки также контекстно-свободны, поэтому контекстно-свободный язык не обязательно означает не регулярный). Единственное, что вы можете делать с регулярными выражениями в HTML, - это эвристика, но она работает не во всех условиях. Должна быть возможность представить файл HTML, который будет неправильно соответствовать любому регулярному выражению.

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

Имейте в виду, что, хотя сам HTML не является регулярным, части просматриваемой страницы могут быть обычными.

Например, размещение <form> тегов является ошибкой ; если веб-страница работает правильно, то использование регулярного выражения для захвата <form> было бы вполне разумным.

Недавно я провел парсинг веб-страниц, используя только Selenium и регулярные выражения. Я ушел с ним , потому что данные , которые я хотел было положить в <form>, и поставить в простом формате таблицы (так что я мог рассчитывать даже на <table>, <tr> и <td> быть невложенных - что на самом деле очень необычно). В некоторой степени регулярные выражения были даже почти необходимы, потому что часть структуры, к которой мне нужно было получить доступ, была ограничена комментариями. (Beautiful Soup может оставлять вам комментарии, но было бы сложно захватить <!-- BEGIN --> и <!-- END --> заблокировать их с помощью Beautiful Soup.)

Однако, если бы мне пришлось беспокоиться о вложенных таблицах, мой подход просто не сработал бы! Мне бы пришлось вернуться к Beautiful Soup. Однако даже в этом случае иногда можно использовать регулярное выражение, чтобы захватить нужный фрагмент, а затем развернуть его оттуда.

Собственно, синтаксический анализ HTML с помощью регулярного выражения вполне возможен в PHP. Вам просто нужно проанализировать всю строку в обратном направлении, используя, strrpos чтобы найти < и повторить регулярное выражение оттуда, используя неловкие спецификаторы каждый раз, чтобы преодолеть вложенные теги. Не изящный и ужасно медленный для больших вещей, но я использовал его для своего личного редактора шаблонов для своего веб-сайта. На самом деле я не разбирал HTML, а создал несколько настраиваемых тегов для запроса записей базы данных для отображения таблиц данных (мой <#if()> тег мог таким образом выделять специальные записи). Я не был готов использовать синтаксический анализатор XML только для пары самостоятельно созданных тегов (с очень не XML-данными внутри них) здесь и там.

Таким образом, хотя этот вопрос в значительной степени мертв, он все равно появляется в поиске Google. Я прочитал его и подумал, что «вызов принят», и закончил исправлять свой простой код, не заменяя все. Решил предложить другое мнение всем, кто ищет по аналогичной причине. Также последний ответ был опубликован 4 часа назад, так что это все еще актуальная тема.

http://htmlparsing.com/regexes )

Допустим, у вас есть файл HTML, в котором вы пытаетесь извлечь URL-адреса из тегов <img>.

<img src="http://example.com/whatever.jpg">

Итак, вы пишете на Perl такое регулярное выражение:

if ( $html =~ /<img src="(.+)"/ ) {
    $url = $1;
}

В этом случае $url действительно будет содержать http://example.com/whatever.jpg . Но что происходит, когда вы начинаете получать HTML вот так:

<img src='http://example.com/whatever.jpg'>

или

<img src=http://example.com/whatever.jpg>

или

<img border=0 src="http://example.com/whatever.jpg">

или

<img
    src="http://example.com/whatever.jpg">

или вы начинаете получать ложные срабатывания от

<!-- // commented out
<img src="http://example.com/outdated.png">
-->

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

Знаешь ... есть много мыслей о том, что ты НЕ МОЖЕШЬ этого сделать, и я думаю, что все по обе стороны забора правы и неправы. Вы МОЖЕТЕ это сделать, но это требует немного больше обработки, чем просто запуск одного регулярного выражения. Возьмите это (я написал это за час) в качестве примера. Предполагается, что HTML полностью действителен, но в зависимости от того, какой язык вы используете для применения вышеупомянутого регулярного выражения, вы можете внести некоторые исправления в HTML, чтобы убедиться, что он будет успешным. Например, удаление закрывающих тегов, которых там не должно быть: </img>например. Затем добавьте закрывающую косую черту HTML к элементам, в которых они отсутствуют, и т. Д.

Я бы использовал это в контексте написания библиотеки, которая позволила бы мне выполнять поиск HTML-элементов [x].getElementsByTagName(), например , как в JavaScript . Я бы просто разделил функциональность, которую я написал в разделе DEFINE регулярного выражения, и использовал бы ее для перехода внутри дерева элементов, по одному за раз.

Итак, будет ли это окончательным 100% ответом на проверку HTML? Нет. Но это только начало, и немного поработав, это можно сделать. Однако пытаться сделать это внутри одного выполнения регулярного выражения непрактично и неэффективно.

Это выражение извлекает атрибуты из элементов HTML. Он поддерживает:

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

(?:\<\!\-\-(?:(?!\-\-\>)\r\n?|\n|.)*?-\-\>)|(?:<(\S+)\s+(?=.*>)|(?<=[=\s])\G)(?:((?:(?!\s|=).)*)\s*?=\s*?[\"']?((?:(?<=\")(?:(?<=\\)\"|[^\"])*|(?<=')(?:(?<=\\)'|[^'])*)|(?:(?!\"|')(?:(?!\/>|>|\s).)+))[\"']?\s*)

Проверить это . Лучше работает с флагами gisx, как в демонстрации.

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

Используйте с параметрами 'sx'. "g" тоже, если вам повезет:

(?P<content>.*?)                # Content up to next tag
(?P<markup>                     # Entire tag
  <!\[CDATA\[(?P<cdata>.+?)]]>| # <![CDATA[ ... ]]>
  <!--(?P<comment>.+?)-->|      # <!-- Comment -->
  </\s*(?P<close_tag>\w+)\s*>|  # </tag>
  <(?P<tag>\w+)                 # <tag ...
    (?P<attributes>
      (?P<attribute>\s+
# <snip>: Use this part to get the attributes out of 'attributes' group.
        (?P<attribute_name>\w+)
        (?:\s*=\s*
          (?P<attribute_value>
            [\w:/.\-]+|         # Unquoted
            (?=(?P<_v>          # Quoted
              (?P<_q>['\"]).*?(?<!\\)(?P=_q)))
            (?P=_v)
          ))?
# </snip>
      )*
    )\s*
  (?P<is_self_closing>/?)   # Self-closing indicator
  >)                        # End of tag

Этот разработан для Python (он может работать для других языков, не пробовал, он использует положительный просмотр вперед, отрицательный просмотр назад и именованные обратные ссылки). Поддерживает:

  • Открыть тег - <div ...>
  • Закрыть тег - </div>
  • Комментарий - <!-- ... -->
  • CDATA - <![CDATA[ ... ]]>
  • Самозакрывающийся тег - <div .../>
  • Необязательные значения атрибутов - <input checked>
  • Значения атрибутов без кавычек / кавычек - <div style='...'>
  • Одиночные / двойные кавычки - <div style="...">
  • Экранированные цитаты - <a title='John\'s Story'>
    (это не совсем правильный HTML, но я хороший парень)
  • Пространства вокруг знаков равенства - <a href = '...'>
  • Именованные захваты для интересных битов

Также неплохо не запускаться по неверно сформированным тегам, например, когда вы забываете < или > .

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

  • content- Все содержимое до следующего тега. Вы можете оставить это без внимания.
  • markup - Весь тег со всем, что в нем.
  • comment - Если это комментарий, то его содержание.
  • cdata- Если это <![CDATA[...]]>, то содержимое CDATA.
  • close_tag- Если это закрывающий тег ( </div>), имя тега.
  • tag- Если это открытый тег ( <div>), имя тега.
  • attributes- Все атрибуты внутри тега. Используйте это, чтобы получить все атрибуты, если у вас нет повторяющихся групп.
  • attribute - Повторяется каждый атрибут.
  • attribute_name - Повторяется, имя каждого атрибута.
  • attribute_value- Повторяется, значение каждого атрибута. Сюда входят кавычки, если они были процитированы.
  • is_self_closing- Это /если это самозакрывающийся тег, иначе ничего.
  • _qи _v- игнорировать их; они используются внутри для обратных ссылок.

Если ваш механизм регулярных выражений не поддерживает повторяющиеся именованные захваты, вызывается раздел, который вы можете использовать для получения каждого атрибута. Просто запустите , что регулярное выражение на attributes группы , чтобы получить каждый attribute, attribute_name и attribute_value из него.

Демо здесь: https://regex101.com/r/mH8jSu/11

HTML / XML делится на разметку и контент. Регулярное выражение полезно только при синтаксическом анализе лексического тега. Я думаю, вы могли бы вывести содержание. Это был бы хороший выбор для парсера SAX. Теги и контент могут быть доставлены в определяемую пользователем функцию, где можно отслеживать вложение / закрытие элементов.

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

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

Нормальные элементы анализируются с помощью этой формы:

Ядро этих тегов использует это регулярное выражение

 (?:
      " [\S\s]*? " 
   |  ' [\S\s]*? ' 
   |  [^>]? 
 )+

Вы заметите это [^>]? как одно из изменений. Это будет соответствовать несбалансированным кавычкам из неправильно сформированных тегов.

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

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

Это общая форма для простых старых тегов. Обратите внимание на [\w:] представление имени тега? На самом деле допустимые символы, представляющие имя тега, представляют собой невероятный список символов Unicode.

 <     
 (?:
      [\w:]+ 
      \s+ 
      (?:
           " [\S\s]*? " 
        |  ' [\S\s]*? ' 
        |  [^>]? 
      )+
      \s* /?
 )
 >

Двигаясь дальше, мы также видим, что вы просто не можете искать определенный тег, не проанализировав ВСЕ теги. Я имею в виду, что вы могли бы, но для этого нужно было бы использовать комбинацию глаголов, например (* SKIP) (* FAIL), но все же все теги должны быть проанализированы.

Причина в том, что синтаксис тега может быть скрыт внутри других тегов и т. Д.

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

По мере того, как новый HTML, xml или любой другой разрабатывает новые конструкции, просто добавьте его как одну из альтернатив.


Примечание к веб-странице - я никогда не видел веб-страницу (или xhtml / xml), с которой у этого
были бы проблемы. Если найдешь, дай мне знать.

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


Полное необработанное регулярное выражение

<(?:(?:(?:(script|style|object|embed|applet|noframes|noscript|noembed)(?:\s+(?>"[\S\s]*?"|'[\S\s]*?'|(?:(?!/>)[^>])?)+)?\s*>)[\S\s]*?</\1\s*(?=>))|(?:/?[\w:]+\s*/?)|(?:[\w:]+\s+(?:"[\S\s]*?"|'[\S\s]*?'|[^>]?)+\s*/?)|\?[\S\s]*?\?|(?:!(?:(?:DOCTYPE[\S\s]*?)|(?:\[CDATA\[[\S\s]*?\]\])|(?:--[\S\s]*?--)|(?:ATTLIST[\S\s]*?)|(?:ENTITY[\S\s]*?)|(?:ELEMENT[\S\s]*?))))>

Форматированный вид

 <
 (?:
      (?:
           (?:
                # Invisible content; end tag req'd
                (                             # (1 start)
                     script
                  |  style
                  |  object
                  |  embed
                  |  applet
                  |  noframes
                  |  noscript
                  |  noembed 
                )                             # (1 end)
                (?:
                     \s+ 
                     (?>
                          " [\S\s]*? "
                       |  ' [\S\s]*? '
                       |  (?:
                               (?! /> )
                               [^>] 
                          )?
                     )+
                )?
                \s* >
           )

           [\S\s]*? </ \1 \s* 
           (?= > )
      )

   |  (?: /? [\w:]+ \s* /? )
   |  (?:
           [\w:]+ 
           \s+ 
           (?:
                " [\S\s]*? " 
             |  ' [\S\s]*? ' 
             |  [^>]? 
           )+
           \s* /?
      )
   |  \? [\S\s]*? \?
   |  (?:
           !
           (?:
                (?: DOCTYPE [\S\s]*? )
             |  (?: \[CDATA\[ [\S\s]*? \]\] )
             |  (?: -- [\S\s]*? -- )
             |  (?: ATTLIST [\S\s]*? )
             |  (?: ENTITY [\S\s]*? )
             |  (?: ELEMENT [\S\s]*? )
           )
      )
 )
 >

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

Причина в том, что регулярные выражения не могут обрабатывать произвольно вложенные выражения. См. Можно ли использовать регулярные выражения для сопоставления вложенных шаблонов?

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

Регулярные выражения недостаточно эффективны для такого языка, как HTML. Конечно, есть несколько примеров, в которых можно использовать регулярные выражения. Но в целом для разбора не подходит.

Две быстрые причины:

  • написать регулярное выражение, которое может противостоять злонамеренному вводу, сложно; намного сложнее, чем использовать готовый инструмент
  • написать регулярное выражение, которое может работать с нелепой разметкой, с которой вы неизбежно застрянете, сложно; намного сложнее, чем использовать готовый инструмент

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

Проблема в том, что большинство пользователей, которые задают вопрос, связанный с HTML и регулярным выражением, делают это, потому что не могут найти собственное регулярное выражение, которое работает. Затем нужно подумать, будет ли все проще при использовании парсера DOM или SAX или чего-то подобного. Они оптимизированы и созданы для работы с XML-подобными структурами документов.

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

Если вы просто хотите найти все URL-адреса, которые выглядят так, как будто http://.../ вы в порядке с регулярными выражениями. Но если вы хотите найти все URL-адреса в a-элементе, который имеет класс mylink, вам, вероятно, лучше использовать соответствующий синтаксический анализатор.

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

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

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

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

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

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

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

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

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

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