Как написать парсер скобочной грамматики с заданным набором скобок "<> [] () -+"
Всем привет!
Ради интереса написала парсер c простой скобочной грамматикой (пока без проверки ошибок). Код ниже.
Здесь открывающей скобке (, соответствует закрывающая - ), [ - ], l - r, < - >, то есть порядок в LEFT_BRACKETS и RIGHT_BRACKETS одинаковый. Для задачи все скобки уникальны.
Вопрос: Читала, что это гораздо эффективнее делается помощью конечного автомата, но не могу понять, что для грамматики является состоянием КА, а что переходом? Подскажите, пожалуйста, куда копать, чтобы понять, как это лучше написать.
my_string = "<[(()(()))(()lrl()<><><<<>>>[]r)]>"
class node:
LEFT_BRACKETS = ["(", "[", "l", "<"]
RIGHT_BRACKETS = [")", "]", "r", ">"]
MAX_INT = 100000000000000000000000000000
is_open: bool
is_close: bool
parent: object
word: str
bracket_index: int
children: list
current_sym_index: int
def __init__(self, parent):
self.parent = parent
self.word = ""
self.is_open = False
self.is_close = False
self.bracket_index = node.MAX_INT
self.children = []
def parse(self, symbol):
if self.is_close:
return
if not self.is_open and symbol in node.LEFT_BRACKETS:
self.is_open = True
self.bracket_index = node.LEFT_BRACKETS.index(symbol)
elif self.is_open and self.no_children_open() and symbol in node.RIGHT_BRACKETS and node.RIGHT_BRACKETS[self.bracket_index] == symbol:
self.is_close = True
self.is_open = False
elif self.is_open:
if symbol in node.LEFT_BRACKETS and self.no_children_open():
self.children.append(node(self))
self.word += symbol
for child in self.children:
child.parse(symbol)
def no_children_open(self, no_open = True):
for child in self.children:
no_open = no_open and child.is_close
return no_open
def print(self):
print("--" + node.LEFT_BRACKETS[self.bracket_index]
+ node.RIGHT_BRACKETS[self.bracket_index])
print(self.word)
for child in self.children:
child.print()
_node: node = node(None)
for symbol in my_string:
_node.parse(symbol=symbol)
_node.print()
Ответы (1 шт):
Если ограничиться только проверкой валидности последовательности, то решение со стеком (как описано в комментарии @stanislav-volodarskiy) может быть таким:
class BracketParser:
def __init__(self, brackets):
self.brackets = {bracket_type[0]: bracket_type[1]
for bracket_type in brackets}
def is_valid_brackets(self, parse_string):
stack = []
for symbol in parse_string:
if symbol in self.brackets.keys():
stack.append(self.brackets[symbol])
if symbol in self.brackets.values():
if not stack or stack.pop() != symbol:
return False
return stack == []
Причем методу is_valid_brackets не нужно проходить полностью по строке - при выявлении ошибки в процессе выполнения сразу происходит возврат с False.
Так как такой метод менее ресурсоемкий (максимум 1 полное прохождение по строке), то для реального использования можно сначала проверять строку на корректность им, а в случае ошибки - запускать более подробную проверку как у Вас.
У меня получилось написать парсер без использования списка детей, т.к. по ходу работы он максимум пытается вернуться назад по цепи родителей в поисках подходящей более ранней скобки:
class BracketNode:
def __init__(self, index, bracket, parent):
self.index = index
self.bracket = bracket
self.parent = parent
def get_node_indices(self):
indices = [self.index]
last_node = self.parent
while last_node:
indices.append(last_node.index)
last_node = last_node.parent
return indices
class BracketParser:
def __init__(self, brackets):
self.brackets = {bracket_type[0]: bracket_type[1]
for bracket_type in brackets}
def parse(self, parse_string):
if self.is_valid_brackets(parse_string, self.brackets):
return True, []
parse_errors = self.full_bracket_parse(parse_string, self.brackets)
if len(parse_errors) > 1:
parse_errors_from_end = self.full_bracket_parse(
parse_string[::-1],
{v: k for k, v in self.brackets.items()}
)
reversed_parse_errors = [len(parse_string) - i - 1 for i in parse_errors_from_end]
parse_errors = min(parse_errors, reversed_parse_errors, key=len)
return False, sorted(parse_errors)
@staticmethod
def is_valid_brackets(parse_string, bracket_mapping):
stack = []
for symbol in parse_string:
if symbol in bracket_mapping.keys():
stack.append(bracket_mapping[symbol])
if symbol in bracket_mapping.values():
if not stack or stack.pop() != symbol:
return False
return stack == []
@staticmethod
def full_bracket_parse(parse_string, bracket_mapping):
error_stack = []
current_node = None
for idx, symbol in enumerate(parse_string):
if symbol in bracket_mapping.keys():
current_node = BracketNode(idx, bracket_mapping[symbol], current_node)
if symbol in bracket_mapping.values():
probable_error_stack = []
last_node = current_node
while last_node:
if symbol == last_node.bracket:
current_node = last_node.parent
error_stack.extend(probable_error_stack)
break
probable_error_stack.append(last_node.index)
last_node = last_node.parent
else:
error_stack.append(idx)
if current_node:
error_stack.extend(current_node.get_node_indices())
return error_stack
Если строка определяется как невалидная быстрым методом, то вызываем метод полной проверки с учетом предшествующих скобок:
- Храним последний узел (в самом начале
None) и накапливаемerror_stackс индексами ошибочных скобок; - Если скобка левая - создаем новый узел с ссылкой на прошлый актуальный (у первого корневого в последовательности будет None) и указанием требуемой правой скобки;
- Если скобка правая - входим в цикл итерации от последнего узла до корневого (по
parent is None) и проверяем, совпадает ли символ с ранее сохраненной скобкой: -
- Если не находит совпадение до родительского включительно, то считаем проверяемую скобку некорректной. Оставляем текущий узел нетронутым;
-
- Если в процессе находит совпадение, то считаем некорректными скобки в промежутке между проверяемой и найденной (возможно и нет промежутка, если совпадение было сразу же). Меняем узел на родителя удачного узла (по сути забываем про успешный узел) - в т.ч. текущий узел может стать обратно None, если совпадение было с корневым.
- В случае, если по окончании проверки строки последний узел не None, значит в нём накоплены невостребованные левые скобки - получаем их индексы и добавляем к
error_stack.
В результате получаем перечень индексов некорректных скобок из исходной строки и можем как показать ошибки, так и исправить их, например, такими методами (со @staticmethod добавляется к классу парсера, а без них - используется просто как функции):
@staticmethod
def get_error_string(origin_string, error_positions):
if not error_positions:
return f'{origin_string}\nNo error symbols'
error_string = ''
for idx, symbol in enumerate(origin_string):
error_string += '^' if idx in error_positions else ' '
return f'{origin_string}\n{error_string}'
@staticmethod
def get_valid_string(origin_string, error_positions):
if not error_positions:
return origin_string
valid_string = ''
for idx, symbol in enumerate(origin_string):
valid_string += symbol if idx not in error_positions else ''
return valid_string or 'No valid symbols'
Пример вызова:
_brackets = ['()', '[]', '<>']
parser = BracketParser(_brackets)
_string = '<[(((()(()))(()()<><><<<>>>[])]>'
is_valid, errors = parser.parse(_string)
print(parser.get_error_string(_string, errors))
# <[(((()(()))(()()<><><<<>>>[])]>
# ^^
Но с таким алгоритмом есть нюанс - в основе лежит приоритет предыдущих скобок, а возможные будущие никак не учитываются. К чему это приводит:
# Добавим внутрь последовательности круглых скобок квадратную
_string = '<[(()(()))(()([)<><><<<>>>[])]>'
# <[(()(()))(()([)<><><<<>>>[])]>
# ^
# Всё нормально, указывает на корректную лишнюю скобку
# "Повернем" скобку
_string = '<[(()(()))(()(])<><><<<>>>[])]>'
# <[(()(()))(()(])<><><<<>>>[])]>
# ^ ^ ^ ^^
# Алгоритм преобразует к следующей последовательности:
# <[(()(()))()]<><><<<>>>[]>
Сама последовательность верная, но операций по исправлению требуется намного больше, чем могло бы быть. Для того, чтобы отчасти это исправить парсинг делается как с начала строки, так и с конца (если в первом случае требуется более 1 исправления) - из них выбирается вариант с наименьшим числом исправлений.
В результате получается более приемлемый результат:
_string = '<[(()(()))(()([)<><><<<>>>[])]>'
# <[(()(()))(()(])<><><<<>>>[])]>
# ^
Но всё ещё можно "споткнуться" о такой вариант:
_string = '[((][)())]'
# [((][)())]
# ^^ ^ ^
# Здесь достаточно убрать ][