自己动手写json解析器0x01-分词

背景

作为一个程序员,心里一直有一个手撸编译器的梦,奈何技术不够一直没能付诸实践,JSON虽然不是一门语言,但很适合用来作为编译器的练手,原因在于

  • 关键字较少,结构简单
  • 语法简单,没有判断,循环等高级语言语法
  • 文本格式,测试比较方便

虽然写代码硬解析也能做到,但总归不科学,对于复杂的语法,硬解析根本无法解决。从阮一峰的博客了解到the-super-tiny-compiler这个项目,该项目是一个迷你编译器,将lisp表达式转化为c语言表达时,代码去掉注释不到200行,很适合用来学习,这个项目给了我很多启发,开始对写一个json解析器有了一点思路,该系列博文将记录一个完成json解析器的实现过程,当然我自己也是小白,不是什么编译器专家,只是希望给同样是小白的你一点参考,大神可以绕道。

说明

如何把一个json字符串解析成一个java对象,大概要分为一下步骤

  • 分词(tokenizer)将json字符串分解成一个个独立的单元,比如下面这个简单的json字符串
{
  "name": "asan",
  "age": 32
}

经过分词后会分解成下面这种格式

[
{"type":"object","value":"{","valueType":"object"},{"type":"key","value":"name","valueType":"string"},{"type":"value","value":"asan","valueType":"string"},{"type":"key","value":"age","valueType":"string"},{"type":"value","value":32,"valueType":"number"},{"type":"object","value":"}","valueType":"object"}
]

这期间会将无用字符过滤,分解为一个个token

  • 解析抽象语法树(AST):tokenizer只是将字符串分解平铺,AST负责将平铺的各个token按照语义变成一棵树,带有层级结构,比如上面的token解析抽象语法树如下
{
    "items": [
        {
            "name": "name",
            "type": "value",
            "value": "asan"
        },
        {
            "name": "age",
            "type": "value",
            "value": 32
        }
    ],
    "type": "object"
}
  • 对象生成,根据抽象语法树生成对象

无论是tokenizer还是ast,格式都不是固定,上面只是一个参考,但作用都是类似的,基本上解析器都要经过tokenizer和ast两个步骤。

分词(tokenizer)

示例json

如无特殊说明,后续程序都是基于以下这个json进行测试

{
  "name": "asan",
  "age": 32,
  "mail": null,
  "married": true,
  "birthday": "1992-02-08",
  "salary": 1234.56,
  "deposit": -342.34,
  "description": "a \"hundsome\" man",
  "tags": [
    "coder",
    "mid-age"
  ],
  "location": {
    "province": "福建省",
    "city": "泉州市",
    "area": "晋江市"
  },
  "family": [
    {
      "relation": "couple",
      "name": "Helen"
    },
    {
      "relation": "daughter",
      "name": "XiaoMan"
    }
  ]
}

该示例基本包含了json所有常用的元素,可以满足基本的测试

  • 基本数据类型:字符串,整型,浮点型,日期,null,布尔值
  • 对象(location)
  • 数组(family)
  • 基本类型数组(tags)

分词

我们首先定义一个保存分词结果的结构,该结构至少需要包含以下两个字段

  • type:token类型,包含object(对象),array(数组),key(字段名),value(字段值),kvSymbol(key-value之间的冒号:)
  • value:token值

但一个type可能不足以描述json,比如json的value有字符串,整型,浮点型等,但type都是value,你可能会说为什么不每个定义一个类型呢,如果每个定义一个类型,那么到时候判断该token是不是value类型的时候就比较麻烦,需要依次判断是不是字符串,整型,浮点型等,因此我们增加了一个字段valueType用来存储值类型

  • type:token类型
  • value:token值
  • valueType:值类型(string,bool,null,number)

我们暂时不去定义枚举,先把解析器实现再去重构代码,暂时不考虑代码的合理性。

以下是第一版本的解析器

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

/**
 * @Description:
 * @author: jianfeng.zheng
 * @since: 2022/12/20 11:56
 * @history: 1.2022/12/20 created by jianfeng.zheng
 */
public class JSONParser {

    //currentIndex保存当前字符串扫描的位置,字符串是逐字符进行扫描
    private int currentIndex = 0;

    /**
     * 对json字符串进行分词
     *
     * @param json json字符串
     * @return token列表
     */
    public List tokenizer(String json) {
        // 保存分词结果
        List tokens = new ArrayList<>();

        while (currentIndex < json.length()) {
            char c = json.charAt(currentIndex);

            //对于空白符可以直接跳过,如果有更多的空白符只需添加新的判断即可
            if (c == ' ' || c == '\r' || c == '\n' || c == ',') {
                //字符只要处理过了必须要将当前位置移动到下一个
                ++currentIndex;
                continue;
            } else if (c == '{' || c == '}') {
                //对象
                tokens.add(new Token("object", c));
            } else if (c == '[' || c == ']') {
                //数组
                tokens.add(new Token("array", c));
            } else if (c == '"') {
                //字符串
                StringBuffer value = new StringBuffer();
                char cc = json.charAt(++currentIndex);

                // 这里以"作为字符串结束符的标志
                // 当然这个不严谨因为没考虑到转义,但这个问题留着后面解决,我们暂时忽略
                while (cc != '"') {
                    value.append(cc);
                    cc = json.charAt(++currentIndex);
                }
                tokens.add(new Token("string", value.toString()));
            } else if (c == ':') {
                // key-value中间的分隔符
                tokens.add(new Token("kvSymbol", "kvSymbol", c));
            } else if (c >= '0' && c <= '9') {
                //数字
                StringBuffer value = new StringBuffer();
                value.append(c);
                char cc = json.charAt(++currentIndex);
                //这里考虑到带有小数点的浮点数
                while (cc == '.' || (cc >= '0' && cc <= '9')) {
                    value.append(cc);
                    cc = json.charAt(++currentIndex);
                }
                //数字暂时统一用浮点数进行表示
                tokens.add(new Token("value", "number", Float.parseFloat(value.toString())));
            }
            ++currentIndex;
        }
        return tokens;
    }
}

代码流程如下

  • 循环遍历json字符串
  • 检测关键字,并识别出关键字以token保存
  • 对于字符串的处理目前不论是key还是value统一保存的是string类型
  • 对于数字类型的处理目前都是以Float浮点数进行保存

测试

我们写一个程序程序进行测试

public class Main {

    public static void main(String[] args) {
        String json = "{\"name\": \"asan\", \"age\": 32}";
        JSONParser parser = new JSONParser();
        List tokens = parser.tokenizer(json);
        System.out.println(String.format("|%-12s|%-12s|%-15s|", "type", "valueType", "value"));
        System.out.println("-------------------------------------------");
        for (Token t : tokens) {
            System.out.println(String.format("|%-12s|%-12s|%-15s|",
                    t.getType(),
                    t.getValueType(),
                    t.getValue()));
        }
        System.out.println("-------------------------------------------");
    }
}

我们先拿一个比较简单的json{"name": "asan", "age": 32}进行测试,测试结果如下

|type        |valueType   |value          |
-------------------------------------------
|object      |object      |{              |
|string      |string      |name           |
|kvSymbol    |kvSymbol    |:              |
|string      |string      |asan           |
|string      |string      |age            |
|kvSymbol    |kvSymbol    |:              |
|value       |number      |32.0           |
-------------------------------------------

目前这个结果符合我们的预期。

优化

其他基本类型

目前程序value类型程序只处理了字符串和数字,bool和null类型未处理,由于程序是一个字符一个字符对字符串进行扫描,但要判断bool和null必须往后进行扫描。

  • 判断null
if ((c == 'n') &&
    json.startsWith("null", currentIndex)) {
  tokens.add(new Token("value", "null", null));
  //如果读取到null值需要将当前指针往前移动3个字符(null占4个字符,除去已经读取到的1个字符串还需要移动3个字符)
  currentIndex += 3;
}
  • 判断bool值

bool值的判断就是判断true和false两个字符串,和判断空值类似

if ((c == 't') &&
    json.startsWith("true", currentIndex)) {
  tokens.add(new Token("value", "bool", true));
  currentIndex += 3;
}
if ((c == 'f') &&
    json.startsWith("false", currentIndex)) {
  tokens.add(new Token("value", "bool", false));
  //false是5个字符因此需要移动4位
  currentIndex += 4;
}

我们将测试的json字符串修改为{"name": "asan", "age": 32,"mail": null,"married": true}再进行测试,结果如下

|type        |valueType   |value          |
-------------------------------------------
|object      |object      |{              |
|string      |string      |name           |
|kvSymbol    |kvSymbol    |:              |
|string      |string      |asan           |
|string      |string      |age            |
|kvSymbol    |kvSymbol    |:              |
|value       |number      |32.0           |
|string      |string      |mail           |
|kvSymbol    |kvSymbol    |:              |
|value       |null        |null           |
|string      |string      |married        |
|kvSymbol    |kvSymbol    |:              |
|value       |bool        |true           |
|object      |object      |}              |
-------------------------------------------

结果符合预期。

字符串处理

字符串目前的处理方式是检测到"就当作是字符串,直到下一个"出现,但这种处理方式是不严谨的,有可能字符串本身就包含了",因此需要对转义字符进行处理,我们修改字符串的处理函数

if (c == '"') {
  //字符串
  StringBuffer value = new StringBuffer();
  char cc = json.charAt(++currentIndex);
  // 这里以"作为字符串结束符的标志
  while (cc != '"') {
      if (cc == '\\') {
          cc = json.charAt(++currentIndex);
      }
      value.append(cc);
      cc = json.charAt(++currentIndex);
  }
  tokens.add(new Token("string", value.toString()));
 }

我们将测试字符串改为{"name": "asan", "age": 32,"description": "a \"hudsom\" man","married": true}再测试,结果如下

|type        |valueType   |value          |
-------------------------------------------
|object      |object      |{              |
|string      |string      |name           |
|kvSymbol    |kvSymbol    |:              |
|string      |string      |asan           |
|string      |string      |age            |
|kvSymbol    |kvSymbol    |:              |
|value       |number      |32.0           |
|string      |string      |description    |
|kvSymbol    |kvSymbol    |:              |
|string      |string      |a "hudsom" man |
|string      |string      |married        |
|kvSymbol    |kvSymbol    |:              |
|value       |bool        |true           |
|object      |object      |}              |
-------------------------------------------

成功识别到了转义字符串

数字处理

数字处理目前也有一些问题

  • 没有处理负数情况
  • 没有处理科学技术法
  • 没有区分浮点和整型统一都是浮点数

程序修改如下

if ((c >= '0' && c <= '9') || c == '-') {
  // 数字
  StringBuffer value = new StringBuffer();
  value.append(c);
  // 判断是不是浮点数
  boolean isFloat = false;
  //如果json是一位整型比如:1,那么这里不判断就会报错
  if (currentIndex + 1 < json.length()) {
      char cc = json.charAt(++currentIndex);
      // 判断包含浮点型,整型,科学技术法
      while (cc == '.' || (cc >= '0' && cc <= '9') || cc == 'e' || cc == 'E' || cc == '+' || cc == '-') {
          value.append(cc);
          if (cc == '.') {
              isFloat = true;
          }
          cc = json.charAt(++currentIndex);
      }
  }
  if (isFloat) {
      //浮点数
      tokens.add(new Token("value", "float", Float.parseFloat(value.toString())));
  } else {
      //整型
      tokens.add(new Token("value", "long", Long.parseLong(value.toString())));
  }
}

我们用字符串{"age":32,"deposit": -342.34}进行测试,测试结果如下

|type        |valueType   |value          |
-------------------------------------------
|object      |object      |{              |
|string      |string      |age            |
|kvSymbol    |kvSymbol    |:              |
|value       |long        |32             |
|string      |string      |deposit        |
|kvSymbol    |kvSymbol    |:              |
|value       |float       |-342.34        |
-------------------------------------------

完整测试

我们用完整的字符串进行测试,结果如下

|type        |valueType   |value          |
-------------------------------------------
|object      |object      |{              |
|string      |string      |name           |
|kvSymbol    |kvSymbol    |:              |
|string      |string      |asan           |
|string      |string      |age            |
|kvSymbol    |kvSymbol    |:              |
|value       |long        |32             |
|string      |string      |married        |
|kvSymbol    |kvSymbol    |:              |
|value       |bool        |true           |
|string      |string      |birthday       |
|kvSymbol    |kvSymbol    |:              |
|string      |string      |1992-02-08     |
|string      |string      |salary         |
|kvSymbol    |kvSymbol    |:              |
|value       |float       |1234.56        |
|string      |string      |description    |
|kvSymbol    |kvSymbol    |:              |
|string      |string      |a "hudsom" man |
|string      |string      |tags           |
|kvSymbol    |kvSymbol    |:              |
|array       |array       |[              |
|string      |string      |coder          |
|string      |string      |mid-age        |
|array       |array       |]              |
|string      |string      |location       |
|kvSymbol    |kvSymbol    |:              |
|object      |object      |{              |
|string      |string      |province       |
|kvSymbol    |kvSymbol    |:              |
|string      |string      |福建省            |
|string      |string      |city           |
|kvSymbol    |kvSymbol    |:              |
|string      |string      |泉州市            |
|string      |string      |area           |
|kvSymbol    |kvSymbol    |:              |
|string      |string      |晋江市            |
|object      |object      |}              |
|string      |string      |family         |
|kvSymbol    |kvSymbol    |:              |
|array       |array       |[              |
|object      |object      |{              |
|string      |string      |relation       |
|kvSymbol    |kvSymbol    |:              |
|string      |string      |couple         |
|string      |string      |name           |
|kvSymbol    |kvSymbol    |:              |
|string      |string      |Helen          |
|object      |object      |}              |
|object      |object      |{              |
|string      |string      |relation       |
|kvSymbol    |kvSymbol    |:              |
|string      |string      |daughter       |
|string      |string      |name           |
|kvSymbol    |kvSymbol    |:              |
|string      |string      |XiaoMan        |
|object      |object      |}              |
|array       |array       |]              |
|object      |object      |}              |
-------------------------------------------

完整代码

public class JSONParser {

    //currentIndex保存当前字符串扫描的位置,字符串是逐字符进行扫描
    private int currentIndex = 0;

    /**
     * 对json字符串进行分词
     *
     * @param json json字符串
     * @return token列表
     */
    public List tokenizer(String json) {
        // 保存分词结果
        List tokens = new ArrayList<>();
        while (currentIndex < json.length()) {
            char c = json.charAt(currentIndex);
            //对于空白符可以直接跳过,如果有更多的空白符只需添加新的判断即可
            if (c == ' ' || c == '\r' || c == '\n' || c == ',') {
                //字符只要处理过了必须要将当前位置移动到下一个
                ++currentIndex;
                continue;
            } else if (c == '{' || c == '}') {
                //对象
                tokens.add(new Token("object", c));
            } else if (c == '[' || c == ']') {
                //数组
                tokens.add(new Token("array", c));
            } else if (c == '"') {
                //字符串
                StringBuffer value = new StringBuffer();
                char cc = json.charAt(++currentIndex);
                // 这里以"作为字符串结束符的标志
                while (cc != '"') {
                    if (cc == '\\') {
                        cc = json.charAt(++currentIndex);
                    }
                    value.append(cc);
                    cc = json.charAt(++currentIndex);
                }
                tokens.add(new Token("string", value.toString()));
            } else if (c == ':') {
                // key-value中间的分隔符
                tokens.add(new Token("kvSymbol", "kvSymbol", c));
            } else if ((c >= '0' && c <= '9') || c == '-') {
                // 数字
                StringBuffer value = new StringBuffer();
                value.append(c);
                // 判断是不是浮点数
                boolean isFloat = false;
                //如果json是一位整型比如:1,那么这里不判断就会报错
                if (currentIndex + 1 < json.length()) {
                    char cc = json.charAt(++currentIndex);
                    // 判断包含浮点型,整型,科学技术法
                    while (cc == '.' || (cc >= '0' && cc <= '9') || cc == 'e' || cc == 'E' || cc == '+' || cc == '-') {
                        value.append(cc);
                        if (cc == '.') {
                            isFloat = true;
                        }
                        cc = json.charAt(++currentIndex);
                    }
                }
                if (isFloat) {
                    //浮点数
                    tokens.add(new Token("value", "float", Float.parseFloat(value.toString())));
                } else {
                    //整型
                    tokens.add(new Token("value", "long", Long.parseLong(value.toString())));
                }
            } else if ((c == 'n') && json.startsWith("null", currentIndex)) {
                tokens.add(new Token("value", "null", null));
                currentIndex += 3;
            } else if ((c == 't') &&
                    json.startsWith("true", currentIndex)) {
                tokens.add(new Token("value", "bool", true));
                currentIndex += 3;
            } else if ((c == 'f') &&
                    json.startsWith("false", currentIndex)) {
                tokens.add(new Token("value", "bool", false));
                //false是5个字符因此需要移动4位
                currentIndex += 4;
            }
            ++currentIndex;
        }
        //将当前位置重置
        currentIndex = 0;
        return tokens;
    }
}

代码

完整代码请参考项目https://github.com/wls1036/tiny-json-parser的0x01分支

你可能感兴趣的:(解析器编译器)