寫在前面
在介紹 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
分別代表的意思是
- 匹配字母和數字
- 匹配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;
中 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 還是一個很不錯的解決方案。