有时候我发现我好像比较喜欢看电脑干活,电脑在忙等于我在忙,电脑很累然而我不累
为了满足自己看电脑干活的爱好,我打算把平时可能会遇到的写mongo查询语句的任务简单化,大伙都知道mongo的查询语法是类似json格式的,写起来不如SQL那样畅快淋漓,于是我就想把这个查询的活完全交给电脑去完成,但是电脑又不是什么高情商,我在想什么它怎么可能知道?于是可悲的代沟让我的语言表达倍感压力——mongo语法的语言表达
为了缩小我和电脑之间的代沟,我打算设计一个新的语法翻译器,用新潮的类python语法构造mongo语法,当然这个翻译器是因地制宜的,它仅支持很简单的几个操作(但是却可以大大节省我的键盘寿命,特别是“{”、“}”、“:”这几个键的寿命)
就像设计一个全新的编程语言一样,接下来我们先设计关键字和运算符:
| 关键字/运算符 | 描述 | 用法示例 |
|---|---|---|
| title | 网页标题 | - |
| header | http响应头 | - |
| body | html响应 | - |
| or | 或运算符 | ‘nginxwebui’ in body or title is ‘nginxwebui’ |
| and | 且运算符 | ‘nginxwebui’ in body and title is ‘nginxwebui’ |
| in | 包含运算符 | ‘nginxwebui’ in body |
| is | 相等运算符 | title is ‘nginxwebui’ |
| not | 非运算符 | not title is ‘nginxwebui’ |
设计好了这一套优美的英国话之后,可能大伙会对not运算符的优美性产生质疑
但是如果设计成title is not xxx这种,我的工作量会大一倍,这个跟python的解析有关,虽然存在投机取巧的可能,但是我连投机取巧都懒得做,具体什么情况咱后面再说
大伙可能都记得lex和yacc的痛苦,但是python就非常甜,因为这套优美的查询语句基本就是照抄python的关键字的,所以直接使用python的ast模块就能直接一把梭
下面给个例子:
import ast
test = "'a' in title and ('b' in header or body is 'c')"
tree = ast.parse(test)
print(ast.dump(tree, indent=2))运行结果如下:
Module(
body=[
Expr(
value=BoolOp(
op=And(),
values=[
Compare(
left=Constant(value='a'),
ops=[
In()],
comparators=[
Name(id='title', ctx=Load())]),
BoolOp(
op=Or(),
values=[
Compare(
left=Constant(value='b'),
ops=[
In()],
comparators=[
Name(id='header', ctx=Load())]),
Compare(
left=Name(id='body', ctx=Load()),
ops=[
Is()],
comparators=[
Constant(value='c')])
])]))],
type_ignores=[])
直接帮我们生成了语法树,接下来要做的事情就是遍历这个语法树,一边遍历一边组装mongo查询语句
因为要实现一边遍历一边组装mongo语句,所以要制造一个自动机类似物,不过先不急,我们先来看看mongo语法是怎样的,针对每部分进行实现即可
mongo语法类似json,结构化程度很高,因此自动构造也比较容易
mongodb有一个$and运算符,语法规则如下:
{ $and: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] }
这个效果就是mongo会输出满足所有expression的数据
这个and运算符实际上mongo的设计和python语法树的表示方式是心有灵犀的,我们可以运行代码看看:
import ast
test = "'a' in title and 'b' in header and body is 'c'"
tree = ast.parse(test)
print(ast.dump(tree, indent=2))结果:
Module(
body=[
Expr(
value=BoolOp(
op=And(),
values=[
Compare(
left=Constant(value='a'),
ops=[
In()],
comparators=[
Name(id='title', ctx=Load())]),
Compare(
left=Constant(value='b'),
ops=[
In()],
comparators=[
Name(id='header', ctx=Load())]),
Compare(
left=Name(id='body', ctx=Load()),
ops=[
Is()],
comparators=[
Constant(value='c')])]))],
type_ignores=[])
可能上面的运行结果看起来没那么直观,我简化一下给大家看看:
Module(
body=[
Expr(
value=BoolOp(
op=And(),
values=[
Compare(...),
Compare(...),
Compare(...)],
type_ignores=[])
看values字段,是不是跟mongo的语法一模一样
这样就好设计了,自动机在遍历到BoolOp的时候,检查op的值是否是And,如果是,就遍历values然后把Compare表达式转换出来的结果填到mongo语法的方括号里
具体的代码我拉在文末了,不然东一坨西一坨的影响环境
和and一模一样,没区别
为什么我不先介绍in运算符的原理呢?因为in运算符是is运算符的一个特殊情况,所以一会再说
mongo做完整匹配的语法差不多是这样的:
db.users.find({ age: 30 })
冒号左边是字段名,右边是匹配的值
我们的语法支持title is ‘a’,也支持’a’ is title,这根据用户的自身情况,如果倒装是喜欢用户的,那山东人用户可能是
那这个要怎么做呢?很简单,先看看语法树:
import ast
test = "'a' is title"
test2 = "title is 'a'"
tree = ast.parse(test)
tree2 = ast.parse(test2)
print(ast.dump(tree))
print(ast.dump(tree2))Module(body=[Expr(value=Compare(left=Constant(value='a'), ops=[Is()], comparators=[Name(id='title', ctx=Load())]))], type_ignores=[])
Module(body=[Expr(value=Compare(left=Name(id='title', ctx=Load()), ops=[Is()], comparators=[Constant(value='a')]))], type_ignores=[])
可以看到Constant和Name也是反过来的,但是我们在转换成mongo语法的时候还是一定要保持左边字段名右边值的格式
这怎么办呢?实际上我们可以设计一个字典,这个字典的键有两个,一个是Constant,另一个是Name,遍历语法树时,如果遍历到is语句,那就把Constant和Name都存入字典对应的位置,组装mongo语句的时候再拿出来就好
刚才说in运算符是is的一个特例,现在就来解释
我们先来看看in运算符对应mongo里面的哪个语法
db.users.find({ "name" : { $regex : "web"} })
差不多就是上面这种,mongo支持正则表达式,作为语法翻译器,我们不需要处理用户输入的常量数据,所以我们也是支持在in表达式里面塞正则表达式
其实in的语法树和is的是一样的,除了那个ops字段之外,都一样,但是in运算符有一个顺序问题,我只想支持'a' in title,而不想支持title in 'a',大伙都学过英语,都知道这两句话的差异
所以在写代码的时候做了代码层面的限制,语法树节点left的类型必须是Constant,而comparators[0]的类型必须是Name,其它跟is运算符一样
not运算符是比较特殊的,之前也提到我只支持了not [expr]这样的格式,而不支持[expr] is not [expr]这样的,因为python语法树给他俩打的标签不一样:
import ast
test = "not 'a' in title"
test2 = "'a' not in title"
tree = ast.parse(test)
tree2 = ast.parse(test2)
print(ast.dump(tree))
print(ast.dump(tree2))Module(body=[Expr(value=UnaryOp(op=Not(), operand=Compare(left=Constant(value='a'), ops=[In()], comparators=[Name(id='title', ctx=Load())])))], type_ignores=[])
Module(body=[Expr(value=Compare(left=Constant(value='a'), ops=[NotIn()], comparators=[Name(id='title', ctx=Load())]))], type_ignores=[])
因为python是把not in算成单独一种运算符,因此我如果要解析这个语法,那我就必须实现in、not in、is、is not四个语法的解析代码,但是如果我把not单独拎出来,我只需实现not、is、in三个语法的解析代码;虽然可以做一个魔改,把not in语法转换成not ‘xxx’ in title这种类型,但是感觉还是麻烦,主要是容易出现不确定的情况,怕出现不稳定的问题,所以就一刀切了
接下来想办法把not的语法树转换成mongo语法,mongo的not语法如下:
{ <field>: { $not: { <operator-expression> } } }
可以看到它这个的mongo数据库字段名是写在not运算符外面的,但是我们的语法规定not写在外面,刚刚避免了not in和not is的麻烦,但是却迎来了语法顺序的问题
但是这个也不是无法解决的,我们可以回顾刚才见过的所有mongo语法,在字典格式的mongo语法中,字典的键总是mongo字段名,而字典的值总是用户输入
因此只需将语法树operand字段的语法翻译结果的键提取到外面来就好了,直接重新整一遍格式就ok
语法树遍历的代码如下
import ast
def dfs(node):
if not node:
return {}
if node.__class__ == ast.BoolOp:
if node.op.__class__ == ast.And:
res = {'$and': []}
for i in node.values:
con = dfs(i)
res['$and'].append(con)
return res
if node.op.__class__ == ast.Or:
res = {'$or': []}
for i in node.values:
con = dfs(i)
res['$or'].append(con)
return res
if node.__class__ == ast.Compare:
op = node.ops[0]
if op.__class__ == ast.Is:
l = dfs(node.left)
r = dfs(node.comparators[0])
flags = {node.left.__class__: l, node.comparators[0].__class__: r}
if ast.Name in flags and ast.Constant in flags:
return {flags[ast.Name]: flags[ast.Constant]}
return {}
if op.__class__ == ast.In:
if node.left.__class__ == ast.Constant and node.comparators[0].__class__ == ast.Name:
l = dfs(node.left)
r = dfs(node.comparators[0])
return {r: {'$regex': l}}
return {}
if node.__class__ == ast.UnaryOp:
if node.op.__class__ == ast.Not:
not_expr = list(dfs(node.operand).items())[0]
res = {not_expr[0]: {'$not': not_expr[1]}}
return res
if node.__class__ == ast.Constant:
return node.value
if node.__class__ == ast.Name:
return node.id外部调用:
from dfs import dfs
import ast
text = "'/imc/login.jsf' in body and '/imc/javax.faces.resource/images/login_help.png.jsf?ln=primefaces-imc-new-webui' in body"
tree = ast.parse(text)
#print(ast.dump(tree))
print(dfs(tree.body[0].value))
运行结果:
{'$and': [{'body': {'$regex': '/imc/login.jsf'}}, {'body': {'$regex': '/imc/javax.faces.resource/images/login_help.png.jsf?ln=primefaces-imc-new-webui'}}]}
感觉应该没问题,有问题再改