Python: как сопоставить вложенные скобки с регулярным выражением?

Я пытаюсь сопоставить строку, подобную математическому выражению, с вложенными скобками.

import re

p = re.compile('(.+)')
str = '(((1+0)+1)+1)'
print p.findall(s)

['(((1+0)+1)+1)']

Я хотел, чтобы он соответствовал всем вложенным выражениям, таким как(1+0), ((1+0)+1)...
Мне даже все равно, соответствует ли он нежелательным, таким как ((1+0), я могу позаботиться о них.

почему он этого еще не делает, и как я могу это сделать?

12 ответов


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

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


как уже упоминалось, регулярные выражения не подходят для вложенных конструкций. Я приведу основной пример, используя pyparsing:

import pyparsing # make sure you have this installed

thecontent = pyparsing.Word(pyparsing.alphanums) | '+' | '-'
parens     = pyparsing.nestedExpr( '(', ')', content=thecontent)

вот пример использования:

>>> parens.parseString("((a + b) + c)")

выход:

(                          # all of str
 [
  (                        # ((a + b) + c)
   [
    (                      #  (a + b)
     ['a', '+', 'b'], {}   
    ),                     #  (a + b)      [closed]
    '+',
    'c'
   ], {}
  )                        # ((a + b) + c) [closed]
 ], {}  
)                          # all of str    [closed]

(С newlining/отступов/комментариев сделано вручную)

Edit: изменено для устранения ненужных Forward, согласно предложениям пола Макгвайра.

получить выход в формате вложенного списка:

res = parens.parseString("((12 + 2) + 3)")
res.asList()

выход:

[[['12', '+', '2'], '+', '3']]

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

import regex

s = 'aaa(((1+0)+1)+1)bbb'

result = regex.search(r'''
(?<rec> #capturing group rec
 \( #open parenthesis
 (?: #non-capturing group
  [^()]++ #anyting but parenthesis one or more times without backtracking
  | #or
   (?&rec) #recursive substitute of group rec
 )*
 \) #close parenthesis
)
''',s,flags=regex.VERBOSE)


print(result.captures('rec'))

выход:

['(1+0)', '((1+0)+1)', '(((1+0)+1)+1)']

связанная ошибка в regex: http://code.google.com/p/mrab-regex-hg/issues/detail?id=78


языки регулярных выражений недостаточно мощны для сопоставления произвольно вложенных конструкций. Для этого вам нужен push-down автомат (т. е. парсер). Существует несколько таких инструментов, таких как PLY.

Python также предоставляет библиотека парсер для собственного синтаксиса, который может сделать то, что вам нужно. Однако результат чрезвычайно детализирован и занимает некоторое время, чтобы обернуть голову. Если вы заинтересованы в этом ракурсе, следующее обсуждение пытается объясните все как можно проще.

>>> import parser, pprint
>>> pprint.pprint(parser.st2list(parser.expr('(((1+0)+1)+1)')))
[258,
 [327,
  [304,
   [305,
    [306,
     [307,
      [308,
       [310,
        [311,
         [312,
          [313,
           [314,
            [315,
             [316,
              [317,
               [318,
                [7, '('],
                [320,
                 [304,
                  [305,
                   [306,
                    [307,
                     [308,
                      [310,
                       [311,
                        [312,
                         [313,
                          [314,
                           [315,
                            [316,
                             [317,
                              [318,
                               [7, '('],
                               [320,
                                [304,
                                 [305,
                                  [306,
                                   [307,
                                    [308,
                                     [310,
                                      [311,
                                       [312,
                                        [313,
                                         [314,
                                          [315,
                                           [316,
                                            [317,
                                             [318,
                                              [7,
                                               '('],
                                              [320,
                                               [304,
                                                [305,
                                                 [306,
                                                  [307,
                                                   [308,
                                                    [310,
                                                     [311,
                                                      [312,
                                                       [313,
                                                        [314,
                                                         [315,
                                                          [316,
                                                           [317,
                                                            [318,
                                                             [2,
                                                              '1']]]]],
                                                         [14,
                                                          '+'],
                                                         [315,
                                                          [316,
                                                           [317,
                                                            [318,
                                                             [2,
                                                              '0']]]]]]]]]]]]]]]],
                                              [8,
                                               ')']]]]],
                                          [14,
                                           '+'],
                                          [315,
                                           [316,
                                            [317,
                                             [318,
                                              [2,
                                               '1']]]]]]]]]]]]]]]],
                               [8, ')']]]]],
                           [14, '+'],
                           [315,
                            [316,
                             [317,
                              [318, [2, '1']]]]]]]]]]]]]]]],
                [8, ')']]]]]]]]]]]]]]]],
 [4, ''],
 [0, '']]

вы можете облегчить боль с этой короткой функции:

def shallow(ast):
    if not isinstance(ast, list): return ast
    if len(ast) == 2: return shallow(ast[1])
    return [ast[0]] + [shallow(a) for a in ast[1:]]

>>> pprint.pprint(shallow(parser.st2list(parser.expr('(((1+0)+1)+1)'))))
[258,
 [318,
  '(',
  [314,
   [318, '(', [314, [318, '(', [314, '1', '+', '0'], ')'], '+', '1'], ')'],
   '+',
   '1'],
  ')'],
 '',
 '']

числа поступают из модулей Python symbol и token, который вы можете использовать для создания таблицы поиска от чисел до имен:

map = dict(token.tok_name.items() + symbol.sym_name.items())

вы даже можете сложить это отображение в shallow() функция, чтобы вы могли работать со строками вместо чисел:

def shallow(ast):
    if not isinstance(ast, list): return ast
    if len(ast) == 2: return shallow(ast[1])
    return [map[ast[0]]] + [shallow(a) for a in ast[1:]]

>>> pprint.pprint(shallow(parser.st2list(parser.expr('(((1+0)+1)+1)'))))
['eval_input',
 ['atom',
  '(',
  ['arith_expr',
   ['atom',
    '(',
    ['arith_expr',
     ['atom', '(', ['arith_expr', '1', '+', '0'], ')'],
     '+',
     '1'],
    ')'],
   '+',
   '1'],
  ')'],
 '',
 '']

стек является лучшим инструментом для работы: -

import re
def matches(line, opendelim='(', closedelim=')'):
    stack = []

    for m in re.finditer(r'[{}{}]'.format(opendelim, closedelim), line):
        pos = m.start()

        if line[pos-1] == '\':
            # skip escape sequence
            continue

        c = line[pos]

        if c == opendelim:
            stack.append(pos+1)

        elif c == closedelim:
            if len(stack) > 0:
                prevpos = stack.pop()
                # print("matched", prevpos, pos, line[prevpos:pos])
                yield (prevpos, pos, len(stack))
            else:
                # error
                print("encountered extraneous closing quote at pos {}: '{}'".format(pos, line[pos:] ))
                pass

    if len(stack) > 0:
        for pos in stack:
            print("expecting closing quote to match open quote starting at: '{}'"
                  .format(line[pos-1:]))

в клиентском коде, поскольку функция записывается как функция генератора, просто используйте шаблон цикла for для развертывания совпадений: -

line = '(((1+0)+1)+1)'
for openpos, closepos, level in matches(line):
    print(line[openpos:closepos], level)

этот тестовый код производит следующее На моем экране, заметил, что второй параметр в распечатке указывает глубина в скобках.

1+0 2
(1+0)+1 1
((1+0)+1)+1 0

из связанного ответа:

из утилиты lilypond convert-ly (и написанной / защищенной авторским правом, поэтому я могу показать ее здесь):

def paren_matcher (n):
    # poor man's matched paren scanning, gives up
    # after n+1 levels.  Matches any string with balanced
    # parens inside; add the outer parens yourself if needed.
    # Nongreedy.
    return r"[^()]*?(?:\("*n+r"[^()]*?"+r"\)[^()]*?)*?"*n

convert-ly имеет тенденцию использовать это как paren_matcher (25) в своих регулярных выражениях, что, вероятно, излишне для большинства приложений. Но затем он использует его для сопоставления выражений схемы.

Да, он ломается после заданного предела, но возможность просто подключить его к регулярным выражениям все еще бьет "правильные" альтернативы, поддерживающие неограниченную глубину в удобстве использования.


сбалансированные пары (например, круглые скобки) - это пример языка, который не может быть распознан регулярными выражениями.

ниже приводится краткое объяснение математики, почему это так.

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

например, состояние можно использовать для подсчета, скажем, несопоставленных левых скобок. Но поскольку количество состояний для такого рода подсчета должно быть полностью ограничено, то данный FSM может рассчитывать максимум n-1, где n - число состояний ФШМ может быть. Если n, скажем, 10, тогда максимальное число несопоставленных левых скобок, которые может сопоставить FSM, равно 10, пока он разрыв. Поскольку вполне возможно иметь еще одну левую скобку, нет никакого возможного FSM, который может правильно распознать полный язык согласованных скобок.

ну и что? Предположим, вы просто выбираете действительно большой n? Проблема в том, что как способ описания FSM регулярные выражения в основном описывают все переход из одного состояния в другое. Поскольку для любого N, FSM потребуется 2 перехода состояния (один для соответствия левому скобки, и один для соответствия справа), само регулярное выражение должно расти, по крайней мере, постоянным фактором, кратным n

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

expression ::= `(` expression `)` expression
           |    nothing
 

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


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

import re

def scan(p, string):
    found = p.findall(string)
    for substring in found:
        stripped = substring[1:-1]
        found.extend(scan(p, stripped))
    return found

p = re.compile('\(.+\)')
string = '(((1+0)+1)+1)'
all_found = scan(p, string)
print all_found

этот код, однако,не соответствует "правильным" скобкам. Если вам нужно это сделать, вам будет лучше с помощью специализированного парсера.


вот демо для вашего вопроса, хотя он неуклюжий, пока он работает

import re s = '(((1+0)+1)+1)'

def getContectWithinBraces( x , *args , **kwargs):
    ptn = r'[%(left)s]([^%(left)s%(right)s]*)[%(right)s]' %kwargs
    Res = []
    res = re.findall(ptn , x)
    while res != []:
        Res = Res + res
        xx = x.replace('(%s)' %Res[-1] , '%s')
        res = re.findall(ptn, xx)
        print(res)
        if res != []:
            res[0] = res[0] %('(%s)' %Res[-1])
    return Res

getContectWithinBraces(s , left='\(\[\{' , right = '\)\]\}')

Я считаю, что эта функция может удовлетворить ваши потребности, я бросил это вместе быстро, так что не стесняйтесь, чтобы очистить его немного. При выполнении гнезд легко думать об этом назад и работать оттуда =]

def fn(string,endparens=False):
    exp = []
    idx = -1
    for char in string:
        if char == "(":
            idx += 1
            exp.append("")
        elif char == ")":
            idx -= 1
            if idx != -1:
                exp[idx] = "(" + exp[idx+1] + ")"
        else:
            exp[idx] += char
    if endparens:
        exp = ["("+val+")" for val in exp]
    return exp

многие сообщения предполагают, что для вложенных фигурных скобок, РЕГУЛЯРНОЕ ВЫРАЖЕНИЕ-ЭТО НЕ СПОСОБ СДЕЛАТЬ ЭТО. ПРОСТО ПОСЧИТАЙТЕ СКОБКИ: Например, см.: регулярное выражение для обнаружения полу-двоеточия C++ для & while циклов

вот полный образец python для итерации через строку и подсчета фигурных скобок:

# decided for nested braces to not use regex but brace-counting
import re, string

texta = r'''
nonexistent.\note{Richard Dawkins, \textit{Unweaving the Rainbow: Science, Delusion
and the Appetite for Wonder} (Boston: Houghton Mifflin Co., 1998), pp. 302, 304,
306-309.} more text and more.

 This is a statistical fact, not a
guess.\note{Zheng Wu, \textit{Cohabitation: An Alternative Form
of Family Living} (Ontario, Canada: Oxford University Press,
2000), p. 149; \hbox{Judith} Treas and Deirdre Giesen, ``Title
and another title,''
\textit{Journal of Marriage and the Family}, February 2000,
p.\,51}

more and more text.capitalize
'''
pos = 0
foundpos = 0
openBr = 0 # count open braces
while foundpos <> -1:
    openBr = 0
    foundpos = string.find(texta, r'\note',pos)
    # print 'foundpos',foundpos
    pos = foundpos + 5
    # print texta[pos]
    result = ""
    while foundpos > -1 and openBr >= 0:
        pos = pos + 1
        if texta[pos] == "{":
            openBr = openBr + 1
        if texta[pos] == "}":
            openBr = openBr - 1
        result = result + texta[pos]
    result = result[:-1] # drop the last } found.
    result = string.replace(result,'\n', ' ') # replace new line with space
    print result