在开发过滤器、规则引擎或者查询构造器的过程中,经常需要解析形如(a > 1 AND b < 2 OR c = 3)这样的条件字符串,把括号内的每个子条件提取出来分别处理。这类需求看似简单,实际写正则的时候却容易遇到各种坑:括号匹配不上、运算符被吃掉、嵌套括号处理不了等等。本文就来系统地梳理这个问题的几种解法。

一、先明确目标:要提取什么样的内容
在动手写正则之前,先把目标结构分析清楚。假设输入字符串是这样的:
expr = "(a > 1 AND b < 2 OR c = 3)"
我们希望得到三个独立的条件:a > 1、b < 2、c = 3,同时最好能保留它们之间的逻辑关系,也就是知道第一个和第二个之间是AND,第二个和第三个之间是OR。这里有两种典型的提取策略:
第一种是整体捕获再拆分:先用正则匹配最外层括号,拿到括号内的完整内容,再用re.split按AND|OR进行二次拆分。这种方式实现简单,两层正则各司其职,可读性好。第二种是一步到位的重复分组:利用re.findall配合(...)捕获组和+量词,一次性把所有条件抓出来。两种方式各有适用场景,下面分别展开。
还有一个细节需要注意:逻辑运算符可能是AND、OR,也可能是&&、||,甚至是中文环境下的空格分隔。写正则前一定要确认实际的运算符形式,否则匹配结果会漏掉内容。另外,运算符前后可能有也可能没有空格,正则里要做出相应处理。
二、两步法:先取括号内容,再按运算符拆分
这是最推荐新手使用的方案,逻辑清晰、容易调试。第一步用\(([^)]+)\)匹配一对不含嵌套的括号,其中\(和\)是转义后的字面括号,[^)]+表示一个或多个非右括号字符,放在捕获组里就是括号内的内容。第二步用re.split按逻辑运算符切分。完整代码如下:
import re expr = "(a > 1 AND b < 2 OR c = 3)" # 第一步:提取括号内的完整内容 inner = re.search(r'\(([^)]+)\)', expr).group(1) print(inner) # 输出: a > 1 AND b < 2 OR c = 3 # 第二步:按 AND 或 OR 拆分 conditions = re.split(r'\s+(?:AND|OR)\s+', inner) print(conditions) # 输出: ['a > 1', 'b < 2', 'c = 3']
这里有个关键点:正则\s+(?:AND|OR)\s+使用了非捕获组(?:...)。如果直接写(AND|OR),re.split会把捕获到的运算符也放进结果列表里。有些场景下这反而是优点——如果你需要同时拿到运算符和条件,故意用捕获组就能实现:
# 带捕获组,运算符也会出现在结果中 parts = re.split(r'\s+(AND|OR)\s+', inner) print(parts) # 输出: ['a > 1', 'AND', 'b < 2', 'OR', 'c = 3']
拆分出来的列表中,偶数下标是条件,奇数下标是运算符,遍历一遍就能重建出完整的逻辑树。如果条件里可能出现&&或||写法,把正则改成\s*(?:&&|\|\|)\s*即可,注意|和&在正则中都有特殊含义,必须转义。
三、一步法:用findall配合捕获组直接提取条件
如果不需要运算符,只想要条件列表,可以用findall一次完成。思路是:每个条件由字母开头的变量名加上运算符和值组成,可以归纳成一个模式,比如\w+\s*[<>=!]+\s*[\w.]+。用这个模式去扫描整串文本即可:
import re expr = "(a > 1 AND b < 2 OR c = 3)" conditions = re.findall(r'\w+\s*(?:[<>=!]=?|like|in)\s*[\w."\']+', expr) print(conditions) # 输出: ['a > 1', 'b < 2', 'c = 3']
这种写法的好处是灵活,即使没有外层括号、或者一行里有多个括号表达式,也能全部抓出来。缺点是模式的泛化能力取决于你对条件格式的预判。如果条件可能是name like '%abc%'这种带引号和百分号的写法,字符类[\w."']+就得跟着扩展;如果值可能是日期、函数调用甚至子查询,一个正则很快就会膨胀到难以维护。所以一步法适合格式可控的场景,比如自己系统生成的过滤条件。
在JavaScript里写法基本一致,只是matchAll配合g标志更常用:
const expr = "(a > 1 AND b < 2 OR c = 3)"; const pattern = /\w+\s*(?:[<>=!]=?|AND|OR)\s*[\w.]+/g; // 先把AND OR过滤掉,只留条件 const raw = expr.match(pattern); const conditions = raw.filter(s => !/^(AND|OR)$/.test(s)); console.log(conditions); // [ 'a > 1', 'b < 2', 'c = 3' ]
四、嵌套括号怎么办:正则的局限与替代方案
前面所有方案都有一个隐含前提:括号不嵌套。一旦输入变成((a > 1 AND b < 2) OR c = 3),[^)]+会在第一个右括号处提前截断,拿到的是(a > 1 AND b < 2这样的残缺内容。这是正则表达式的根本局限——标准正则不支持递归结构,无法表达任意深度的括号配对。
有几种应对思路。第一种是限定嵌套层数:如果业务上能保证最多两层,可以手工展开,比如\((?:[^()]|\([^()]*\))+\)表示括号里可以再包含一层括号。层数再深一层,模式长度翻倍,三层以上基本不可读。第二种是逐层剥离:反复用最简正则\(([^()]*)\)匹配最内层括号,提取后替换成占位符,循环直到没有括号为止,最后逆序还原。这种写法代码量稍多,但能处理任意深度:
import re
def extract_conditions(expr):
results = []
placeholder_map = {}
counter = [0]
def replace_inner(m):
token = f"__P{counter[0]}__"
placeholder_map[token] = m.group(1)
counter[0] += 1
results.append(m.group(1))
return token
# 反复提取最内层括号
while '(' in expr:
new_expr = re.sub(r'\(([^()]*)\)', replace_inner, expr)
if new_expr == expr:
break # 括号不配对,防止死循环
expr = new_expr
return results
print(extract_conditions("((a > 1 AND b < 2) OR c = 3)"))
# 输出: ['a > 1 AND b < 2', '(a > 1 AND b < 2) OR c = 3' 的占位形式...]
第三种也是工程上最稳妥的方案:放弃纯正则,改用词法分析加递归下降解析。先把输入切成token(标识符、运算符、比较符、括号),再写一个几十行的递归下降解析器,把表达式构建成语法树。这样不仅处理嵌套毫无压力,还能校验语法错误、支持优先级(比如NOT高于AND高于OR)。如果条件解析是系统的核心功能,这条路的前期投入绝对值得。
总结一下选择建议:无嵌套时用两步法,代码最少最直观;格式可控且只要条件列表时用findall一步法;存在嵌套但层数固定时用限定深度的模式;嵌套任意且是核心功能时,老老实实上解析器。正则很强大,但它终究是为线性结构设计的工具,认清边界比炫技更重要。