PEG.js 介紹與基礎使用

寫在前面

在介紹 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

這裏的正則表達式就是一個用來解析 sql 語句的 parser ,那麼有沒有一種更加簡即可維護的工具能用來編寫 parser 呢? PEG.js 就是一個很適合咱們的工具。 不過,它和正則表達式仍是有一點區別。


PEG.js 是一個 parser generator ,它會將咱們用 PEG.js 語法編寫的 sql 語法文件轉化爲能夠直接運行的 parser , 而咱們也是經過這個parser去解析 sql 語句。因此咱們的工做就是編寫 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 文件

在 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

分別表明的意思是

  • 匹配字母和數字
  • 匹配1個或多個 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 仍是一個很不錯的解決方案。

相關文章
相關標籤/搜索