Как я могу использовать рекурсивное регулярное выражение или другой метод для рекурсивной проверки этой BBcode-подобной разметки в Python? - PullRequest
1 голос
/ 28 марта 2019

Я пытаюсь написать программу, которая проверяет документы, написанные на языке разметки, похожем на 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 до 10000 символов в каждый раз за время, разумное для сборки 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 Ответ

1 голос
/ 28 марта 2019

Ваша главная проблема в жадном жетоне ((?!\[\/\1\]).)*?.

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

Во-вторых, вы соответствуете только закрывающему тегу и не ограничивает начальный тег.Первый шаг - сделать / перед \1 необязательным, \/?.Он не остановится до [tag] как теги без атрибутов.Чтобы добавить поддержку атрибутов, добавьте необязательную группу после \1, (?:\s[^]]*)?.Он соответствует необязательной последовательности пробелов, а затем любые 0+ символов, отличные от ].

Фиксированное регулярное выражение будет иметь вид

\[([biu]|h[123]|l(?:arge|ist)|small|table|grid)](?:(?!\[/?\1(?:\s[^]]*)?]).|(?R))*\[/\1]

Не забудьте скомпилировать его с regex.DOTALL чтобы соответствовать нескольким переводам строки.

...