Парсер рекурсивных скобок для выражений строк - PullRequest
0 голосов
/ 02 марта 2019

Предполагая, что у меня есть выражение, аналогичное приведенному ниже (на самом деле это выражение SQL):

"v1 and (v2 and (v3 or v4))"

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

def parse_conditions(expr):
    def _helper(iter):
        items = []
        for item in iter:
            if item == '(':
                result, closeparen = _helper(iter)
                if not closeparen:
                    raise ValueError("Unbalanced parentheses")
                items.append(result)
            elif item == ')':
                return items, True
            else:
                items.append(item)
        return items, False
    return _helper(iter(expr))[0] 

, которая выдает следующий вывод:

print(parse_conditions("v1 and (v2 and (v3 or v4))"))

['v', '1', ' ', 'a', 'n', 'd', ' ', ['v', '2', ' ', 'a', 'n', 'd', ' ', ['v', '3', ' ', 'o', 'r', ' ', 'v', '4']]]

Однако ожидаемый результат будет либо

['v1 and', ['v2 and', ['v3 or v4']]]

или

['v1', and', ['v2', and', ['v3', 'or', 'v4']]]

Есть мысли, как этого добиться?

1 Ответ

0 голосов
/ 02 марта 2019

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

import re

_tokenizer = re.compile(r'\s*([()])\s*').split
def tokenize(s):
    return filter(None, _tokenizer(s))

и использование tokenize()) вместо iter():

def parse_conditions(expr):
    def _helper(tokens):
        items = []
        for item in tokens:
            if item == '(':
                result, closeparen = _helper(tokens)
                if not closeparen:
                    raise ValueError("Unbalanced parentheses")
                items.append(result)
            elif item == ')':
                return items, True
            else:
                items.append(item)
        return items, False
    return _helper(tokenize(expr))[0] 

Вызов filter(None, ...) отфильтровывает пустые строки, которые re.split() генерирует в точках, где ввод начинается или заканчивается ( или ), или если два ( или ) символов непосредственно следуют друг за другом.

Демонстрация:

>>> s = 'v1 and (v2 and (v3 or v4))'
>>> parse_conditions(s)
['v1 and', ['v2 and', ['v3 or v4']]]

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

Разделение на пробелы, где мы не включаем пробелы в токены:

_tokenizer = re.compile(r'(?:([()])|\s+)').split

производит:

>>> parse_conditions(s)
['v1', 'and', ['v2', 'and', ['v3', 'or', 'v4']]]

, тогда как фокусировка на допустимых операторах будет такой:

_tokenizer = re.compile(r'\s*([()]|\b(?:or|and)\b)\s*').split

и для вашего примера ввода, который дает тот же результат.

Обратите внимание, что в вашем коде есть ошибка;он не обнаружит несбалансированную закрывающую круглую скобку:

>>> parse_conditions('foo) and bar')
['foo']

Вам необходимо проверить, что первый _helper() вызов возвращает False для второго элемента в возвращенном кортеже.Вместо return _helper(tokenize(expr))[0] используйте:

items, closing = _helper(tokenize(expr))
if closing:  # indicating there was a closing ) without opening (
    raise ValueError("Unbalanced parentheses")
return items

Наконец, я бы здесь не использовал рекурсию, а вместо этого использовал бы явный стек для замены стека вызовов, на котором строится рекурсия.Ваш собственный стек ограничен только памятью, где стек рекурсии ограничен фиксированным размером (по умолчанию 1000):

def parse_conditions(expr):
    stack = []  # or a `collections.deque()` object, which is a little faster
    top = items = []
    for token in tokenize(expr):
        if token == '(':
            stack.append(items)
            items.append([])
            items = items[-1]
        elif token == ')':
            if not stack:
                raise ValueError("Unbalanced parentheses")
            items = stack.pop()  
        else:
            items.append(token)
    if stack:
        raise ValueError("Unbalanced parentheses")
    return top

Вам может быть интересно посмотреть на модуль tokenize , который реализует токенизатор для кода Python;исходный код использует серию регулярных выражений для разделения исходного кода Python на токены (где токен содержит не только текст токена, но также тип токена , начало и конецпозиции (столбец, кортеж строки) и полная строка, из которой он получен).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...