PEG.js 介绍与基础使用

栏目: 数据库 · 发布时间: 7年前

内容简介:假如有这样一个场景:用户输入一句简单的 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 介绍与基础使用
这里的正则表达式就是一个用来解析 sql 语句的 parser ,那么有没有一种更加简便可维护的工具能用来编写 parser 呢? PEG.js

就是一个很适合我们的工具。 不过,它和正则表达式还是有一点区别。

PEG.js 介绍与基础使用
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.js 介绍与基础使用
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;selectname 中间的 空格 当作 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' ],
  [],
  ';' 
]
复制代码

现在结构是解析出来了,不过,展示上却不是非常的美观,比如 nameuser 被拆分成了字符数组,这是因为我们在 table_namecolumn_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 还是一个很不错的解决方案。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Programming in Haskell

Programming in Haskell

Graham Hutton / Cambridge University Press / 2007-1-18 / GBP 34.99

Haskell is one of the leading languages for teaching functional programming, enabling students to write simpler and cleaner code, and to learn how to structure and reason about programs. This introduc......一起来看看 《Programming in Haskell》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

在线进制转换器
在线进制转换器

各进制数互转换器

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器