Tuesday, February 24, 2009

ANTLR (4) - Simple Lexer/Parser Example

上篇介紹一個簡單的Lexer範例後,接著我們把Parser也拿進來一起講。在下面的例子中,我們會讀進一個文字檔,讓這個文字檔根據我們想要的格式再做一次編排。如下圖所示。



大概想一下要怎麼把上面的data轉換成下面的格式?先前的Lexer可以幫我們辨識文件裡的組成元件,分辨什麼是數字,分辨什麼是字母。但就原始data來看,我們還必須讓程式知道每一筆資料,到底以什麼樣的順序包含了這些小元件。換句話說,我們必需分析一下這份資料檔案的結構,是如何由這些元件組成的。要分析一份資料,以top-down的思考方式為佳。

以上面的例子來說,以空白做為分隔,第一欄是生日(DOB, Date Of Birth),第二欄是姓名(NAME),和第三欄的年齡(AGE),最後用分號(SEMI)做節尾。這樣的文法結構,我們就寫在Parser裡。在第一篇概念介紹中提過,Parser的工作即是付予這些元件意義、辨識文件的整體結構。

範例檔案:Simple2.g
class SimpleParser extends Parser;

entry : (d:DOB n:NAME a:AGE(SEMI)
{
  System.out.println(
    "Name: " + n.getText() +
    ", Age: " + a.getText() +
    ", DOB: " + d.getText() );
}
)*
;
根據top-down的原則,我們先藉由Parser的語法把文件的結構描繪出來。

第一條rule命名為entry。這也是Java程式第一個開始parse文章的起點。這條rule要在文章中尋找的樣式是:生日(DOB),空一格,姓名(NAME),空一格,年齡(AGE),分號(SEMI)做結尾。至於為什麼最後的SEMI要用括號括起來?是因為在我們原始檔案中,分號是緊接著年齡出現,若不使用括號,就會變成AGESEMI,這啥…!?所以寫成AGE(SEMI),以便區隔這是兩個不同的元件。簡單來說,entry這條rule是要找出
DOB NAME AGE;
這樣的格式,也就是找出一串符合entry : (d:DOB n:NAME a:AGE(SEMI))*的文字。最後的星號 '*' 是代表entry可以出現0至多次。

d:DOB,前面的d:是指當我找到符合這樣格式的字串時,就順便把 DOB 指派給 d變數,之後就可以直接用這個變數來代表DOBn:NAME a:AGE 亦是如此。中間大括號的部份,是你可以放程式碼的地方,這裡就簡單的把剛剛那些變數,依照自己喜歡的順序在螢幕上列印出來。

描繪出文件的大架構後,我們就可以往下去定義每個元件(i.e., DOB, NAME, AGE)到底長什麼樣子,好讓程式一一辨識出哪些東西是DOB,哪些東西是NAME,又都是數字的話,那到底是DOB還是AGE。要辨識元件,就交給Lexer!(Parser是辨識整體架構。)
class SimpleLexer extends Lexer;

NAME : ('a'..'z'|'A'..'Z')+;

DOB : (('0'..'9')('0'..'9')('/'))=>
(('0'..'9')('0'..'9')'/')(('0'..'9')('0'..'9')'/')('0'..'9')('0'..'9')
| ('0'..'9')+      { $setType(AGE); } ;

WS :
(' '
| '\t'
| '\r' '\n'   { newline(); }
| '\n'        { newline(); }
)
{ $setType(Token.SKIP); }
;

SEMI : ';' ;

NAME : ('a'..'z'|'A'..'Z')+;
第一條Lexer的rule叫做NAME,按照字面翻譯,就是只要是由大小寫英文所組成的連續字元,我們都把它視為NAME


DOB : ('0'..'9' '0'..'9' '/')=>
(('0'..'9')('0'..'9')'/')(('0'..'9')('0'..'9')'/')('0'..'9')('0'..'9')
| ('0'..'9')+    { $setType(AGE); } ;
第二條rule稍稍複雜了些,主要是用來辨識生日(DOB)的格式(ex. 08/06/84),但如果同樣是數字字串,可是又不符合數字字元間有斜線字元('/'),那我們就把它另外當做是AGE元件。

先不管上面的語法代表什麼意思,直覺來想,我們會把DOBAGE當成兩條獨立的rule來寫:
DOB : ('0'..'9')('0'..'9')'/'('0'..'9')('0'..'9')'/'('0'..'9')('0'..'9') ;
AGE : ('0'..'9')+ ;
一條是符合"xx/xx/xx"的字串,一條是符合"xx"的字串。但如果這樣定義,會發現ANTLR在產生程式碼的過程中,會出現下面的警告訊息:
warning: lexical nondeterminism between rules DOB and AGE upon
simple2.g:0: k==1:'0'..'9'
也就是說ANTLR沒辦法在 options { k=1; }的判斷條件下(k=1,代表只比對第一個字元),保證正確無誤的指出以數字開頭的字串,是DOB還是AGE。但我們可以透過調整k的值來解決:options { k=3; },這樣表示我會至少會看完前面三個字元,再判斷我該屬於哪個元件。先看前三個字元,就知道數字後會不會跟著斜線字元('/'),有的話,就是DOB,沒有的話,就是AGE。(寫到這邊才驚覺,原來 Lexical rule 的命名需要全部都用大寫字母...。)

當Lexer中有很多這樣會讓程式感到困惑的判斷條件時,建議以調整k的數值來處理。如果只有一兩個時,可以採用範例的寫法:
DOB : (('0'..'9')('0'..'9')('/'))=>
這行是說看能不能在字串中找到兩個數字後面還接了一個斜線的格式("xx/"),如果可以的話,Lexer就必須往下比對下面定義的規則:
(('0'..'9')('0'..'9')'/')(('0'..'9')('0'..'9')'/')('0'..'9')('0'..'9')
若成功的符合了上面的規則,我就可以理所當然的把它歸類為DOB。但如果失敗了,或是根本不符合("xx/"),我們就必須再去比對 '|' 字元後的第二條件:
| ('0'..'9')+    { $setType(AGE); } ;
如果符合了('0'..'9')+這個條件,我就去呼叫ANTLR的專屬語法$setType(type),把符合這個條件的字串指派給AGE元件。


WS :
(' '
| '\t'
| '\r' '\n' { newline(); }
| '\n' { newline(); }
)
{ $setType(Token.SKIP); }
;
這段是定義一些空白、換行的字元該如何處理。$setType(Token.SKIP);即是當我遇到上面那些特殊字元時,就忽略跳過。根據官方說法{ newline(); }是告訴Lexer在遇到'\r' '\n'或是'\n'的換行字元要換行!不然會噎到。不過我試過把那兩個newline();拿掉,但好像還是好好的…迷漾的newline();,我搞不懂你。


SEMI : ';' ;
最後一條rule,看到 ';',即為SEMI

定義完這些規則後,就可以用ANTLR Tool把程式碼gen出來。
java antlr.Tool Simple2.g
除此之外,還必需準備一個含有main()的主程式來執行。
import java.io.*;
public class Main {
  public static void main(String args[]) {
    DataInputStream input = new DataInputStream(System.in);
    SimpleLexer lexer = new SimpleLexer(input);
    SimpleParser parser = new SimpleParser(lexer);
    try {
      parser.entry();
    } catch(Exception e) {}
  }
}
最後準備一個文字檔data.txt如下:
06/06/82 Peter 20;
03/04/83 Rosie 19;
04/05/81 Mikey 21;
將所有的*.java經過compile後,把文字檔讀入並執行,就可以得到我們想要的結果。
E:\workspace\ANTLR_Test>javac *.java
E:\workspace\ANTLR_Test>java Main < data.txt
Name: Peter, Age: 20, DOB: 06/06/82
Name: Rosie, Age: 19, DOB: 03/04/83
Name: Mikey, Age: 21, DOB: 04/05/81
假如我們把data.txt的21改成a21,會得到下面的結果:
E:\workspace\ANTLR_Test>java Main < data.txt
Name: Peter, Age: 20, DOB: 06/06/82
Name: Rosie, Age: 19, DOB: 03/04/83
line 3:16: expecting AGE, found 'a'
也就說原本預期在DOB NAME之後,應該是AGE,但現在卻發現是'a',所以程式就無法順利執行完。

ANTLR 系列文章:

ANTLR (1) - 概念介紹
ANTLR (2) - Lexer和Parser的文件格式
ANTLR (3) - Simple Lexer Example
ANTLR (4) - Simple Lexer/Parser Example

2 comments:

  1. 非常感謝妳的貼文, 幫助我很多, 真希望有機會能認識妳

    ReplyDelete