難點:
- 如何拆詞?如何定義分隔符?
- 匹配的優先順序是什麼?
關鍵點:
- 有限自動機
- 元素拆分
解析 age >= 45
為了入門字詞是如何拆分識別的,我們舉一個最簡單的例子age >= 45
- 只有三種型別:識別符號(age)、大於號(GE)、數字字面量(IntLiteral)
- 使用空格分隔不同的元素
思路:
- 從左到右依次讀取字串
- 使用有限自動機,根據讀到的字元進行狀態轉換,狀態機如下
先上程式碼,理解一下上述過程,也可以除錯進去看看執行的邏輯是什麼樣的。
SimpleToken.java
/**
* Token的一個簡單實現。只有型別和文字值兩個屬性。
*/
public final class SimpleToken implements Token {
//Token型別
public TokenType type = null;
//文字值
public String text = null;
@Override
public TokenType getType() {
return type;
}
@Override
public String getText() {
return text;
}
}
public interface Token{
public TokenType getType();
public String getText();
}
SimpleTokenReader
public class SimpleTokenReader implements TokenReader {
List<Token> tokens = null;
int pos = 0;
public SimpleTokenReader(List<Token> tokens) {
this.tokens = tokens;
}
@Override
public Token read() {
if (pos < tokens.size()) {
return tokens.get(pos++);
}
return null;
}
@Override
public Token peek() {
if (pos < tokens.size()) {
return tokens.get(pos);
}
return null;
}
@Override
public void unread() {
if (pos > 0) {
pos--;
}
}
@Override
public int getPosition() {
return pos;
}
@Override
public void setPosition(int position) {
if (position >=0 && position < tokens.size()){
pos = position;
}
}
}
public interface TokenReader{
public Token read();
public Token peek();
public void unread();
public int getPosition();
public void setPosition(int position);
}
MyLexer.java
public class MyLexer {
private StringBuffer tokenText = null; //臨時儲存token的文字
private List<Token> tokens = null; //儲存解析出來的Token
private SimpleToken token = null; //當前正在解析的Token
public static void main(String[] args) {
MyLexer lexer = new MyLexer();
String script = "age >= 45";
System.out.println("parse: " + script);
SimpleTokenReader tokenReader = lexer.tokenize(script);
dump(tokenReader);
}
//是否是字母
private boolean isAlpha(int ch) {
return ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z';
}
//是否是數字
private boolean isDigit(int ch) {
return ch >= '0' && ch <= '9';
}
//是否是空白字元
private boolean isBlank(int ch) {
return ch == ' ' || ch == '\t' || ch == '\n';
}
// 有限狀態機的各種狀態。
private enum DfaState {
Initial,
Id, GT, GE,
IntLiteral
}
/**
* 有限狀態機進入初始狀態。
* 這個初始狀態其實並不做停留,它馬上進入其他狀態。
* 開始解析的時候,進入初始狀態;某個Token解析完畢,也進入初始狀態,在這裡把Token記下來,然後建立一個新的Token。
*/
private DfaState initToken(char ch) {
if (tokenText.length() > 0) {
token.text = tokenText.toString();
tokens.add(token);
tokenText = new StringBuffer();
token = new SimpleToken();
}
DfaState newState = DfaState.Initial;
if (isAlpha(ch)) { //第一個字元是字母
newState = DfaState.Id; //進入Id狀態
token.type = TokenType.Identifier;
tokenText.append(ch);
} else if (isDigit(ch)) { //第一個字元是數字
newState = DfaState.IntLiteral;
token.type = TokenType.IntLiteral;
tokenText.append(ch);
} else if (ch == '>') { //第一個字元是>
newState = DfaState.GT;
token.type = TokenType.GT;
tokenText.append(ch);
} else {
newState = DfaState.Initial; // skip all unknown patterns
}
return newState;
}
/**
* 解析字串,形成Token。
* 這是一個有限狀態自動機,在不同的狀態中遷移。
* @param code
* @return
*/
public SimpleTokenReader tokenize(String code) {
tokens = new ArrayList<Token>();
CharArrayReader reader = new CharArrayReader(code.toCharArray());
tokenText = new StringBuffer();
token = new SimpleToken();
int ich = 0;
char ch = 0;
DfaState state = DfaState.Initial;
try {
while ((ich = reader.read()) != -1) {
ch = (char) ich;
switch (state) {
case Initial:
state = initToken(ch); //重新確定後續狀態
break;
case Id:
if (isAlpha(ch) || isDigit(ch)) {
tokenText.append(ch); //保持識別符號狀態
} else {
state = initToken(ch); //退出識別符號狀態,並儲存Token
}
break;
case GT:
if (ch == '=') {
token.type = TokenType.GE; //轉換成GE
state = DfaState.GE;
tokenText.append(ch);
} else {
state = initToken(ch); //退出GT狀態,並儲存Token
}
break;
case IntLiteral:
if (isDigit(ch)) {
tokenText.append(ch); //繼續保持在數字字面量狀態
} else {
state = initToken(ch); //退出當前狀態,並儲存Token
}
break;
default:
}
}
// 把最後一個token送進去
if (tokenText.length() > 0) {
initToken(ch);
}
} catch (IOException e) {
e.printStackTrace();
}
return new SimpleTokenReader(tokens);
}
public static void dump(SimpleTokenReader tokenReader){
System.out.println("text\ttype");
Token token = null;
while ((token= tokenReader.read())!=null){
System.out.println(token.getText()+"\t\t"+token.getType());
}
}
}
不難理解,對吧。
無非就是在 tokenize
函式中挨個讀取字串,根據上面自動機實現的邏輯。
遇到分隔字元(如空格)就會觸發 initToken
將前面讀取到的字元和型別進行儲存。
你可能會有疑問:
搞這麼複雜幹什麼?按空格切分然後再字串匹配不就行了?
確實可以實現,使用這種方式實現還更簡單,但是我們想要做的是一個更通用的處理邏輯。
如果按照提議的方式,對於更復雜的字串(比如不是空格分隔、空格不一定是分隔符、關鍵字保留等)那就需要更多的人工邏輯來處理,而且會越來越複雜和難以擴充套件,很可能一個特例導致需要推倒重來。