从零开始写一个JSON解析器

JSON的语法简单,很适合上完编译原理课程之后或者学习完龙书语法分析之后的练手。

**JSON**

JSON格式

{"a" : 123 }
[1,2,3]
{"a": [1,2,3] }
{"a" : {"b" : 123 }}

基础为以上几种。有了基本的格式,可以考虑先写词法分析部分。JSON格式类似于key/value存储,其中key为string类型,value复杂一些,可包括,string,int,double,jsonobject,jsonarray,true,false,null标记。所以对每个token都要逐一分析,构造出DFA。

  • 字符串
    字符串以 " 开头,以 " 结束。其中需要考虑的是转义字符的忽略以及unicode的格式(\u hex hex hex hex)
 private Token readString() throws IOException, JsonLexException {
        StringBuilder builder = new StringBuilder();
        while ( (this.at_ = read()) != '\"') {
            if (this.at_ == '\r' || this.at_ == '\n') error("string", this.at_);
            if (this.at_ == '\\') {
                //check escape char
                this.at_ = read();
                if (isEscapeChar(this.at_)) {
                    builder.append('\\');
                    builder.append((char) this.at_);
                } else if (this.at_ == 'u'){
                    //unicode
                    builder.append('\\');
                    builder.append('u');
                    builder.append(unicode());

                } else {
                    error("string", this.at_);
                }
            } else {
                builder.append((char) this.at_);
            }
        }
        if ( this.at_ != '\"') {
            //string is not closed
            error("string[not closed]", this.at_);
        }
        return this.token_ = new Token(JsonToken.STRING, new Value(builder.toString()));
    }

    private String unicode() throws IOException, JsonLexException {
        StringBuilder builder = new StringBuilder();
        int i=0;
        for (;i<4 && isHexChar(this.at_ = read()); builder.append((char) this.at_),++i);
        if (i < 4) error("unicode", this.at_);
        return builder.toString();
    }
  • 数字
    数字包含有符号int double以及科学计数法表示稍微麻烦,但是画出DFA分析就会明了很多。
DFA

据此就可很快写出来。

private Token readNumber(int at) throws IOException,JsonLexException {

        StringBuilder builder = new StringBuilder();
        int status = 0;
        while (true) {
            switch (status) {
                case 0:
                    this.at_ = at;
                    if (this.at_ == '+' || this.at_ == '-') status = 1;
                    else if (Character.isDigit(this.at_)) status = 2;
                    else error("number", this.at_);
                    builder.append((char) this.at_);
                    break;
                case 1:
                    this.at_ = read();
                    if (Character.isDigit(this.at_)) status = 2;
                    else error("number", this.at_);
                    builder.append((char) this.at_);
                    break;
                case 2:
                    this.at_ = read();
                    if (this.at_ == '.') status = 3;
                    else if (Character.isDigit(this.at_)) status = 2;
                    else if (this.at_ == 'E' || this.at_ == 'e') status = 5;
                    else status = 8;
                    if (status != 8) {
                        builder.append((char) this.at_);
                    }
                    break;
                case 3:
                    this.at_ = read();
                    if (Character.isDigit(this.at_)) status = 4;
                    else error("number", this.at_);
                    builder.append((char) this.at_);
                    break;
                case 4:
                    this.at_ = read();
                    if (Character.isDigit(this.at_)) status = 4;
                    else if (this.at_ == 'E' || this.at_ == 'e') status = 5;
                    else status = 9;
                    if (status != 9) {
                        builder.append((char) this.at_);
                    }
                    break;
                case 5:
                    this.at_ = read();
                    if (this.at_ == '+' || this.at_ == '-') status = 6;
                    else if (Character.isDigit(this.at_)) status = 7;
                    else error("number", this.at_);
                    builder.append((char) this.at_);
                    break;
                case 6:
                    this.at_ = read();
                    if (Character.isDigit(this.at_)) status = 7;
                    else error("number", this.at_);
                    builder.append((char) this.at_);
                    break;
                case 7:
                    this.at_ = read();
                    if (Character.isDigit(this.at_)) status = 7;
                    else status = 10;
                    if (status != 10) {
                        builder.append((char) this.at_);
                    }
                    break;
                case 8: // int
                    unread(this.at_); // not a digit
                    return this.token_ = new Token(JsonToken.INT, new Value(Integer.valueOf(builder.toString())));
                case 9: //double without 'E' or 'e'
                    unread(this.at_); // not a digit
                    return this.token_ = new Token(JsonToken.DOUBLE, new Value(Double.valueOf(builder.toString())));
                case 10://double with 'E' or 'e'
                    unread(this.at_);// not a digit
                    return this.token_ = new Token(JsonToken.DOUBLE,new Value(Double.valueOf(builder.toString())));
                default:
                    error("number", this.at_);
            }
        }
    }
  • 其他字符
    对于其他终结符,true false null则只要匹配首字符来判断就可以了。

综上就可写出来lexer了

public Token scan() throws IOException, JsonLexException {
        //this.at_ = '\0';
        while (isWhiteBlack( this.at_ = read() ));
        switch ((int)this.at_) {
            case '{':
                return this.token_ = new Token(JsonToken.BEGIN_OBJ, new Value("{"));
            case '}':
                return this.token_ = new Token(JsonToken.END_OBJ, new Value("}"));
            case '[':
                return this.token_ = new Token(JsonToken.BEGIN_ARR, new Value("["));
            case ']':
                return this.token_ = new Token(JsonToken.END_ARR, new Value("]"));
            case ':':
                return this.token_ = new Token(JsonToken.COLON, new Value(":"));
            case ',':
                return this.token_ = new Token(JsonToken.COMMA, new Value(","));
            case '1': case '2': case '3': case '4': case '5':
            case '6': case '7': case '8': case '9': case '0':
            case '-': case '+': case '.':
                return this.token_ = readNumber(this.at_);
            case '\"':
                return this.token_ = readString();
            case 'n':
                return this.token_ = readNull();
            case 't':
                return this.token_ = readTrue();
            case 'f':
                return this.token_ = readFalse();
            case -1:
                return this.token_ = new Token(JsonToken.EOF, new Value("eof"));
            default:
                this.token_ = null;
                error("scan->default",this.at_);
                return null;
        }
    }

词法分析就完成了。

语法分析

语法分析有LL, LR等方法这里采取LL(1)分析。

首先要构造出JSON文法。根据JSON基础格式。

obj -> { members }
members -> pair members' | eps
members' -> , pair members' | eps
pair -> string : value
array -> [ elem ]
elem -> value elem' | eps
elem' -> , value elem' | eps
value -> obj | array | number | string | true | false | null

求出FIRST集和FOLLOW集,验证可知为LL(1)文法。这样就可以用递归下降来做语法分析。
之后写出SDT就可以根据SDT来编写代码了。


    public Json parse() throws IOException, JsonParseException, JsonLexException {
        Token token = lexer_.scan();
        if (token.getToken() == JsonToken.BEGIN_OBJ) {
            return object();
        } else if (token.getToken() == JsonToken.BEGIN_ARR) {
            return array();
        } else {
            error("parse", token.toString());
            return null;
        }
    }

    //文法 { member }
    public JsonObject object() throws IOException,
            JsonParseException, JsonLexException {
        Map<String, Value> map = new HashMap<>();
        return new JsonObject(member(map));
    }

    //对应文法 pair mem' | eps
    public Map<String ,Value> member(Map<String, Value> map) throws IOException,
            JsonLexException, JsonParseException {
        Token token = lexer_.scan();
        if (token.getToken() == JsonToken.END_OBJ) { //eps
            return map;
        }
        return member_1( pair(map) );
    }

    //对应文法 , pair mem' | eps
    public Map<String, Value> member_1(Map<String, Value> map) throws IOException, JsonLexException, JsonParseException {
        Token token = lexer_.scan();
        if (token.getToken() == JsonToken.COMMA) { //,
            lexer_.scan();
            return member_1( pair(map) );
        } else if (token.getToken() == JsonToken.END_OBJ) { //eps
            return map;
        } else {
            error("member_1", token.toString());
            return null;
        }
    }

    //文法 string : value
    private Map<String, Value> pair(Map<String, Value> map) throws IOException, JsonLexException, JsonParseException {
        Token token = lexer_.peek();
        if (token.getToken() == JsonToken.STRING) {
            String key = (String) token.getValue().get();
            token = lexer_.scan();
            if (token.getToken() == JsonToken.COLON) {
                lexer_.scan();
                map.put(key, value());
                return map;
            } else {
                error("pair", token.toString());
                return null;
            }
        } else {
            error("pair", token.toString());
            return null;
        }
    }

    //文法 [ elem ]
    public JsonArray array() throws IOException,JsonParseException, JsonLexException  {
        List<Value> list = new LinkedList<>();
        return new JsonArray(elem(list));
    }
    //文法 value elem' | eps
    public List<Value> elem(List<Value> list) throws IOException,
            JsonLexException,JsonParseException  {
        Token token = lexer_.scan();
        if (token.getToken() == JsonToken.END_ARR) { // eps
            return list;
        } else {
            list.add(value());
            return elem_1(list);
        }
    }

    // 文法 , value elem' | eps
    public List<Value> elem_1(List<Value> list) throws IOException,
            JsonLexException, JsonParseException  {
        Token token = lexer_.scan();
        if (token.getToken() == JsonToken.COMMA) { // ,
            lexer_.scan();
            list.add(value());
            return elem_1(list);
        } else if (token.getToken() == JsonToken.END_ARR) { // eps
            return list;
        } else {
            error("elem_1",token.toString());
            return null;
        }
    }

    public Value value() throws IOException, JsonLexException,
            JsonParseException {
        Token token = lexer_.peek();
        if (isCommonValue(token.getToken())) {
            return new Value(token.getValue());
        } else if (token.getToken() == JsonToken.BEGIN_OBJ) {
            return new Value(object());
        } else if (token.getToken() == JsonToken.BEGIN_ARR) {
            return new Value(array());
        } else {
            error("value",token.toString());
            return null;
        }
    }

    private boolean isCommonValue(JsonToken token) {
        return (token == JsonToken.STRING || token == JsonToken.FALSE ||
                token == JsonToken.INT || token == JsonToken.DOUBLE ||
                token == JsonToken.TRUE || token == JsonToken.NULL) ? true : false;
    }


    public void error(String position, String value) throws JsonParseException {
        throw new JsonParseException(String.format("an error occurred on Parser [%s %s]",position,value));
    }

至此一个简单的解析器已经写完了,只是实现了简单的解析功能,一个完善的JAVA JSON解析器至少可以对对象序列化和反序列化。后续将会添加上这部分功能,再更新一遍 :-|

源代码:ToyJSON

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,495评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,493评论 18 399
  • Linux 系统管理基础本教材作为牛耳学院Linux 基础配套教材。共180课时。版权所有◎2016 牛耳学院 L...
    StarShift阅读 582评论 0 3
  • 已经好久没有联系你了。很早之前我反复地对自己说不要再想你,于是反复之间我真的不再想起你,为此花了5年的时间。可是那...
    一zhi鱼阅读 325评论 6 0
  • 时间全被被你带走 在你走了之后 回忆在无声的游走 在眼底沉入了双眸 在每次下雨的时候 又在萧瑟冷冷的深秋 在四季跌...
    单边温暖阅读 283评论 0 0