在介绍 PEG.js 以前,咱们先来讲下咱们为何须要它。javascript
假若有这样一个场景:用户输入一句简单的 sql 查询语句 select name from user
, 咱们的任务是获取到里面的列名,表名。在现有工具下,咱们第一个想到的就是正则表达式,熟悉的同窗应该能很快写出这样的正则表达式:java
/^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
这两种状况咱们都没考虑到,因此咱们还须要完善一下正则表达式node
/^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"]
,从而咱们根据结构化的数据来获取本身想要的信息。sql
首先,咱们将 PEG.js 安装到全局下npm
npm install -g pegjs
复制代码
安装好了能够验证一下数组
$ pegjs -v
PEG.js 0.10.0
复制代码
而后咱们在新建一个目录用来存放咱们代码。本文的结构以下bash
peg
文件夹用来存放咱们的语法文件,
dist
文件夹用来存放咱们生成的 parser。建立完项目以后,咱们下面就开始来正式使用 PEG.js 来解析 sql。
首先,咱们先肯定一下咱们要实现的 select 语句,本文为了便捷,不会编写完善的语法文件,因此这里以最简单的 select 语句为例子。ide
select
column_name | *
from
table_name;
复制代码
在 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 仍是一个很不错的解决方案。