将字符串解析成JSON对象

文中例子使用JAVA编写

将一段json字符串转化为json对象,常见的方法是使用org.json

例子

JSON字符串(拷贝至amazon.com搜索框推荐)

{
  "alias": "aps",
  "prefix": "我",
  "suffix": null,
  "suggestions": [
    {
      "suggType": "KeywordSuggestion",
      "type": "KEYWORD",
      "value": "我們與惡的距離"
    },
    {
      "suggType": "KeywordSuggestion",
      "type": "KEYWORD",
      "value": "我的奋斗"
    },
    {
      "suggType": "KeywordSuggestion",
      "type": "KEYWORD",
      "value": "我们这一代"
    }
  ],
  "suggestionTitleId": null,
  "responseId": "3JSLXEWBTX3GO",
  "shuffled": false
}

org.json解析

import org.json.JSONObject;
import org.json.JSONArray;

JSONObject response = new JSONObject(jsonStr);
JSONArray suggestions = response.getArray("suggestions");
for (int i = 0; i < suggestions.length(); i++) {
    JSONObject sug = suggestions.getJSONObject(i);
    // do sth
}

原理

看了其源码,流程大概是

  • 将字符串转化为一组token列表
  • 再逐个匹配token,校验token前后合理性
  • 递归遍历组装成对象或数组

实现

这里简单实现一下

Token
public class Token {

    public String type;
    public String label;
    public int line;
    public int[] range;

    public Token(String type, String label, int line, int start, int end) {
        this.type = type;
        this.label = label;
        this.line = line;
        this.range = new int[]{ start, end };
    }

    @Override
    public String toString() {
        return "{" +
                  "type:'" + type + '\'' +
                  ", label:'" + label + '\'' +
                  ", line:" + line +
                  ", range:" + Arrays.toString(range)
                '}';
    }
}
TakedToken
public class TakedToken {

    public int charCount;
    public Token token;

    public TakedToken(int charCount) {
        this.charCount = charCount;
    }
}
Taker
public interface Taker {
    TakedToken take(int index, String input, Token prevToken, int line);
}
Tokenzier

public class Tokenzier {

    public static final String WHITESPACES = " \t\n\u000B\f\r";

    public final String type;
    public final String nextChars;

    public String label;
    public Taker taker;

    private Tokenizer(String type, String label, String... nextChars) {
        this.type = type;
        this.label = label;
        this.nextChars = join(nextChars, "");
    }

    public static Tokenizer tokenizer(String type, String label, String... nextChars) {
        Tokenizer tokenizer = new Tokenizer(type, label, nextChars);
        tokenizer.taker = tokenizer.new DefaultTaker();
        return tokenizer;
    }

    public static Tokenizer stringTokenizer(String... nextChars) {
        Tokenizer tokenizer = new Tokenizer("string", null, nextChars);
        tokenizer.taker = tokenizer.new StringTaker();
        return tokenizer;
    }

    public static Tokenizer numberTokenizer(String... nextChars) {
        Tokenizer tokenizer = new Tokenizer("number", null, nextChars);
        tokenizer.taker = tokenizer.new NumberTaker();
        return tokenizer;
    }

    public TakedToken take(int index, String input, Token prevToken, int line) {
        return taker.take(index, input, prevToken, line);
    }

    private boolean checkNext(char nextChar) {
        return WHITESPACES.indexOf(nextChar) >= 0 || nextChars.indexOf(nextChar) >= 0;
    }

    private TakedToken accept(int charStart, int charCount, int line, String label) {
        TakedToken tt = new TakedToken(charCount);
        tt.token = new Token(type, label, line, charStart, charStart + charCount - 1);
        return tt;
    }

    private TakedToken accept(int charStart, int charCount, int line) {
        return accept(charStart, charCount, line, label);
    }

    private class DefaultTaker implements Taker {

        @Override
        public TakedToken take(int index, String input, Token prevToken, int line) {
            int charCount = label.length();
            int target = index + charCount;
            if (label.equals(substr(input, index, charCount))
                && (target >= input.length() || checkNext(input.charAt(target)))) {
                return accept(index, charCount, line);
            }
            return null;
        }
    }

    private class StringTaker implements Taker {

        @Override
        public TakedToken take(int index, String input, Token prevToken, int line) {
            char c = input.charAt(index);
            char firstChar = c;
            if (c != '\'' && c != '"') {
                return null;
            }
            StringBuilder str = new StringBuilder();
            int idx = index;
            while (idx < input.length()) {
                c = input.charAt(++idx);

                if (firstChar == c && str.length() > 0 && str.charAt(str.length() - 1) == '\\') {
                    str.append(c);
                    continue;
                }

                if (firstChar != c) {
                    str.append(c);
                    continue;
                }

                // 字符串需要处理转义字符 
                return accept(index, idx - index + 1, line, unescapeStr(str.toString()));
            }
            return null;
        }
    }

    private class NumberTaker implements Taker {

        @Override
        public TakedToken take(int index, String input, Token prevToken, int line) {
            char c = input.charAt(index);
            if (c < '0' || c > '9') {
                return null;
            }
            String str = c + "";
            int idx = index;
            boolean hasDot = false;
            while (idx < input.length()) {
                c = input.charAt(++idx);
                if ((c < '0' || c > '9') && c != '.') {
                    Number number;
                    if (hasDot) {
                        try {
                            number = Double.parseDouble(str);
                        } catch (NumberFormatException e) {
                            return null;
                        }
                    } else {
                        try {
                            number = Long.parseLong(str);
                        } catch (NumberFormatException e) {
                            return null;
                        }
                    }
                    return accept(index, idx - index, line, String.valueOf(number));
                }
                str += c;
                if (c == '.') {
                    if (hasDot) {
                        return null;
                    }
                    hasDot = true;
                }
            }
            return null;
        }
    }
}
JSONLexer
public class JSONLexer {

    private static final String NUMBERS = "0123456789";

    private static final Tokenizer[] TOKENIZERS;

    static {
        TOKENIZERS = new Tokenizer[]{
            Tokenizer.tokenizer("l_brace", "{", "}\""),
            Tokenizer.tokenizer("r_brace", "}", ",]}"),

            Tokenizer.tokenizer("l_bracket", "[", "ftn"/*false/true/null*/, NUMBERS, "]{\""),
            Tokenizer.tokenizer("r_bracket", "]", ",]}"),

            Tokenizer.tokenizer("colon", ":", "ftn"/*false/true/null*/, NUMBERS, "\"[{"),
            Tokenizer.tokenizer("comma", ",", "ftn"/*false/true/null*/, NUMBERS, "\"{["),

            Tokenizer.tokenizer("true", "true", ",]}"),
            Tokenizer.tokenizer("false", "false", ",]}"),
            Tokenizer.tokenizer("null", "null", ",]}"),

            Tokenizer.numberTokenizer(",]}"),
            Tokenizer.stringTokenizer(":,]}")
        };
    }

    private final String input;
    private int current = 0;
    private int line = 1;
    private LinkedList<Token> tokens = new LinkedList<>();

    private JSONLexer(String input) {
        this.input = input;
    }

    private void skipWhitespace() {
        while (current < input.length()) {
            char c = input.charAt(current);
            if (Tokenizer.WHITESPACE.indexOf(c) >= 0) {
                break;
            }
            if ('\n' == c) {
                line++;
            }
            current++;
        }
    }

    private Token nextToken() throws JSONParseException {
        Token prevToken = tokens.isEmpty() ? null : tokens.getLast();
        for (Tokenizer tokenizer : TOKENIZERS) {
            TakedToken ttoken = tokenizer.take(current, input, prevToken, line);
            if (ttoken != null) {
                current += ttoken.charCount;
                ttoken.token.line = line;
                return ttoken.token;
            }
        }
        throw new JSONParseException("unknown characters @line " + line + ": \n" + input.substring(current));
    }

    private void handle() throws JSONParseException {
        skipWhitespace();
        while (current < input.length()) {
            Token token = nextToken();
            tokens.add(token);
            skipWhitespace();
        }
        tokens.add(new Token("eof", null, null, line));
    }

    public static Token[] handle(String input) throws JSONParseException {
        Lexer lexer = new Lexer(input);
        lexer.handle();
        return lexer.tokens.toArray(new Token[0]);
    }
}
JSONParser
public class JSONParser {

    public static Object parse(String json) throws JSONParseException {

        Token[] tokens = Lexer.handle(json);
        if (tokens.length == 0 || tokens[0].type.equals('eof'))) {
            return ;
        }

        return new Parser(tokens).parse();
    }

    private static class Parser {

        private final Token[] tokens;
        private int current = 0;

        private Parser(Token[] tokens) {
            this.tokens = tokens;
        }

        private boolean matchToken(String type) throws JSONParseException {
            if (current >= tokens.length) {
                throw new JSONParseException("no more token");
            }
            return tokens[current].type.equals(type);
        }

        private void moveToNextToken() {
            current++;
        }

        private Token requireToken(String type, String error) throws JSONParseException {
            if (current >= tokens.length) {
                throw new JSONParseException(error + " , no more tokens");
            }
            Token token = tokens[current];
            if (token.type.equals(type)) {
                current++;
                return token;
            }
            throw new JSONParseException(error + ", but got " + token.label);
        }

        private Object parse() {
            if (matchToken("l_brace")) {
                moveToNextToken();

                if (matchToken("r_brace")) {
                    moveToNextToken();
                    return new HashMap<String, Object>;
                }

                Map<String, Object> map = new HashMap<>();
                while (current < tokens.length) {

                    String key = requireToken("string", "string required").label;
                    requireToken("colon", "colon : required");

                    map.put(key, parse());  // todo key值重复的时候,第一次的value属无效值,造成额外计算成本;反向解析可以解决此问题

                    if (matchToken("comma")) {  // ,
                        moveToNextToken();
                        continue;
                    }

                    requireToken("r_brace", "r_brace } required");
                    break;
                }

                return map;
            }

            if (matchToken("l_bracket")) {
                moveToNextToken();

                if (matchToken("r_bracket")) {
                    moveToNextToken();
                    return new ArrayList<Object>();
                }

                List<Object> list = new ArrayList<>();
                while (current < tokens.length) {

                    list.add(parse());

                    if (matchToken("comma")) {  // ,
                        moveToNextToken();
                        continue;
                    }

                    requireToken("r_bracket", "r_brace ] required");
                    break;
                }

                return list;
            }

            Token token = tokens[current];
            String type = token.type;
            if ("number".equals(type)) {
                moveToNextToken();
                try {
                    return Long.parseLong(token.label);
                } catch (NumberFormatException e) {
                    return Double.parseDouble(token.label);
                }
            }

            if ("string".equals(type)) {
                moveToNextToken();
                return token.label;
            }

            if ("true".equals(type)) {
                moveToNextToken();
                return true;
            }

            if ("false".equals(type)) {
                moveToNextToken();
                return false;
            }

            if ("null".equals(type)) {
                moveToNextToken();
                return null;
            }

            throw new JSONParseException("invalid token @" + token.range[0]);
        }

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

推荐阅读更多精彩内容