背景
作為一個程式設計師,心裡一直有一個手擼編譯器的夢,奈何技術不夠一直沒能付諸實踐,JSON雖然不是一門語言,但很適合用來作為編譯器的練手,原因在於
- 關鍵字較少,結構簡單
- 語法簡單,沒有判斷,迴圈等高階語言語法
- 文字格式,測試比較方便
雖然寫程式碼硬解析也能做到,但總歸不科學,對於複雜的語法,硬解析根本無法解決。從阮一峰的部落格瞭解到the-super-tiny-compiler這個專案,該專案是一個迷你編譯器,將lisp表示式轉化為c語言表達時,程式碼去掉註釋不到200行,很適合用來學習,這個專案給了我很多啟發,開始對寫一個json解析器有了一點思路,該系列博文將記錄一個完成json解析器的實現過程,當然我自己也是小白,不是什麼編譯器專家,只是希望給同樣是小白的你一點參考,大神可以繞道。
說明
如何把一個json字串解析成一個java物件,大概要分為一下步驟
- 分詞(tokenizer)將json字串分解成一個個獨立的單元,比如下面這個簡單的json字串
{
"name": "asan",
"age": 32
}
經過分詞後會分解成下面這種格式
[
{"type":"object","value":"{","valueType":"object"},{"type":"key","value":"name","valueType":"string"},{"type":"value","value":"asan","valueType":"string"},{"type":"key","value":"age","valueType":"string"},{"type":"value","value":32,"valueType":"number"},{"type":"object","value":"}","valueType":"object"}
]
這期間會將無用字元過濾,分解為一個個token
- 解析抽象語法樹(AST):tokenizer只是將字串分解平鋪,AST負責將平鋪的各個token按照語義變成一棵樹,帶有層級結構,比如上面的token解析抽象語法樹如下
{
"items": [
{
"name": "name",
"type": "value",
"value": "asan"
},
{
"name": "age",
"type": "value",
"value": 32
}
],
"type": "object"
}
- 物件生成,根據抽象語法樹生成物件
無論是tokenizer還是ast,格式都不是固定,上面只是一個參考,但作用都是類似的,基本上解析器都要經過tokenizer和ast兩個步驟。
分詞(tokenizer)
示例json
如無特殊說明,後續程式都是基於以下這個json進行測試
{
"name": "asan",
"age": 32,
"mail": null,
"married": true,
"birthday": "1992-02-08",
"salary": 1234.56,
"deposit": -342.34,
"description": "a \"hundsome\" man",
"tags": [
"coder",
"mid-age"
],
"location": {
"province": "福建省",
"city": "泉州市",
"area": "晉江市"
},
"family": [
{
"relation": "couple",
"name": "Helen"
},
{
"relation": "daughter",
"name": "XiaoMan"
}
]
}
該示例基本包含了json所有常用的元素,可以滿足基本的測試
- 基本資料型別:字串,整型,浮點型,日期,null,布林值
- 物件(location)
- 陣列(family)
- 基本型別陣列(tags)
分詞
我們首先定義一個儲存分詞結果的結構,該結構至少需要包含以下兩個欄位
- type:token型別,包含object(物件),array(陣列),key(欄位名),value(欄位值),kvSymbol(key-value之間的冒號:)
- value:token值
但一個type可能不足以描述json,比如json的value有字串,整型,浮點型等,但type都是value
,你可能會說為什麼不每個定義一個型別呢,如果每個定義一個型別,那麼到時候判斷該token是不是value型別的時候就比較麻煩,需要依次判斷是不是字串,整型,浮點型等,因此我們增加了一個欄位valueType用來儲存值型別
- type:token型別
- value:token值
- valueType:值型別(string,bool,null,number)
我們暫時不去定義列舉,先把解析器實現再去重構程式碼,暫時不考慮程式碼的合理性。
以下是第一版本的解析器
import java.util.ArrayList;
import java.util.List;
/**
* @Description:
* @author: jianfeng.zheng
* @since: 2022/12/20 11:56
* @history: 1.2022/12/20 created by jianfeng.zheng
*/
public class JSONParser {
//currentIndex儲存當前字串掃描的位置,字串是逐字元進行掃描
private int currentIndex = 0;
/**
* 對json字串進行分詞
*
* @param json json字串
* @return token列表
*/
public List<Token> tokenizer(String json) {
// 儲存分詞結果
List<Token> tokens = new ArrayList<>();
while (currentIndex < json.length()) {
char c = json.charAt(currentIndex);
//對於空白符可以直接跳過,如果有更多的空白符只需新增新的判斷即可
if (c == ' ' || c == '\r' || c == '\n' || c == ',') {
//字元只要處理過了必須要將當前位置移動到下一個
++currentIndex;
continue;
} else if (c == '{' || c == '}') {
//物件
tokens.add(new Token("object", c));
} else if (c == '[' || c == ']') {
//陣列
tokens.add(new Token("array", c));
} else if (c == '"') {
//字串
StringBuffer value = new StringBuffer();
char cc = json.charAt(++currentIndex);
// 這裡以"作為字串結束符的標誌
// 當然這個不嚴謹因為沒考慮到轉義,但這個問題留著後面解決,我們暫時忽略
while (cc != '"') {
value.append(cc);
cc = json.charAt(++currentIndex);
}
tokens.add(new Token("string", value.toString()));
} else if (c == ':') {
// key-value中間的分隔符
tokens.add(new Token("kvSymbol", "kvSymbol", c));
} else if (c >= '0' && c <= '9') {
//數字
StringBuffer value = new StringBuffer();
value.append(c);
char cc = json.charAt(++currentIndex);
//這裡考慮到帶有小數點的浮點數
while (cc == '.' || (cc >= '0' && cc <= '9')) {
value.append(cc);
cc = json.charAt(++currentIndex);
}
//數字暫時統一用浮點數進行表示
tokens.add(new Token("value", "number", Float.parseFloat(value.toString())));
}
++currentIndex;
}
return tokens;
}
}
程式碼流程如下
- 迴圈遍歷json字串
- 檢測關鍵字,並識別出關鍵字以token儲存
- 對於字串的處理目前不論是key還是value統一儲存的是string型別
- 對於數字型別的處理目前都是以Float浮點數進行儲存
測試
我們寫一個程式程式進行測試
public class Main {
public static void main(String[] args) {
String json = "{\"name\": \"asan\", \"age\": 32}";
JSONParser parser = new JSONParser();
List<Token> tokens = parser.tokenizer(json);
System.out.println(String.format("|%-12s|%-12s|%-15s|", "type", "valueType", "value"));
System.out.println("-------------------------------------------");
for (Token t : tokens) {
System.out.println(String.format("|%-12s|%-12s|%-15s|",
t.getType(),
t.getValueType(),
t.getValue()));
}
System.out.println("-------------------------------------------");
}
}
我們先拿一個比較簡單的json{"name": "asan", "age": 32}
進行測試,測試結果如下
|type |valueType |value |
-------------------------------------------
|object |object |{ |
|string |string |name |
|kvSymbol |kvSymbol |: |
|string |string |asan |
|string |string |age |
|kvSymbol |kvSymbol |: |
|value |number |32.0 |
-------------------------------------------
目前這個結果符合我們的預期。
最佳化
其他基本型別
目前程式value型別程式只處理了字串和數字,bool和null型別未處理,由於程式是一個字元一個字元對字串進行掃描,但要判斷bool和null必須往後進行掃描。
- 判斷null
if ((c == 'n') &&
json.startsWith("null", currentIndex)) {
tokens.add(new Token("value", "null", null));
//如果讀取到null值需要將當前指標往前移動3個字元(null佔4個字元,除去已經讀取到的1個字串還需要移動3個字元)
currentIndex += 3;
}
- 判斷bool值
bool值的判斷就是判斷true和false兩個字串,和判斷空值類似
if ((c == 't') &&
json.startsWith("true", currentIndex)) {
tokens.add(new Token("value", "bool", true));
currentIndex += 3;
}
if ((c == 'f') &&
json.startsWith("false", currentIndex)) {
tokens.add(new Token("value", "bool", false));
//false是5個字元因此需要移動4位
currentIndex += 4;
}
我們將測試的json字串修改為{"name": "asan", "age": 32,"mail": null,"married": true}
再進行測試,結果如下
|type |valueType |value |
-------------------------------------------
|object |object |{ |
|string |string |name |
|kvSymbol |kvSymbol |: |
|string |string |asan |
|string |string |age |
|kvSymbol |kvSymbol |: |
|value |number |32.0 |
|string |string |mail |
|kvSymbol |kvSymbol |: |
|value |null |null |
|string |string |married |
|kvSymbol |kvSymbol |: |
|value |bool |true |
|object |object |} |
-------------------------------------------
結果符合預期。
字串處理
字串目前的處理方式是檢測到"
就當作是字串,直到下一個"
出現,但這種處理方式是不嚴謹的,有可能字串本身就包含了"
,因此需要對跳脫字元進行處理,我們修改字串的處理函式
if (c == '"') {
//字串
StringBuffer value = new StringBuffer();
char cc = json.charAt(++currentIndex);
// 這裡以"作為字串結束符的標誌
while (cc != '"') {
if (cc == '\\') {
cc = json.charAt(++currentIndex);
}
value.append(cc);
cc = json.charAt(++currentIndex);
}
tokens.add(new Token("string", value.toString()));
}
我們將測試字串改為{"name": "asan", "age": 32,"description": "a \"hudsom\" man","married": true}
再測試,結果如下
|type |valueType |value |
-------------------------------------------
|object |object |{ |
|string |string |name |
|kvSymbol |kvSymbol |: |
|string |string |asan |
|string |string |age |
|kvSymbol |kvSymbol |: |
|value |number |32.0 |
|string |string |description |
|kvSymbol |kvSymbol |: |
|string |string |a "hudsom" man |
|string |string |married |
|kvSymbol |kvSymbol |: |
|value |bool |true |
|object |object |} |
-------------------------------------------
成功識別到了跳脫字元串
數字處理
數字處理目前也有一些問題
- 沒有處理負數情況
- 沒有處理科學技術法
- 沒有區分浮點和整型統一都是浮點數
程式修改如下
if ((c >= '0' && c <= '9') || c == '-') {
// 數字
StringBuffer value = new StringBuffer();
value.append(c);
// 判斷是不是浮點數
boolean isFloat = false;
//如果json是一位整型比如:1,那麼這裡不判斷就會報錯
if (currentIndex + 1 < json.length()) {
char cc = json.charAt(++currentIndex);
// 判斷包含浮點型,整型,科學技術法
while (cc == '.' || (cc >= '0' && cc <= '9') || cc == 'e' || cc == 'E' || cc == '+' || cc == '-') {
value.append(cc);
if (cc == '.') {
isFloat = true;
}
cc = json.charAt(++currentIndex);
}
}
if (isFloat) {
//浮點數
tokens.add(new Token("value", "float", Float.parseFloat(value.toString())));
} else {
//整型
tokens.add(new Token("value", "long", Long.parseLong(value.toString())));
}
}
我們用字串{"age":32,"deposit": -342.34}
進行測試,測試結果如下
|type |valueType |value |
-------------------------------------------
|object |object |{ |
|string |string |age |
|kvSymbol |kvSymbol |: |
|value |long |32 |
|string |string |deposit |
|kvSymbol |kvSymbol |: |
|value |float |-342.34 |
-------------------------------------------
完整測試
我們用完整的字串進行測試,結果如下
|type |valueType |value |
-------------------------------------------
|object |object |{ |
|string |string |name |
|kvSymbol |kvSymbol |: |
|string |string |asan |
|string |string |age |
|kvSymbol |kvSymbol |: |
|value |long |32 |
|string |string |married |
|kvSymbol |kvSymbol |: |
|value |bool |true |
|string |string |birthday |
|kvSymbol |kvSymbol |: |
|string |string |1992-02-08 |
|string |string |salary |
|kvSymbol |kvSymbol |: |
|value |float |1234.56 |
|string |string |description |
|kvSymbol |kvSymbol |: |
|string |string |a "hudsom" man |
|string |string |tags |
|kvSymbol |kvSymbol |: |
|array |array |[ |
|string |string |coder |
|string |string |mid-age |
|array |array |] |
|string |string |location |
|kvSymbol |kvSymbol |: |
|object |object |{ |
|string |string |province |
|kvSymbol |kvSymbol |: |
|string |string |福建省 |
|string |string |city |
|kvSymbol |kvSymbol |: |
|string |string |泉州市 |
|string |string |area |
|kvSymbol |kvSymbol |: |
|string |string |晉江市 |
|object |object |} |
|string |string |family |
|kvSymbol |kvSymbol |: |
|array |array |[ |
|object |object |{ |
|string |string |relation |
|kvSymbol |kvSymbol |: |
|string |string |couple |
|string |string |name |
|kvSymbol |kvSymbol |: |
|string |string |Helen |
|object |object |} |
|object |object |{ |
|string |string |relation |
|kvSymbol |kvSymbol |: |
|string |string |daughter |
|string |string |name |
|kvSymbol |kvSymbol |: |
|string |string |XiaoMan |
|object |object |} |
|array |array |] |
|object |object |} |
-------------------------------------------
完整程式碼
public class JSONParser {
//currentIndex儲存當前字串掃描的位置,字串是逐字元進行掃描
private int currentIndex = 0;
/**
* 對json字串進行分詞
*
* @param json json字串
* @return token列表
*/
public List<Token> tokenizer(String json) {
// 儲存分詞結果
List<Token> tokens = new ArrayList<>();
while (currentIndex < json.length()) {
char c = json.charAt(currentIndex);
//對於空白符可以直接跳過,如果有更多的空白符只需新增新的判斷即可
if (c == ' ' || c == '\r' || c == '\n' || c == ',') {
//字元只要處理過了必須要將當前位置移動到下一個
++currentIndex;
continue;
} else if (c == '{' || c == '}') {
//物件
tokens.add(new Token("object", c));
} else if (c == '[' || c == ']') {
//陣列
tokens.add(new Token("array", c));
} else if (c == '"') {
//字串
StringBuffer value = new StringBuffer();
char cc = json.charAt(++currentIndex);
// 這裡以"作為字串結束符的標誌
while (cc != '"') {
if (cc == '\\') {
cc = json.charAt(++currentIndex);
}
value.append(cc);
cc = json.charAt(++currentIndex);
}
tokens.add(new Token("string", value.toString()));
} else if (c == ':') {
// key-value中間的分隔符
tokens.add(new Token("kvSymbol", "kvSymbol", c));
} else if ((c >= '0' && c <= '9') || c == '-') {
// 數字
StringBuffer value = new StringBuffer();
value.append(c);
// 判斷是不是浮點數
boolean isFloat = false;
//如果json是一位整型比如:1,那麼這裡不判斷就會報錯
if (currentIndex + 1 < json.length()) {
char cc = json.charAt(++currentIndex);
// 判斷包含浮點型,整型,科學技術法
while (cc == '.' || (cc >= '0' && cc <= '9') || cc == 'e' || cc == 'E' || cc == '+' || cc == '-') {
value.append(cc);
if (cc == '.') {
isFloat = true;
}
cc = json.charAt(++currentIndex);
}
}
if (isFloat) {
//浮點數
tokens.add(new Token("value", "float", Float.parseFloat(value.toString())));
} else {
//整型
tokens.add(new Token("value", "long", Long.parseLong(value.toString())));
}
} else if ((c == 'n') && json.startsWith("null", currentIndex)) {
tokens.add(new Token("value", "null", null));
currentIndex += 3;
} else if ((c == 't') &&
json.startsWith("true", currentIndex)) {
tokens.add(new Token("value", "bool", true));
currentIndex += 3;
} else if ((c == 'f') &&
json.startsWith("false", currentIndex)) {
tokens.add(new Token("value", "bool", false));
//false是5個字元因此需要移動4位
currentIndex += 4;
}
++currentIndex;
}
//將當前位置重置
currentIndex = 0;
return tokens;
}
}
程式碼
完整程式碼請參考專案https://github.com/wls1036/tiny-json-parser的0x01分支