我试图匹配一个类似于数学表达式的字符串,其中包含嵌套括号。
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),我也可以处理。
为什么它现在没有做到这一点,我该怎么做呢?
正如其他人所提到的,正则表达式不适用于嵌套结构。我将使用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]
为了以嵌套列表格式获取输出:
res = parens.parseString("((12 + 2) + 3)")
res.asList()
输出:
[[['12', '+', '2'], '+', '3']]
asList()
,它将输出转换为嵌套列表。如果使用pprint.pprint
打印它们,则应该得到良好的缩进输出。此外,nestedExpr
方法是隐式递归的,因此在这种情况下,您可以将内容定义为仅为thecontent = (pyparsing.Word(pyparsing.alphanums) | '+' | '-')
- 无需进行Forward操作。 - PaulMcG(a + b) + (c + (d + e))
这样没有公共根的表达式,此方法将失败,但是对于((a + b) + (c + (d + e)))
则可以。 - norok2有一个新的正则表达式引擎模块正在准备中,以取代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
中相关的bug: http://code.google.com/p/mrab-regex-hg/issues/detail?id=78
(?0)
来引用自身。对于给定的示例,regex.findall(r'\((?:[^()]++|(?0))++\)', s)
会返回['(((1+0)+1)+1)']
。 - Sundeep正则表达式语言的能力不足以匹配任意嵌套结构。为此,您需要一个下推自动机(即解析器)。有几个这样的工具可用,例如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'],
')'],
'',
'']
这个正则表达式尝试尽可能多地匹配文本,因此消耗了整个字符串。它不会在该字符串的部分中寻找正则表达式的其他匹配项。这就是为什么您只会得到一个答案。
解决方案是不使用正则表达式。如果您真的要解析数学表达式,请使用真正的解析解决方案。如果您只想捕获括号内的内容,只需循环遍历字符,在看到 ( 和 ) 时计数并增加或减少计数器即可。
Stack是完成工作的最佳工具:
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:]))
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)作为括号匹配器,这对于大多数应用程序来说可能过于复杂。但是它确实用于匹配Scheme表达式。
是的,在给定限制后它会失效,但是能够将其插入正则表达式仍然比支持无限深度的“正确”替代方案在可用性方面更加优越。
我相信这个函数可能适合你的需求,我很快地把它组合起来了,所以随意稍微整理一下。当进行嵌套时,很容易倒过来考虑并从那里开始工作 =]
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
平衡的括号对(例如)是一种不能被正则表达式识别的语言的例子。
接下来简要解释一下这是为什么。
正则表达式是定义有限状态自动机(缩写为FSM)的一种方式。这样的设备有一个有限的可能状态来存储信息。它可以使用的状态并没有特别的限制,但这意味着它能够识别的不同位置的数量是绝对最大的。
例如,状态可以用于计数,比如未匹配的左括号。但是因为那种计数所需的状态量必须完全受限,所以给定的FSM只能计数到最大值n-1,其中n是FSM可以处于的状态数。如果n是10,那么FSM可以匹配的未匹配左括号的最大数量是10,直到它崩溃。由于完全有可能有一个以上的左括号,因此没有可能的FSM能够正确地识别匹配括号的完整语言。
那又怎样?假设你只选择一个非常大的n呢?问题在于,作为描述FSM的一种方式,正则表达式基本上描述了从一个状态到另一个状态的所有转换。由于对于任何N,FSM都需要2个状态转换(一个用于匹配左括号,一个用于匹配右括号),因此正则表达式本身必须至少增长一个与n成比例的常数因子。L = { ww | w \in {0,1}* }
这样既不是正则也不是上下文无关的东西。虽然它们不能匹配任意嵌套的匹配括号,但原因并非你所说的。 - phooji你可以使用正则表达式,但需要自己做递归。类似下面这样的代码可以实现目标(如果你只需要找到所有括号括起来的表达式):
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
然而,这段代码并不匹配“正确”的括号。如果您需要这样做,最好使用专门的解析器。
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
get_string_inside_outermost_parentheses('a(b)c(d)e')
返回 'b)c(d'
。 - Bill