如何基于字符串内括号逻辑使用Django Q对象生成动态查询
实现带优先级的查询字符串转Django查询的两种方案
你要实现的需求核心是把类SQL的逻辑表达式转换成Django ORM支持的Q对象组合,两种实现方案如下:
方案一:用pyparsing实现(无需手动处理栈,最简便)
不需要自己手写栈处理优先级,直接通过语法规则定义自动解析:
- 先安装依赖
pip install pyparsing django
- 核心实现代码
from pyparsing import infixNotation, opAssoc, Word, alphas, nums, dblQuotedString, removeQuotes from django.db.models import Q # 定义解析规则 # 匹配字段名:字母开头,可包含数字下划线 field = Word(alphas, alphas + nums + "_") # 匹配值:支持数字、双引号字符串 value = Word(nums) | dblQuotedString.setParseAction(removeQuotes) # 匹配比较操作符,示例默认做等于判断,可扩展gt/lt/contains等 op = Word("=", max=1) # 单个条件:字段 + 操作符 + 值,直接转成Q对象 condition = (field + op + value).setParseAction(lambda t: Q(**{f"{t[0]}__exact": t[2]})) # 定义逻辑操作符优先级:AND高于OR,括号自动处理优先级 expr = infixNotation( condition, [ ("AND", 2, opAssoc.LEFT, lambda t: t[0][0] & t[0][2]), ("OR", 2, opAssoc.LEFT, lambda t: t[0][0] | t[0][2]), ] ) # 使用示例 query_str = '(name="abc" OR name="xyz") AND (status="active" OR (status="available" AND age=30))' parsed_q = expr.parseString(query_str)[0] # 直接用于查询即可 # result = YourModel.objects.filter(parsed_q)
方案二:手动栈实现(无第三方解析依赖)
如果不想引入额外依赖,可以自己实现中缀表达式转后缀表达式,再构造Q对象:
from django.db.models import Q def tokenize(query_str): """把查询字符串拆成token""" tokens = [] i = 0 n = len(query_str) while i < n: c = query_str[i] if c in '()': tokens.append(c) i += 1 elif c.isspace(): i += 1 elif query_str[i:i+3] in ('AND', 'OR'): tokens.append(query_str[i:i+3]) i += 3 elif c == '"': # 匹配双引号包裹的字符串值 j = i + 1 while j < n and query_str[j] != '"': j += 1 tokens.append(query_str[i:j+1]) i = j + 1 else: # 匹配字段名、数字、操作符 j = i while j < n and not query_str[j].isspace() and query_str[j] not in '()="': j += 1 tokens.append(query_str[i:j]) i = j # 把拆分的字段、等于号、值合并成单个条件token merged_tokens = [] idx = 0 while idx < len(tokens): if idx + 2 < len(tokens) and tokens[idx+1] == '=': k = tokens[idx] v = tokens[idx+2].strip('"') merged_tokens.append(Q(**{k: v})) idx += 3 else: merged_tokens.append(tokens[idx]) idx += 1 return merged_tokens def infix_to_postfix(tokens): """中缀转后缀(逆波兰表达式),处理优先级和括号""" precedence = {'OR': 1, 'AND': 2} output = [] op_stack = [] for token in tokens: if token == '(': op_stack.append(token) elif token == ')': while op_stack and op_stack[-1] != '(': output.append(op_stack.pop()) op_stack.pop() # 弹出左括号丢弃 elif token in ('AND', 'OR'): while op_stack and op_stack[-1] != '(' and precedence[op_stack[-1]] >= precedence[token]: output.append(op_stack.pop()) op_stack.append(token) else: # 是已经转换好的Q对象,直接入输出队列 output.append(token) while op_stack: output.append(op_stack.pop()) return output def eval_postfix(postfix): """计算后缀表达式得到最终Q对象""" stack = [] for token in postfix: if isinstance(token, Q): stack.append(token) elif token in ('AND', 'OR'): b = stack.pop() a = stack.pop() if token == 'AND': stack.append(a & b) else: stack.append(a | b) return stack[0] if stack else Q() # 使用示例 query_str = '(name="abc" OR name="xyz") AND (status="active" OR (status="available" AND age=30))' tokens = tokenize(query_str) postfix = infix_to_postfix(tokens) parsed_q = eval_postfix(postfix) # 直接用于查询即可 # result = YourModel.objects.filter(parsed_q)
安全提示
一定要加字段白名单校验,解析的时候判断用户传入的字段是不是你允许查询的模型字段,避免用户传入敏感字段导致数据泄露。
内容的提问来源于stack exchange,提问作者Mazhar Ali
相关产品推荐
相关产品推荐

