#python #regex #regex-lookarounds #python-regex #regex-recursion
#python #регулярное выражение #регулярное выражение-поисковые системы #python-regex #регулярное выражение-рекурсия
Вопрос:
Я пытаюсь написать программу, которая проверяет документы, написанные на языке разметки, похожем на BBCode.
Этот язык разметки имеет как совпадающие ( [b]bold[/b] text
), так и несовпадающие ( today is [date]
) теги. К сожалению, использование другого языка разметки не является вариантом.
Однако мое регулярное выражение действует не так, как я хочу. Кажется, что он всегда останавливается на первом соответствующем закрывающем теге вместо того, чтобы идентифицировать этот вложенный тег с рекурсивным (?R)
.
Я использую regex
модуль, который поддерживает (?R)
, а не re
.
Мои вопросы:
-
Как я могу эффективно использовать рекурсивное регулярное выражение для сопоставления вложенных тегов без завершения на первом теге?
-
Если есть метод лучше, чем регулярное выражение, что это за метод?
Вот регулярное выражение, как только я его создам: [(b|i|u|h1|h2|h3|large|small|list|table|grid)](?:((?![/1]).)*?|(?R))*[/1]
Вот тестовая строка, которая работает не так, как ожидалось: [large]test1 [large]test2[/large] test3[/large]
(она должна соответствовать всей этой строке, но останавливается перед test3)
Вот регулярное выражение на regex101.com: https://regex101.com/r/laJSLZ/1
Этот тест не обязательно должен завершаться за миллисекунды или даже секунды, но он должен иметь возможность проверять около 100 файлов по 1000-10 000 символов каждый за время, приемлемое для сборки Travis-CI.
Вот как выглядит логика, использующая это регулярное выражение, для контекста:
import io, regex # https://pypi.org/project/regex/
# All the tags that must have opening and closing tags
matching_tags = 'b', 'i', 'u', 'h1', 'h2', 'h3', 'large', 'small', 'list', 'table', 'grid'
# our first part matches an opening tag:
# [(b|i|u|h1|h2|h3|large|small|list|table|grid)]
# our middle part matches the text in the middle, including any properly formed tag sets in between:
# (?:((?![/1]).)*?|(?R))*
# our last part matches the closing tag for our first match:
# [/1]
pattern = r'[(' '|'.join(matching_tags) r')](?:((?![/1]).)*?|(?R))*[/1]'
myRegex = re.compile(pattern)
data = ''
with open('input.txt', 'r') as file:
data = '[br]'.join(file.readlines())
def validate(text):
valid = True
for node in all_nodes(text):
valid = valid and is_valid(node)
return valid
# (Only important thing here is that I call this on every node, this
# should work fine but the regex to get me those nodes does not.)
# markup should be valid iff opening and closing tag counts are equal
# in the whole file, in each matching top-level pair of tags, and in
# each child all the way down to the smallest unit (a string that has
# no tags at all)
def is_valid(text):
valid = True
for tag in matching_tags:
valid = valid and text.count(f'[{tag}]') == text.count(f'[/{tag}]')
return valid
# this returns each child of the text given to it
# this call:
# all_nodes('[b]some [large]text to[/large] validate [i]with [u]regex[/u]![/i] love[/b] to use [b]regex to [i]do stuff[/i][/b]')
# should return a list containing these strings:
# [b]some [large]text to[/large] validate [i]with [u]regex[/u]![/i] love[/b]
# [large]text to[/large]
# [i]with [u]regex[/u]![/i]
# [u]regex[/u]
# [b]regex to [i]do stuff[/i][/b]
# [i]do stuff[/i]
def all_nodes(text):
matches = myRegex.findall(text)
if len(matches) > 0:
for m in matches:
result = all_nodes(m)
return result
exit(0 if validate(data) else 1)
Комментарии:
1. Смотрите regex101.com/r/ryKfmP/2
Ответ №1:
Ваша основная проблема заключается в ((?![/1]).)*?
жадном токене temperated.
Во-первых, это неэффективно, поскольку вы определяете ее количественно, а затем вы определяете всю группу, в которой она находится, поэтому заставляете механизм регулярных выражений искать больше способов сопоставления строки, и это делает его довольно хрупким.
Во-вторых, вы соответствуете только закрывающему тегу и не ограничиваете начальный тег. Первый шаг — сделать /
before 1
необязательным, /?
. Это не остановится перед [tag]
тегами like без атрибутов. Чтобы добавить поддержку атрибутов, добавьте необязательную группу после 1
, (?:s[^]]*)?
. Оно соответствует необязательной последовательности пробелов, а затем любым символам 0 , отличным от ]
.
Фиксированное регулярное выражение будет выглядеть следующим образом
[([biu]|h[123]|l(?:arge|ist)|small|table|grid)](?:(?![/?1(?:s[^]]*)?]).|(?R))*[/1]
Не забудьте скомпилировать ее с regex.DOTALL
, чтобы она соответствовала нескольким новым строкам.
Комментарии:
1. Большое спасибо за объяснение, Виктор! Это работает отлично и намного быстрее, чем я ожидал, что это может быть.