内容简介:假如有这样一个场景:用户输入一句简单的 sql 查询语句完成了之后发现正则表达式稍微有点长,但是整体结构还是蛮简单的。不过仔细测试发现,我们的正则表达式考虑的还不是很周全,例如:很明显,正则表达式已经比较长了,看过去会有一点眼花,可是,这条正则表达式其实并不完善,例如没有考虑到多列名的情况
在介绍 PEG.js 之前,我们先来说下我们为什么需要它。
假如有这样一个场景:用户输入一句简单的 sql 查询语句 select name from user
, 我们的任务是获取到里面的 列名 , 表名 。在现有 工具 下,我们第一个想到的就是 正则表达式 ,熟悉的同学应该能很快写出这样的正则表达式:
/^select\s+([A-Za-z_]*)\s+from\s+([A-Za-z_]*)$/.exec("select name from user") //["select t from", "name", "user"] 复制代码
完成了之后发现正则表达式稍微有点长,但是整体结构还是蛮简单的。不过仔细测试发现,我们的正则表达式考虑的还不是很周全,例如: select * from user
, select user.name from user
这两种情况我们都没考虑到,所以我们还需要完善一下正则表达式
/^select\s+([A-Za-z_]*|\*|[A-Za-z_]*.[A-Za-z_]*)\s+from\s+([A-Za-z_]*)$/.exec("select user.name from user") //["select user.name from user", "user.name", "user"] 复制代码
很明显,正则表达式已经比较长了,看过去会有一点眼花,可是,这条正则表达式其实并不完善,例如没有考虑到多列名的情况 select name, age from user
,和多表的情况 select user.name, class.name from user,class
,假如继续扩展下去,我们的正则表达式将会变得十分庞大,并且当别人来修改自己的代码的时候,也会变得十分困难,久而久之,就会变得难以维护。
我们仔细思考一下上面的流程,其实正则表达式主要帮助我们做了一件事,那就是将一条 sql 语句转化为结构化的数据 ["select t from", "name", "user"]
,从而我们根据结构化的数据来获取自己想要的信息。
就是一个很适合我们的工具。 不过,它和正则表达式还是有一点区别。
PEG.js 是一个 parser generator ,它会将我们用 PEG.js 语法 编写的 sql 语法文件 转化为可以直接运行的 parser, 而我们也是通过这个parser去解析 sql 语句。所以我们的工作就是编写 sql 语法文件。
安装
首先,我们将 PEG.js 安装到全局下
npm install -g pegjs 复制代码
安装好了可以验证一下
$ pegjs -v PEG.js 0.10.0 复制代码
然后我们在新建一个目录用来存放我们代码。本文的结构如下
peg
文件夹用来存放我们的语法文件,
dist
文件夹用来存放我们生成的 parser。创建完项目之后,我们下面就开始来正式使用 PEG.js 来解析 sql。
开始使用
首先,我们先确定一下我们要实现的 select 语句,本文为了便捷,不会编写完善的语法文件,所以这里以最简单的 select 语句为例子。
select column_name | * from table_name; 复制代码
开始编写 PEG 文件
在 PEG.js 文件的开头,我们来写下第一个规则 ,PEG.js 默认从 第一个 规则开始解析。
start = selectStatement 复制代码
这里我们定义了一个 start
规则,而这个 start
的 解析表达式 由一个 selectStatement
规则组成,而这个 selectStatement
就是我们刚刚定义的 select 语句了。
那么按照我们之前的定义, selectStatement
又由什么组成呢?
经过一些思考,我们应该能写下以下规则
selectStatement = select colunm_clause from table_name ';' 复制代码
selectStatement
的解析表达式由5个部分组成,其中, ';'
代表一个分号字符串,来代表一条 sql 语句的结尾,而其他4个则代表 selectStatement
解析表达式的四个组成部分。所以,我们只要从 selectStatement
开始自顶向下 ,为每一个规则添加解析表达式就好了。
selectStatement = select colunm_clause from table_name ';' colunm_clause = column_name / '*' column_name = ident_part+ table_name = ident_part+ ident_part = [A-Za-z0-9] select = 'select'i from = 'from'i 复制代码
这里 colunm_clause
的解析表达式有一个 /
,它代表 或
的意思,意思是 colunm_clause
是由 column_name
或者 一个 *
组成。 除此之外,还可以发现,有很多语法和 JS 的正则表达式是相似的,比如
[A-Za-z0-9] ident_part+ 'from'i
分别代表的意思是
ident_part from
编写完了之后我们尝试将这个语法文件编译成 JS 可以使用的 parser
pegjs -o dist/selectParser.js peg/select.peg 复制代码
我们这里指定了一个编译输出文件 dist/selectParser.js
和待编译的语法文件 peg/select.peg
,运行结束后,我们可以在 dist 文件夹中看见看见我们的 parser 文件 selectParser.js
。
生成 parser 文件之后就是使用它了
const selectParser=require("./dist/selectParser.js"); console.log(selectParser.parse("select name from user;")) 复制代码
这里调用了 parser 文件的 parse 方法来开始解析,不过,我们却得到了一个错误
> node index.js SyntaxError: Expected "*" or [A-Za-z0-9] but " " found. 复制代码
这里的意思是,parser希望收到 "*"
或 [A-Za-z0-9]
,不过却得到了一个空字符串。回到我们的语法解析文件开头
selectStatement = select colunm_clause from table_name ';' 复制代码
只有 colunm_clause
是由 "*"
和 [A-Za-z0-9]
组成的,那看来是 parser 把我们的输入 select name from user;
中 select
和 name
中间的 空格 当作 colunm_clause
来解析了,导致报错,所以我们需要再完善一下原来的语法文件,加入空格的解析。
selectStatement = select _ colunm_clause _ from _ table_name __ ';' __ = whitespace* _ = whitespace+ whitespace = [ \t\r\n]; 复制代码
现在我们再次编译运行,我们得到了正确的输出
> node index.js [ 'select', [ ' ' ], [ 'n', 'a', 'm', 'e' ], [ ' ' ], 'from', [ ' ' ], [ 'u', 's', 'e', 'r' ], [], ';' ] 复制代码
现在结构是解析出来了,不过,展示上却不是非常的美观,比如 name
和 user
被拆分成了字符数组,这是因为我们在 table_name
和 column_name
中使用了 +
,所以输出的时候,也会变成数组输出;还有,我们其实并不需要空白字符,但是结果里却也包含了它 [ ' ' ]
。因此,我们需要再处理一下匹配的数据。
selectStatement = select _ colunm_clause:colunm_clause _ from _ table_name:table_name __ ';'{ return `column_name=${colunm_clause}, table_name=${table_name}`} column_name = name:ident_part+ {return name.join("")} table_name = name:ident_part+ {return name.join("")} 复制代码
这里我们用到了 PEG.js 的 action ,我们可以在 aciton 里面写 JS 代码,它会在规则匹配成功的时候执行,同时,我们也给规则取了名字(例如 name:ident_part+
中的 name
),以便于我们在 action 中使用。
现在,再次运行编译执行过程,不出意外,我们可以得到以下输出
> node index.js column_name=name, table_name=user 复制代码
这样,一个最最基础的 select 语句解析就完成了~我们可以锦上添花,让它支持多条 sql 语句。完整代码如下:
PEG
start = selectStatements:selectStatement* { return selectStatements.join("\n") } selectStatement = select _ colunm_clause:colunm_clause _ from _ table_name:table_name __ ';'{ return `column_name=${colunm_clause}, table_name=${table_name}`} colunm_clause = column_name / '*' column_name = name:ident_part+ {return name.join("")} table_name = name:ident_part+ {return name.join("")} ident_part = [A-Za-z0-9] select = 'select'i from = 'from'i __ = whitespace* _ = whitespace+ whitespace = [ \t\r\n]; 复制代码
JS
const addParser=require("./dist/addParser.js"); const selectParser=require("./dist/selectParser.js"); const sqls=[ 'select * from user;', 'select name from user;', 'select id from user;' ].join("") console.log(selectParser.parse(sqls)) 复制代码
运行
> node index.js column_name=*, table_name=user column_name=name, table_name=user column_name=id, table_name=user 复制代码
回过头来,我们发现,借助 PEG.js 编写的 parser 很容易维护,整个语法的描述都是有结构的,并且借助于 action ,我们可以很方便的返回我们所需要的结构。
和正则表达式相比,唯一的缺点就是 PEG.js 生成的 parser 更占空间,加载上相对慢一些,不过,我们开发效率和代码的可维护性也有了比较大的提升,综合比较下, PEG.js 还是一个很不错的解决方案。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Docker - 入门(一),基础使用
- 一)redis 基础介绍与使用
- Java Properties类使用基础
- golang基础学习-MongoDB使用
- gitlab命令行使用(基础篇)
- 安卓SQLite基础使用指南
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。