歡迎光臨
每天分享高質量文章

擼一個 JSON 解析器

  • 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做類比):

json java
string Java中的String
number Java中的Long或Double
true/false Java中的Boolean
null Java中的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 含義
NULL null
NUMBER 數字
STRING 字串
BOOLEAN true/false
SEP_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             return true;
        }

        fillBuffer();
        return index     }

    /**
     * 填充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 tokens = new ArrayList();
    private int index = 0;

    public void add(Token token{
        tokens.add(token);
    }

    public Token peek() {
        return index 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     }

    @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 編碼,形如 \u4e2d。且只支援 \u0000 ~ \uFFFF 範圍內的編碼
                    for (int i = 0; i 4; i++) {
                        ch = readerChar.next();
                        if (isHex(ch)) {
                            sb.append(ch);
                        } else {
                            throw new JsonParseException("Invalid character");
                        }
                    }
                }
            } else if (ch == '"') {     // 碰到另一個雙引號,則認為字串解析結束,傳回 Token
                return new Token(TokenType.STRING, sb.toString());
            } else if (ch == '\r' || ch == '\n') {     // 傳入的 JSON 字串不允許換行
                throw new JsonParseException("Invalid character");
            } else {
                sb.append(ch);
            }
        }
    }

該方法也是透過一個死迴圈來讀取字元,首先判斷的是JSON中的跳脫字元。

JSON中允許出現的有以下幾種

   \"
\\
\b
\f
\n
\r
\t
\u four-hex-digits
\/

具體的處理方法封裝在了isEscape()方法中,處理Unicode 編碼時要特別註意一下u的後面會出現四位十六進位制數。當讀取到一個雙引號或者讀取到了非法字元(’\r’或’、’\n’)迴圈退出。

判斷數字的時候也要特別小心,註意負數,frac,exp等等情況。

透過上面的解析,我們可以得到一組token,接下來我們需要以這組token作為輸入,解析出相應的JSON物件

解析出JSON物件

解析之前我們需要定義出JSON物件(JSONObject)和JSON陣列(JSONArray)的物體類。

   package com.json.demo.jsonstyle;

import com.json.demo.exception.JsonTypeException;
import com.json.demo.util.FormatUtil;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

public class JsonObject {
    private Map map = new HashMap();

    public void put(String key, Object value) {
        map.put(key, value);
    }

    public Object get(String key) {
        return map.get(key);
    }
    ...

}

package com.json.demo.jsonstyle;

import com.json.demo.exception.JsonTypeException;
import com.json.demo.util.FormatUtil;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * JSON的陣列形式
* 陣列是值(value)的有序集合。一個陣列以“[”(左中括號)開始,“]”(右中括號)結束。值之間使用“,”(逗號)分隔。
 */

public class JsonArray {
    private List list = new ArrayList();

    public void add(Object obj) {
        list.add(obj);
    }

    public Object get(int index) {
        return list.get(index);
    }

    public int size() {
        return list.size();
    }
    ...
}

之後我們就可以寫解析類了,由於程式碼較長,這裡就不展示了。有興趣的可以去GitHub上下載。實現邏輯比較簡單,也易於理解。

解析類中的parse方法首先根據第一個token的型別選擇呼叫parseJsonObject()或者parseJsonArray(),進而傳回JSON物件或者JSON陣列。上面的解析方法中利用位運算來判斷字元的期待值既提高了程式的執行效率也有助於提高程式碼的ke’du’xi

完成之後我們可以寫一個測試類來驗證下我們的解析器的執行情況。我們可以自己定義一組JSON串也可以透過HttpUtil工具類從網上獲取。最後透過FormatUtil類來規範我們輸出。

具體效果如下圖所示:

參考文章

  • http://www.cnblogs.com/absfree/p/5502705.html
  • https://www.liaoxuefeng.com/article/0014211269349633dda29ee3f29413c91fa65c372585f23000?hmsr=toutiao.io&utm;_medium=toutiao.io&utm;_source=toutiao.io
  • https://segmentfault.com/a/1190000010998941#articleHeader6
  • http://json.org/json-zh.html

已同步到看一看
贊(0)

分享創造快樂