擼一個 JSON 解析器

JSON JavaScript Java Perl XML C語言 Python Java芋道源碼 2019-05-10
  • JSON
  • 解析JSON
  • JSON解析器的基本原理
  • 步驟
  • 獲取token流
  • 解析出JSON對象
  • 參考文章

JSON

JSON(JavaScript Object Notation, JS 對象簡譜) 是一種輕量級的數據交換格式。易於人閱讀和編寫。同時也易於機器解析和生成。採用完全獨立於語言的文本格式,但是也使用了類似於C語言家族的習慣(包括C, C++, C#, Java, JavaScript, Perl, Python等)。這些特性使JSON成為理想的數據交換語言。

JSON與JS的區別以及和XML的區別具體請參考百度百科

JSON有兩種結構:

第一種:對象

“名稱/值”對的集合不同的語言中,它被理解為對象(object),紀錄(record),結構(struct),字典(dictionary),哈希表(hash table),有鍵列表(keyed list),或者關聯數組 (associative array)。

對象是一個無序的“‘名稱/值’對”集合。一個對象以“{”(左括號)開始,“}”(右括號)結束。每個“名稱”後跟一個“:”(冒號);“‘名稱/值’ 對”之間使用“,”(逗號)分隔。

 {"姓名": "張三", "年齡": "18"}

第二種:數組

值的有序列表(An ordered list of values)。在大部分語言中,它被理解為數組(array)。

數組是值(value)的有序集合。一個數組以“[”(左中括號)開始,“]”(右中括號)結束。值之間使用“,”(逗號)分隔。

值(value)可以是雙引號括起來的字符串(string)、數值(number)、true、false、 null、對象(object)或者數組(array)。這些結構可以嵌套。

 [
{
"姓名": "張三",
"年齡":"18"
},
{
"姓名": "里斯",
"年齡":"19"
}
]

通過上面的瞭解可以看出,JSON存在以下幾種數據類型(以Java做類比):

jsonjavastringJava中的StringnumberJava中的Long或Doubletrue/falseJava中的BooleannullJava中的null[array]Java中的List或Object[]{“key”:”value”}Java中的Map

解析JSON

JSON解析器的基本原理

輸入一串JSON字符串,輸出一個JSON對象。

步驟

JSON解析的過程主要分以下兩步:

第一步:對於輸入的一串JSON字符串我們需要將其解析成一組token流。

例如 JSON字符串{“姓名”: “張三”, “年齡”: “18”} 我們需要將它解析成

 {、 姓名、 :、 張三、 ,、 年齡、 :、 18、 }

這樣一組token流

第二步:根據得到的token流將其解析成對應的JSON對象(JSONObject)或者JSON數組(JSONArray)

下面我們來詳細分析下這兩個步驟:

獲取token流

根據JSON格式的定義,token可以分為以下幾種類型

token含義NULLnullNUMBER數字STRING字符串BOOLEANtrue/falseSEP_COLON:SEP_COMMA,BEGIN_OBJECT{END_OBJECT}BEGIN_ARRAY[END_ARRAY]END_DOCUMENT表示JSON數據結束

根據以上的JSON類型,我們可以將其封裝成enum類型的TokenType

 package com.json.demo.tokenizer;
/**
BEGIN_OBJECT({)
END_OBJECT(})
BEGIN_ARRAY([)
END_ARRAY(])
NULL(null)
NUMBER(數字)
STRING(字符串)
BOOLEAN(true/false)
SEP_COLON(:)
SEP_COMMA(,)
END_DOCUMENT(表示JSON文檔結束)
*/
public enum TokenType {
BEGIN_OBJECT(1),
END_OBJECT(2),
BEGIN_ARRAY(4),
END_ARRAY(8),
NULL(16),
NUMBER(32),
STRING(64),
BOOLEAN(128),
SEP_COLON(256),
SEP_COMMA(512),
END_DOCUMENT(1024);
private int code; // 每個類型的編號
TokenType(int code) {
this.code = code;
}
public int getTokenCode() {
return code;
}
}

在TokenType中我們為每一種類型都賦一個數字,目的是在Parser做一些優化操作(通過位運算來判斷是否是期望出現的類型)

在進行第一步之前JSON串對計算機來說只是一串沒有意義的字符而已。第一步的作用就是把這些無意義的字符串變成一個一個的token,上面我們已經為每一種token定義了相應的類型和值。所以計算機能夠區分不同的token,並能以token為單位解讀JSON數據。

下面我們封裝一個token類來存儲每一個token對應的值

 package com.json.demo.tokenizer;
/**
* 存儲對應類型的字面量
*/
public class Token {
private TokenType tokenType;
private String value;
public Token(TokenType tokenType, String value) {
this.tokenType = tokenType;
this.value = value;
}
public TokenType getTokenType() {
return tokenType;
}
public void setTokenType(TokenType tokenType) {
this.tokenType = tokenType;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "Token{" +
"tokenType=" + tokenType +
", value='" + value + '\'' +
'}';
}
}

在解析的過程中我們通過字符流來不斷的讀取字符,並且需要經常根據相應的字符來判斷狀態的跳轉。所以我們需要自己封裝一個ReaderChar類,以便我們更好的操作字符流。

 package com.json.demo.tokenizer;
import java.io.IOException;
import java.io.Reader;
public class ReaderChar {
private static final int BUFFER_SIZE = 1024;
private Reader reader;
private char[] buffer;
private int index; // 下標
private int size;
public ReaderChar(Reader reader) {
this.reader = reader;
buffer = new char[BUFFER_SIZE];
}
/**
* 返回 pos 下標處的字符,並返回
* @return
*/
public char peek() {
if (index - 1 >= size) {
return (char) -1;
}
return buffer[Math.max(0, index - 1)];
}
/**
* 返回 pos 下標處的字符,並將 pos + 1,最後返回字符
* @return
* @throws IOException
*/
public char next() throws IOException {
if (!hasMore()) {
return (char) -1;
}
return buffer[index++];
}
/**
* 下標回退
*/
public void back() {
index = Math.max(0, --index);
}
/**
* 判斷流是否結束
*/
public boolean hasMore() throws IOException {
if (index < size) {
return true;
}
fillBuffer();
return index < size;
}
/**
* 填充buffer數組
* @throws IOException
*/
void fillBuffer() throws IOException {
int n = reader.read(buffer);
if (n == -1) {
return;
}
index = 0;
size = n;
}
}

另外我們還需要一個TokenList來存儲解析出來的token流

 package com.json.demo.tokenizer;
import java.util.ArrayList;
import java.util.List;
/**
* 存儲詞法解析所得的token流
*/
public class TokenList {
private List<Token> tokens = new ArrayList<Token>();
private int index = 0;
public void add(Token token) {
tokens.add(token);
}
public Token peek() {
return index < tokens.size() ? tokens.get(index) : null;
}
public Token peekPrevious() {
return index - 1 < 0 ? null : tokens.get(index - 2);
}
public Token next() {
return tokens.get(index++);
}
public boolean hasMore() {
return index < tokens.size();
}
@Override
public String toString() {
return "TokenList{" +
"tokens=" + tokens +
'}';
}
}

JSON解析比其他文本解析要簡單的地方在於,我們只需要根據下一個字符就可知道接下來它所期望讀取的到的內容是什麼樣的。如果滿足期望了,則返回 Token,否則返回錯誤。

為了方便程序出錯時更好的debug,程序中自定義了兩個exception類來處理錯誤信息。(具體實現參考exception包)

下面就是第一步中的重頭戲(核心代碼):

 public TokenList getTokenStream(ReaderChar readerChar) throws IOException {
this.readerChar = readerChar;
tokenList = new TokenList();
// 詞法解析,獲取token流
tokenizer();
return tokenList;
}
/**
* 將JSON文件解析成token流
* @throws IOException
*/
private void tokenizer() throws IOException {
Token token;
do {
token = start();
tokenList.add(token);
} while (token.getTokenType() != TokenType.END_DOCUMENT);
}
/**
* 解析過程的具體實現方法
* @return
* @throws IOException
* @throws JsonParseException
*/
private Token start() throws IOException, JsonParseException {
char ch;
while (true){ //先讀一個字符,若為空白符(ASCII碼在[0, 20H]上)則接著讀,直到剛讀的字符非空白符
if (!readerChar.hasMore()) {
return new Token(TokenType.END_DOCUMENT, null);
}
ch = readerChar.next();
if (!isWhiteSpace(ch)) {
break;
}
}
switch (ch) {
case '{':
return new Token(TokenType.BEGIN_OBJECT, String.valueOf(ch));
case '}':
return new Token(TokenType.END_OBJECT, String.valueOf(ch));
case '[':
return new Token(TokenType.BEGIN_ARRAY, String.valueOf(ch));
case ']':
return new Token(TokenType.END_ARRAY, String.valueOf(ch));
case ',':
return new Token(TokenType.SEP_COMMA, String.valueOf(ch));
case ':':
return new Token(TokenType.SEP_COLON, String.valueOf(ch));
case 'n':
return readNull();
case 't':
case 'f':
return readBoolean();
case '"':
return readString();
case '-':
return readNumber();
}
if (isDigit(ch)) {
return readNumber();
}
throw new JsonParseException("Illegal character");
}

在start方法中,我們將每個處理方法都封裝成了單獨的函數。主要思想就是通過一個死循環不停的讀取字符,然後再根據字符的期待值,執行不同的處理函數。

下面我們詳解分析幾個處理函數:

 private Token readString() throws IOException {
StringBuilder sb = new StringBuilder();
while(true) {
char ch = readerChar.next();
if (ch == '\\') { // 處理轉義字符
if (!isEscape()) {
throw new JsonParseException("Invalid escape character");
}
sb.append('\\');
ch = readerChar.peek();
sb.append(ch);
if (ch == 'u') { // 處理 Unicode 編碼,形如 \中。且只支持 \

相關推薦

推薦中...