前言
Mapfile是MapServer用來描述一個地圖的配置檔案。它是一個很簡單的宣告式語言,一個地圖(Map)可以有多個層(Layer),每個層可以有很多屬性(鍵值對)。在一個層的定義中,還可以定義若干個類(Class),這個類用以管理不同的樣式(Style)。而每個類或者樣式都可以由若干個屬性(鍵值對)。
這裡有一個實際的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
LAYER NAME "counties" DATA "counties-in-shaanxi-3857" STATUS default TYPE POLYGON TRANSPARENCY 70 CLASS NAME "polygon" STYLE COLOR 255 255 255 OUTLINECOLOR 40 44 52 END END END |
最簡單的層的定義
最簡單的情形是,我們定義了一個層Layer,但是沒有指定任何的屬性:
1 2 |
LAYER END |
我們期望parser可以輸出:
1 |
{layer: null} |
要做到這一步,首先需要定義符號LAYER和END,以及一些對空格,非法字元的處理等:
1 2 3 4 5 6 |
\s+ /* skip whitespace */ \n|\r\n /* skip whitespace */ "LAYER" return "LAYER" "END" return "END" <<EOF>> return 'EOF' . return 'INVALID' |
對於,空格,回車換行等,我們都直接跳過。對應的BNF也非常簡單:
1 2 3 4 5 6 7 |
expressions : decls EOF {return $1;} ; decls : LAYER END {$ $ = {layer: null}} ; |
為層新增屬性
接下來我們來為層新增Name屬性,首先還是新增符號NAME和對字串的定義。這裡的字串被定義為:由雙引號括起來的所有內容。
1 2 3 |
"NAME" return "NAME" '"'("\\"["]|[^"])*'"' return 'STRING' [a-zA-Z]+ return 'WORD' |
然後我們就可以為BNF新增一個新的節:
1 2 3 4 5 6 7 8 |
decls: LAYER decl END {$ $ = {layer: $2}} ; decl: NAME STRING {$ $ = $2.substring(1, $2.length - 1)}; |
在decl中,我們將獲得的字串兩頭的引號去掉$2.substring。這樣decl的值就會是字串本身,而不是帶著雙引號的字串了。修改之後的程式碼可以解析諸如這樣的宣告:
1 2 3 |
LAYER NAME "counties" END |
併產生這樣的輸出:
1 |
{ layer: 'counties' } |
但是如果我們用來解析兩個以上的屬性:
1 2 3 4 |
LAYER NAME "counties" DATA "counties-in-shaanxi-3857" END |
解析器會報告一個錯誤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ node map.js expr /Users/jtqiu/develop/ideas/jison-demo/mapfile/map.js:106 throw new Error(str); ^ Error: Parse error on line 2: ... "counties" DATA "counti ----------------------^ Expecting 'END', got 'WORD' at Object.parseError (/Users/jtqiu/develop/ideas/jison-demo/mapfile/map.js:106:15) at Object.parse (/Users/jtqiu/develop/ideas/jison-demo/mapfile/map.js:171:22) at Object.commonjsMain [as main] (/Users/jtqiu/develop/ideas/jison-demo/mapfile/map.js:620:27) at Object.<anonymous> (/Users/jtqiu/develop/ideas/jison-demo/mapfile/map.js:623:11) at Module._compile (module.js:456:26) at Object.Module._extensions..js (module.js:474:10) at Module.load (module.js:356:32) at Function.Module._load (module.js:312:12) at Function.Module.runMain (module.js:497:10) at startup (node.js:119:16) |
即,期望一個END符號,但是卻看到了一個WORD符號。我們只需要稍事修改,就可以讓當前的語法支援多個屬性的定義:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
decls: LAYER pairs END {$ $ = {layer: $2}} ; pairs: pair {$ $ = $1} | pairs pair {$ $ = merge($1, $2)} ; pair: NAME STRING {$ $ = {name: $2.substring(1, $2.length - 1)}} | DATA STRING {$ $ = {data: $2.substring(1, $2.length - 1)}}; |
先看,pair的定義,它由NAME STRING或者DATA STRING組成,是我們語法中的終結符。再來看pairs的定義:
1 |
pairs: pair | pairs pair; |
這個遞迴的定義可以保證我們可以寫一條pair或者多條pairs pair屬性定義語句。而對於多條的情況,我們需要將這行屬性規約在一起,即當遇到這樣的情形時:
1 2 |
NAME "counties" DATA "counties-in-shaanxi-3857" |
我們需要產生這樣的輸出:{name: “counties”, data: “counties-in-shaanxi-3857”}。但是由於符號是逐個匹配的,我們會得到這樣的匹配結果:{name: “counties”}和{data: “counties-in-shaanxi-3857”},因此我們需要編寫一個簡單的函式來合併這些屬性:
1 2 3 4 5 6 7 8 9 10 11 12 |
function merge(o1, o2) { var obj = {}; for(var k in o1) { obj[k] = o1[k]; } for(var v in o2) { obj[v] = o2[v]; } return obj; } |
按照慣例,這種自定義的函式需要被定義在%{和}%括起來的section中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
... [a-zA-Z]+ return 'WORD' [0-9]+("."[0-9]+)? return 'NUMBER' <<EOF>> return 'EOF' . return 'INVALID' /lex %{ function merge(o1, o2) { var obj = {}; for(var k in o1) { obj[k] = o1[k]; } for(var v in o2) { obj[v] = o2[v]; } return obj; } %} %start expressions %% /* language grammar */ ... |
現在我們的解析器就可以識別多條屬性定義了:
1 2 |
$ node map.js expr { layer: { name: 'counties', data: 'counties-in-shaanxi-3857' } } |
巢狀的結構
現在新的問題又來了,我們的解析器現在可以識別對層的對個屬性的解析了,不過由於CLASS並不是由簡單的鍵值對定義的,所以還需要進一步的修改:
1 2 3 4 |
classes: CLASS pairs END {$ $ = {class: $2}} ; |
類由CLASS關鍵字和END關鍵字定義,而類的屬性定義和Layer的屬性定義並無二致,都可以使用pairs(多條屬性)。而classes事實上是pair的另一種形式,就像對屬性的定義一樣,所以:
1 2 3 4 5 6 7 |
pair: NAME STRING {$ $ = {name: $2.substring(1, $2.length - 1)}} | DATA STRING {$ $ = {data: $2.substring(1, $2.length - 1)}} | classes {$ $ = $1}; |
這樣,解析器就可以識別CLASS子句了。我們注意到,在CLASS中,還可以定義STYLE,因此又需要稍作擴充套件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
pair: NAME STRING {$ $ = {name: $2.substring(1, $2.length - 1)}} | DATA STRING {$ $ = {data: $2.substring(1, $2.length - 1)}} | classes {$ $ = $1} | styles {$ $ = $1}; styles: STYLE pairs END {$ $ = {style: $2}} ; |
這樣,我們的解析器就可以處理樣例中的所有語法了:
1 2 3 4 5 6 7 |
node map.js expr { layer: { name: 'counties', data: 'counties-in-shaanxi-3857', status: 'default', type: 70, class: { name: 'polygon', style: [Object] } } } |
完整的程式碼在github上的這個repo中。
總結
使用BNF定義一個複雜配置檔案的規則,事實上一個比較容易的工作。要手寫這樣一個解析器需要花費很多的時間,而且當你需要parser多種配置檔案時,這將是一個非常無聊且痛苦的事情。學習jison可以幫助你很快的編寫出小巧的解析器,在上面的Mapfile的例子中,所有的程式碼還不到100行。下一次再遇到諸如複雜的文字解析,配置檔案讀取的時候,先不要忙著編寫正規表示式,試試更高效,更輕便的jison吧。