【问题标题】:Python: How to match nested parentheses with regex?Python:如何将嵌套括号与正则表达式匹配?
【发布时间】:2011-07-24 04:24:10
【问题描述】:

我正在尝试匹配带有嵌套括号的类似数学表达式的字符串。

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) 之类的不需要的东西,我可以处理这些。

为什么它还没有这样做,我该怎么做?

【问题讨论】:

标签: python regex nested


【解决方案1】:

正则表达式语言不足以匹配任意嵌套的结构。为此,您需要一个下推自动机(即解析器)。有几个这样的工具可用,例如PLY

Python 还为自己的语法提供了parser library,它可能会满足您的需求。但是,输出非常详细,需要一段时间才能理解。如果你对这个角度感兴趣,下面的讨论会尽量简单地解释。

>>> 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 模块 symboltoken,您可以使用它们来构建从数字到名称的查找表:

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'],
  ')'],
 '',
 '']

【讨论】:

  • 我想说 PLY 更适合更大的解析任务。当我真的需要模仿现有的 Lex/Yacc 解析器时,我发现它最有用。至于使用python自带的解析器库……呵呵。
  • @phooji:我同意。 Python 的解析器库绝对不适合所有人。
  • 实际上并非如此,匹配嵌套括号的 perl 正则表达式工作正常:metacpan.org/pod/Regexp::Common python 使用 perl 正则表达式
【解决方案2】:

你应该编写一个合适的解析器来解析这样的表达式(例如使用 pyparsing)。 正则表达式不是编写体面的解析器的合适工具。

【讨论】:

  • 我把你的回答铭记于心,并写下了我保守地称之为“我的第一个 Pyparsing 语法”。
【解决方案3】:

正则表达式尝试匹配尽可能多的文本,从而消耗您的所有字符串。它不会在该字符串的某些部分上查找正则表达式的其他匹配项。这就是为什么你只能得到一个答案。

解决方案是不使用正则表达式。如果您实际上是在尝试解析数学表达式,请使用真正的解析解决方案。如果您真的只想捕获括号内的片段,只需在您看到 ( 和 ) 时循环计数字符并增加一个减量计数器。

【讨论】:

    【解决方案4】:

    我相信这个功能可能适合您的需要,我很快就把它放在一起,所以请随意清理一下。在做嵌套时,很容易想到它并从那里开始工作=]

    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
    

    【讨论】:

    • 我很欣赏这种情绪,是的,使用详细函数提供请求的列表是蹩脚的。如果我的答案与@phooji 的实用程序不匹配,请原谅我,但我怀疑这里甚至需要不正确的解析模块。虽然我已经从上面的例子中学到了大量的解析和书面描述。
    • @pynator:我对你评论的语义结构感到困惑,但我想我同意 :)
    【解决方案5】:

    正如其他人所提到的,正则表达式不是嵌套构造的方法。我将使用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]
    

    (手动完成换行/缩进/cmets)

    编辑: 根据 Paul McGuire 的建议,修改以消除不必要的 Forward

    以嵌套列表格式获取输出:

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

    输出:

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

    【讨论】:

    • Pyparsing 返回 ParseResults 对象,这些对象具有您描述的 repr 输出。此类还支持asList(),它将输出转换为直接嵌套列表。如果你使用pprint.pprint 打印它们,你应该得到很好的缩进输出。此外,nestedExpr 方法是隐式递归的,因此您可以在这种情况下将内容定义为 thecontent = (pyparsing.Word(pyparsing.alphanums) | '+' | '-') - 无需转发。
    • @Paul McGuire:感谢您的澄清!正如你可能知道的那样,我对 pyparsing 有点青涩。
    • 这对于像(a + b) + (c + (d + e))(没有公共根)这样的表达式会失败,而((a + b) + (c + (d + e)))会起作用。
    【解决方案6】:

    平衡对(例如括号)是正则表达式无法识别的语言示例。

    以下是对数学原理的简要说明。

    正则表达式是定义有限状态自动机(简称 FSM)的一种方式。这种设备具有有限数量的可能状态来存储信息。如何使用该状态并没有特别限制,但这确实意味着它可以识别的不同位置的绝对最大数量。

    例如,状态可用于计算不匹配的左括号。但是因为这种计数的状态量必须是完全有界的,所以给定的 FSM 最多可以计数到 n-1,其中 n 是状态 FSM 可以在其中。如果 n 是 10,则 FSM 可以匹配的不匹配左括号的最大数量是 10,直到它中断。由于完全有可能再有一个左括号,因此没有可能的 FSM 可以正确识别匹配括号的完整语言。

    那又怎样?假设您只选择了一个非常大的 n?问题在于,作为一种描述 FSM 的方式,正则表达式基本上描述了从一个状态到另一个状态的所有 转换。由于对于任何 N,FSM 都需要 2 个状态转换(一个用于匹配左括号,一个用于匹配右括号),正则表达式本身必须至少增长 n

    相比之下,下一个更好的语言类别(上下文无关语法)可以以一种完全紧凑的方式解决这个问题。这是BNF中的一个例子

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

    【讨论】:

    • 你是对的,但也是错的;) 现代编程语言包括可以匹配 L = { ww | w \in {0,1}* } 这样的东西的正则表达式工具,它既不是常规的也不是上下文无关的。确实它们不能匹配任意嵌套的匹配括号,但不是因为你提到的原因。
    • 从历史上看,正则表达式最初是作为一种表达 FSM 的手段,但随着时间的推移,正则表达式获得了很大的力量,以至于大多数现代正则表达式的表达能力超越了正则语法。因此,关于 FSM 的讨论不一定与正则表达式相关。
    • 我们在谈论不同的目的。请参阅montreal.pm.org/tech/neil_kandalgaonkar.shtml 了解我所说的示例。
    • 你错了。有些语言是上下文无关语言的正确子集,但常规语法无法识别:例如,确定性上下文无关语言或可见下推语言。 (更多详情:en.wikipedia.org/wiki/Deterministic_context-free_grammaren.wikipedia.org/wiki/Nested_word
    【解决方案7】:

    您可以使用正则表达式,但您需要自己进行递归。像下面这样的东西可以解决问题(如果你只需要找到,正如你的问题所说,括号中的所有表达式):

    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
    

    但是,此代码与“正确”括号不匹配。如果您需要这样做,最好使用专门的解析器。

    【讨论】:

      【解决方案8】:

      a new regular engine module 正在准备替换 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中的相关bug

      【讨论】:

      • 不...不是 Perl 的“不规则正则表达式”的另一个迭代!
      • 由于没有其他匹配可做,您可以使用(?0) 来引用自身.. 对于给定的样本,regex.findall(r'\((?:[^()]++|(?0))++\)', s) 给出['(((1+0)+1)+1)']
      【解决方案9】:

      许多帖子建议对于嵌套大括号, 正则表达式不是这样做的方法。简单地计算大括号: 例如,参见:Regular expression to detect semi-colon terminated C++ for & while loops

      这是一个完整的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
      

      【讨论】:

        【解决方案10】:

        来自链接的答案:

        来自 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),这对于大多数应用程序来说可能是多余的。但随后它使用它来匹配 Scheme 表达式。

        是的,它在给定的限制之后就崩溃了,但是将其插入正则表达式的能力仍然优于支持无限深度的“正确”替代方案。

        【讨论】:

        • 这是如何工作的?它只返回第一个/外部匹配的括号,对吗?所以你会继续呼吁结果更深入吗?也许您需要检查是否还有更多 ( 解析,因为当没有括号时它会返回一些东西。
        【解决方案11】:

        堆栈是完成这项工作的最佳工具:-

        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
        

        【讨论】:

          【解决方案12】:

          这是一个针对您的问题的演示,虽然它很笨拙,但它确实有效

          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 = '\)\]\}')
          

          【讨论】:

            【解决方案13】:

            我的解决方案是:定义一个函数来提取最外层括号内的内容,然后重复调用该函数,直到获得最内层括号内的内容。

            def get_string_inside_outermost_parentheses(text):
                content_p = re.compile(r"(?<=\().*(?=\))")
                r = content_p.search(text)
                return r.group() 
            
            def get_string_inside_innermost_parentheses(text):
                while '(' in text:
                    text = get_string_inside_outermost_parentheses(text)
                return text
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-07-18
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多