C#实现超级简单和高效的JSON解析器

MojoUnityJson是使用C#实现的JSON解析器 ,算法思路来自于游戏引擎Mojoc的C语言实现Json.h。借助C#的类库,可以比C的实现更加的简洁和全面,尤其是处理Unicode转义字符(\u开头)的解析,C#的StringBuilder本身就支持了Unicode码点(code point)。

 

MojoUnityJson使用递归下降的解析模式,核心解析代码只有450行(去掉空行可能只有300多行),支持标准的JSON格式。算法实现力求简洁明了,用最直接最快速的方法达到目的,没有复杂的概念和模式。除了解析JSON,还提供了一组方便直观的API来访问JSON数据,整体实现只有一个文件,仅依赖System.Collections.GenericSystem.TextSystem 三个命名空间,MojoUnityJson可以很容易的嵌入到其它项目里使用。

本文主要介绍一下,超级简单又高效,并且看一眼就完全明白的解析算法,几乎可以原封不动的复制粘贴成其它语言版本的实现。

保存上下文信息

使用一个简单的结构体,用来在解析的过程中,传递一些上下文数据。

private struct Data
{
    // 需要解析的JSON字符串
    public string        json;
    // 当前JSON字符串解析的位置索引
    public int           index;
    // 缓存一个StringBuilder,用来抠出JSON的一段字符。
    public StringBuilder sb;


    public Data(string json, int index)
    {
        this.json  = json;
        this.index = index;
        this.sb    = new StringBuilder();
    }
}

抽象JSON的值

我们把JSON的值抽象成以下几个类型:

public enum JsonType
 {
     Object,
     Array,
     String,
     Number,
     Bool,
     Null,
 } 

整体解析步骤

// 解析 JsonValue
private static JsonValue ParseValue(ref Data data);

// 解析 JsonObject
private static JsonValue ParseObject(ref Data data);

// 解析 JsonArray
private static JsonValue ParseArray(ref Data data);

// 解析 string
private static JsonValue ParseString(ref Data data);

// 解析 number
private static JsonValue ParseNumber(ref Data data)

这就是全部的解析流程,在ParseValue中会根据字符判断类型,分别调用下面几个不同的解析函数。JsonValue就对应一个JSON的值,它有一个JsonType代表了这个值的类型。这是一个递归的过程,在ParseValue,ParseObject和ParseArray过程中,会递归的调用ParseValue。JSON一定是始于一个,Object或Array,当这个最顶层的值解析完毕的时候,整个JSON也就解析完成了。

解析空白字符

解析过程中,会有很多为了格式化存在的空白字符,需要剔除这些,才能获得有信息的字符,这是一个重复的过程,需要一个函数统一处理。

private static void SkipWhiteSpace(ref Data data)
{
    while (true)
    {
        switch (data.json[data.index])
        {
            case ' ' :
            case '\t':
            case '\n':
            case '\r':
                // 每次消耗一个字符,就向后推进JSON的索引
                data.index++; 
                continue;
        }

        break;
    }
}

解析JsonValue

private static JsonValue ParseValue(ref Data data)
{
    // 跳过空白字符
    SkipWhiteSpace(ref data);
    
    var c = data.json[data.index];

    switch (c)
    {
        case '{': 
            // 表示Object
            return ParseObject(ref data);

        case '[':
            // 表示Array
            return ParseArray (ref data);

        case '"':
            // 表示string
            return ParseString(ref data);

        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
        case '-':
            // 表示数值
            return ParseNumber(ref data);

        case 'f': // 表示可能是false
            if 
            (
                data.json[data.index + 1] == 'a' && 
                data.json[data.index + 2] == 'l' &&
                data.json[data.index + 3] == 's' &&
                data.json[data.index + 4] == 'e'
            )
            {
                data.index += 5;
                // 表示是false
                return new JsonValue(JsonType.Bool, false);
            }
            break;

        case 't': // 表示可能是true
            if 
            (
                data.json[data.index + 1] == 'r' && 
                data.json[data.index + 2] == 'u' &&
                data.json[data.index + 3] == 'e'
            )
            {
                data.index += 4;
                // 表示是true
                return new JsonValue(JsonType.Bool, true);
            }
            break;

        case 'n': // 表示可能是null
            if 
            (
                data.json[data.index + 1] == 'u' && 
                data.json[data.index + 2] == 'l' &&
                data.json[data.index + 3] == 'l'
            )
            {
                data.index += 4;
                // 表示可能是null
                return new JsonValue(JsonType.Null, null);
            }
            break;
    }
    
    // 不能处理了
    throw new Exception(string.Format("Json ParseValue error on char '{0}' index in '{1}' ", c, data.index));
}
  • ParseValue是解析的主入口,代表着解析JsonValue这个抽象的JSON值,其真实的类型在解析的过程中逐渐具体化。
  • 在剥离掉空白字符之后,就可以很容易的通过单个字符,就判断出其可能的数值类型,而不需要向前或向后检索更多的字符。
  • true,false,null 这几个固定的类型,直接就处理掉了,而其它稍微复杂的类型需要使用函数来处理。
  • 这里没有使用if else,而是大量使用了case,是为了提高效率,减少判断次数。

解析JsonObject

private static JsonValue ParseObject(ref Data data)
{
    // Object 对应 C#的Dictionary
    var jsonObject = new Dictionary(JsonObjectInitCapacity);

    // skip '{'
    data.index++;

    do
    {
        // 跳过空白字符
        SkipWhiteSpace(ref data);

        if (data.json[data.index] == '}')
        {
            // 空的Object, "{}"
            break;
        }

        DebugTool.Assert
        (
            data.json[data.index] == '"',  
            "Json ParseObject error, char '{0}' should be '\"' ", 
            data.json[data.index]
        );


        // skip '"'
        data.index++;

        var start = data.index;
        
        // 解析Object的key值
        while (true)
        {
            var c = data.json[data.index++];

            switch (c)
            {
                case '"':
                    // check end '"'
                    break;

                case '\\':
                    // skip escaped quotes
                    data.index++;
                    continue;

                default:
                    continue;
            }

            // already skip the end '"'
            break;
        }

        // get object key string
        // 抠出key字符串
        var key = data.json.Substring(start, data.index - start - 1);
        
        // 跳过空白
        SkipWhiteSpace(ref data);
        
        DebugTool.Assert
        (
            data.json[data.index] == ':',  
            "Json ParseObject error, after key = {0}, char '{1}' should be ':' ", 
            key,
            data.json[data.index]
        );

        // skip ':'
        data.index++;

        // set JsonObject key and value
        // 递归的调用ParseValue获得Object的value值
        jsonObject.Add(key, ParseValue(ref data));
        
        // 跳过空白
        SkipWhiteSpace(ref data);

        if (data.json[data.index] == ',')
        {
            // Object的下一对KV
            data.index++ ;                   
        }
        else
        {
            // 跳过空白
            SkipWhiteSpace(ref data);
            
            DebugTool.Assert
            (
                data.json[data.index] == '}',  
                "Json ParseObject error, after key = {0}, char '{1}' should be '{2}' ",
                key,
                data.json[data.index],
                '}'
            );

            break;
        }
    }
    while (true);

    // skip '}' and return after '}'
    data.index++;

    return new JsonValue(JsonType.Object, jsonObject);
}
  • JsonObject类型就简单的对应C#的Dictionary,value是JsonValue类型。当解析完成后,value的类型就是确定的了。
  • JsonValue是递归的调用ParseValue来处理的,其类型可能是JsonType枚举的任意类型。

解析JsonArray

private static JsonValue ParseArray(ref Data data)
{
    // JsonArray 对应  List
    var jsonArray = new List(JsonArrayInitCapacity);

    // skip '['
    data.index++;

    do
    {
        // 跳过空白
        SkipWhiteSpace(ref data);

        if (data.json[data.index] == ']')
        {
           // 空 "[]"
            break;
        }

        // add JsonArray item 
        // 递归处理List每个元素
        jsonArray.Add(ParseValue(ref data));
       
        // 跳过空白
        SkipWhiteSpace(ref data);

        if (data.json[data.index] == ',')
        {
            // 解析下一个元素
            data.index++;
        }
        else
        {
            // 跳过空白
            SkipWhiteSpace(ref data);
            DebugTool.Assert
            (
                data.json[data.index] == ']',  
                "Json ParseArray error, char '{0}' should be ']' ", 
                data.json[data.index]
            );
            break;
        }
    }
    while (true);

    // skip ']'
    data.index++;

    return new JsonValue(JsonType.Array, jsonArray);
}
  • JsonArray类型就简单的对应C#的List,element是JsonValue类型。当解析完成后,element的类型就是确定的了。
  • JsonValue是递归的调用ParseValue来处理的,其类型可能是JsonType枚举的任意类型。

解析string

private static JsonValue ParseString(ref Data data)
{
    // skip '"'
    data.index++;

    var    start = data.index;
    string str;

    // 处理字符串
    while (true)
    {
        switch (data.json[data.index++])
        {
            case '"': // 字符串结束
                // check end '"'                        

                if (data.sb.Length == 0)
                {
                    // 没有使用StringBuilder,直接抠出字符串
                    str = data.json.Substring(start, data.index - start - 1);
                }
                else
                {
                    // 有特殊字符在StringBuilder
                    str = data.sb.Append(data.json, start, data.index - start - 1).ToString();
                    
                    // clear for next string
                    // 清空字符,供下次使用
                    data.sb.Length = 0;
                }
                break;

            case '\\':
                {
                    // check escaped char
      
                    var  escapedIndex = data.index;
                    char c;
                    
                    // 处理各种转义字符
                    switch (data.json[data.index++])
                    {
                        case '"':
                            c = '"';
                            break;

                        case '\'':
                            c = '\'';
                            break;

                        case '\\':
                            c = '\\';
                            break;

                        case '/':
                            c = '/';
                            break;

                        case 'n':
                            c = '\n';
                            break;

                        case 'r':
                            c = '\r';
                            break;

                        case 't':
                            c = '\t';
                            break;

                        case 'u':
                           // 计算unicode字符的码点
                            c = GetUnicodeCodePoint
                                (
                                    data.json[data.index], 
                                    data.json[data.index + 1], 
                                    data.json[data.index + 2], 
                                    data.json[data.index + 3]
                                );

                            // skip code point
                            data.index += 4;
                            break;

                        default:
                            // not support just add in pre string
                            continue;
                    }

                    // add pre string and escaped char
                    // 特殊处理的字符和正常的字符,一起放入StringBuilder
                    data.sb.Append(data.json, start, escapedIndex - start - 1).Append(c);

                    // update pre string start index
                    start = data.index;
                    continue;
                }

            default:
                continue;
        }

        // already skip the end '"'
        break;
    }

    return new JsonValue(JsonType.String, str);
}

处理字符串麻烦的地方在于,转义字符需要特殊处理,否则转义字符就会原样显示,而不能呈现特殊的作用。好在StringBuilder功能非常强大,提供了处理各种类型的接口。

解析Unicode字符

在JSON中,Unicode字符可以是\u开头跟随4个字符组成的转义字符。这4个字符代表的,其实是一个16进制的数字,就是码点(code point)。码点在StringBuilder的Append重载函数中是直接支持的。所以,我们只要把\u后面的4个字符,转换成码点传递给Append就可以了。

/// 
/// Get the unicode code point.
/// 
private static char GetUnicodeCodePoint(char c1, char c2, char c3, char c4)
{
    // 转义字符\u后面的4个char是一个16进制的数字。
    // 4个char转换为int后,映射到16进制数字的高位到低位,然后相加得到码点。
    // 注意这里需要return char类型,才能被Append正确处理。
    return (char)
           (
               UnicodeCharToInt(c1) * 0x1000 +
               UnicodeCharToInt(c2) * 0x100  +
               UnicodeCharToInt(c3) * 0x10   +
               UnicodeCharToInt(c4)
           );
}


/// 
/// Single unicode char convert to int.
/// 
private static int UnicodeCharToInt(char c)
{
    // 使用switch case 减少 if else 的判断
    switch (c)
    {
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
            return c - '0';

        case 'a':
        case 'b':
        case 'c':
        case 'd':
        case 'e':
        case 'f':
            return c - 'a' + 10;

        case 'A':
        case 'B':
        case 'C':
        case 'D':
        case 'E':
        case 'F':
            return c - 'A' + 10;
    }

    throw new Exception(string.Format("Json Unicode char '{0}' error", c));
}

解析number

private static JsonValue ParseNumber(ref Data data)
{
    var start = data.index;

    // 收集数值字符
    while (true)
    {
        switch (data.json[++data.index])
        {
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
            case '-':
            case '+':
            case '.':
            case 'e':
            case 'E':
                continue;
        }

        break;
    }
    
    // 抠出数值字符串
    var   strNum = data.json.Substring(start, data.index - start);
    float num;
    
    // 当成float处理,当然也可以用double
    if (float.TryParse(strNum, out num))
    {
        return new JsonValue(JsonType.Number, num);
    }
    else
    {
        throw new Exception(string.Format("Json ParseNumber error, can not parse string [{0}]", strNum));
    }
}

如何使用

只有一句话,把Json字符串解析成JsonValue对象,然后JsonValue对象包含了所有的数值。

var jsonValue = MojoUnity.Json.Parse(jsonString);

JsonValue的访问API

// JsonValue 当做 string
public string AsString();

// JsonValue 当做 float
public float AsFloat();

// JsonValue 当做 int
public int AsInt();

// JsonValue 当做 bool
public bool AsBool();

// JsonValue 当做 null
public bool IsNull();

// JsonValue 当做 Dictionary
public Dictionary AsObject();

// JsonValue 当做 Dictionary 并根据 key 获取 value 当做JsonValue 
public JsonValue AsObjectGet(string key);

// JsonValue 当做 Dictionary 并根据 key 获取 value 当做 Dictionary
public Dictionary AsObjectGetObject(string key);

// JsonValue 当做 Dictionary 并根据 key 获取 value 当做 List
public List AsObjectGetArray(string key);

// JsonValue 当做 Dictionary 并根据 key 获取 value 当做 string
public string AsObjectGetString(string key);

// JsonValue 当做 Dictionary 并根据 key 获取 value 当做 float
public float AsObjectGetFloat(string key);

// JsonValue 当做 Dictionary 并根据 key 获取 value 当做 int
public int AsObjectGetInt(string key);

// JsonValue 当做 Dictionary 并根据 key 获取 value 当做 bool
public bool AsObjectGetBool(string key);

// JsonValue 当做 Dictionary 并根据 key 获取 value 当做 null 
public bool AsObjectGetIsNull(string key);

// JsonValue 当做 List
public List AsArray();

// JsonValue 当做 List 并获取 index 的 value 当做 JsonValue
public JsonValue AsArrayGet(int index);

// JsonValue 当做 List 并获取 index 的 value 当做 Dictionary
public Dictionary AsArrayGetObject(int index);

// JsonValue 当做 List 并获取 index 的 value 当做 List
public List AsArrayGetArray(int index);

// JsonValue 当做 List 并获取 index 的 value 当做 string
public string AsArrayGetString(int index);

// JsonValue 当做 List 并获取 index 的 value 当做 float
public float AsArrayGetFloat(int index);

// JsonValue 当做 List 并获取 index 的 value 当做 int
public int AsArrayGetInt(int index);

// JsonValue 当做 List 并获取 index 的 value 当做 bool
public bool AsArrayGetBool(int index);

// JsonValue 当做 List 并获取 index 的 value 当做 null
public bool AsArrayGetIsNull(int index);

最后

MojoUnityJson目的就是完成简单而单一的JSON字符串解析功能,能够读取JSON的数据就是最重要的功能。在网上也了解了一些开源的C#实现的JSON库,不是功能太多太丰富,就是实现有些繁琐了,于是就手动实现了MojoUnityJson。

欢迎大家关注公众号“创小董”我会继续分享更多经历、经验、解决办法。

你可能感兴趣的:(程序员)