Как я могу использовать рекурсивное регулярное выражение или другой метод для рекурсивной проверки этой разметки, подобной BBCode, в Python?

#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. Большое спасибо за объяснение, Виктор! Это работает отлично и намного быстрее, чем я ожидал, что это может быть.