TypeScript实现JSON解析器

512 阅读3分钟

1、JSON语法规则:

JSON的语法可以表示以下三种类型的值:简单值,对象和数组.

  1. 简单值可以在JSON中表示字符串, 数值, 布尔值和null,在JSON中不能使用数值为NaN,也不能为Infinity,undefined也不能使用.字符串必须使用双引号.
  2. 对象可以拆分为左大括号,键值对,右大括号.
  3. 数组拆分为左中括号,数组元素,右中括号.

详情如下:

简单值 := <字符串> | <数字> | <逻辑值> | <null>

数组元素 := <简单值> | <对象> | <数组>

键值对 := <字符串>: (<数组> | <对象> | <简单值>)

可嵌套对象<left, element, right> := <left> ( <element> ("," <element>)* ){0, 1} <right>

数组 := 可嵌套对象<"[", <数组元素>, "]">

对象 := 可嵌套对象<"{", <键值对>, "}">

JSON := 数组 | 对象 | 简单值

2、案例说明:

以JSON格式的字符串{"asd":1}转化为JSON对象.
首先进行分词,"{"、""asd""、":"、"1"、"}",能判断这是一个JSON对象,里面有一个键值对.键值对内遇到一对双引号包裹的定义为字符串,冒号后面的数字1定义为数字.拆分为:

[{"content": "{", "type": "{"}, {"content": "asd", "type": "字符串"}, {"content": ":", "type": ":"}, {"content": "1",  "type": "数字"}, {"content": "}",  "type": "}"}]

然后解析为{"asd": 1}.

3、实现思路:

分词

首先定义类型格式:

export enum 令牌类型 {
  左大括号 = "{",
  右大括号 = "}",
  左中括号 = "[",
  右中括号 = "]",
  引号 = "'",
  逗号 = ",",
  冒号 = ":",
  数字 = "数字",
  字符串 = "字符串",
  逻辑值 = "逻辑值",
  null = "null",
}

然后实现分词函数:

export interface 令牌 {
  content: string;
  type: 令牌类型;
  index: number
}
export interface 解析令牌响应 {
  index_increment: number,
  result: string,
  type: 令牌类型,
  error?: string,
}
class DepthLimiter {
  public current_loop_depth: number = 0;
  public readonly MAX_LOOP_DEPTH = 1000;
  public check_loop_depth() {
    if (this.current_loop_depth > this.MAX_LOOP_DEPTH) {
      throw `递归深度过大: ${this.current_loop_depth}`;
    }
    this.current_loop_depth += 1;  
  }
  public clear() {
    this.current_loop_depth = 0;
  }
}

const depth_limiter = new DepthLimiter()
export const 是否是数字开头 = String.prototype.includes.bind(`-.0123456789`)
export function 是不是n开头(a: string) {
  return a === "n";
}
export function 是不是逻辑值开头(a: string) {
  return a === "t" || a === "f";
}
export function 是否是数字(a: string) {
  if (!"0123456789".includes(a)) {
    return false;
  }
  return true;
}
export function 是否是字符串(a: string) {
  if (a === "") return true;
  const 第一位 = a[0];
  const 最后一位 = a[a.length - 1];
  if (a.length >= 2 && 第一位 === '"' && 最后一位 === '"') {
    return true;
  }
  return false;
}
export function 分词(a: string): Array<令牌> {
  const res: 令牌[] = [];
  for (let i = 0; i < a.length;) {
    const char = a[i];
    let r: 解析令牌响应;
    if (" \t\n\r".includes(char)) {
      i += 1;
    } else if (是否是数字开头(char)) {
      r = 解析数字(a.slice(i))
      res.push({
        content: r.result,
        type: r.type,
        index: i
      })
      i += r.index_increment
    } else if (char === `{`) {
      res.push({
        content: char,
        type: 令牌类型.左大括号,
        index: i
      });
      i += 1;
    } else if (char === `}`) {
      res.push({
        content: char,
        type: 令牌类型.右大括号,
        index: i
      });
      i += 1;
    } else if (char === `[`) {
      res.push({
        content: char,
        type: 令牌类型.左中括号,
        index: i
      });
      i += 1;
    } else if (char === `]`) {
      res.push({
        content: char,
        type: 令牌类型.右中括号,
        index: i
      });
      i += 1;
    } else if (char === `,`) {
      res.push({
        content: char,
        type: 令牌类型.逗号,
        index: i
      });
      i += 1
    } else if (char === `:`) {
      res.push({
        content: char,
        type: 令牌类型.冒号,
        index: i
      });
      i += 1;
    } else if (char === `"`) {
      r = 解析字符串(a.slice(i));
      res.push({
        content: r.result,
        type: r.type,
        index: i
      })
      i += r.index_increment;
    } else if (是不是n开头(char)) {
      r = 解析null(a.slice(i));
      res.push({
        content: r.result,
        type: r.type,
        index: i
      })
      i += r.index_increment;
    } else if (是不是逻辑值开头(char)) {
      r = 解析逻辑值(a.slice(i));
      res.push({
        content: r.result,
        type: r.type,
        index: i
      })
      i += r.index_increment;
    } else {
      throw `语法错误: 未知的符号 ${char}`;
    }
    if (r?.error) {
      throw `error: ${r.error} 于位置 ${i}`;
    }
  }
  return res;
}

解析

解析数字

现在来实现解析数字函数: 数字分为整数,浮点数,带有e/E的数字.
比如:

  1 -1 1.1 -2.0 234 123   
  1e2 1E2 20e3 -3e8 1.2e3  

具体如下:

解析整数 1 -1 2 0
解析浮点数 := 解析整数 . 解析正整数
解析普通数字 := 解析整数 | 解析浮点数
解析科学计数法 := 解析普通数字 e|E 解析正整数
解析数字 := 解析普通数字 | 解析科学计数法

具体实现:

function 解析正整数(s: string, dont_throw_when_zero = false): string {
  let res = '';
  for (const c of s) {
    if ("0123456789".includes(c)) {
      res += c;
    } else {
      break;
    }
  }
  if (dont_throw_when_zero) {
    return res;
  }
  if (res.length && res[0] === "0") {
    if (res.length === 1) {
      return '0';
    } else {
      throw `长整数不能以0开头`;
    }
  }
  return res;
}
export function 解析整数(a: string): string {
  let res = '';
  let to_parsed = a;
  if (to_parsed[0] === "-") {
    to_parsed = to_parsed.slice(1);
    res += '-';
  }
  const 正整数解析结果 = 解析正整数(to_parsed);
  if (正整数解析结果 === "") {
    throw `不正确的数字${a}`;
  } else {
    res += 正整数解析结果;
  }
  if (res.length === 1 && res[0] === "-") {
    throw `只有一个负号`;
  }
  return res;
}
export function 解析普通数字(a: string): string {
  const prefix = 解析整数(a);
  let i = prefix.length;
  if (a[i] !== `.`) {
    return prefix;
  }
  const suffix = 解析正整数(a.slice(i + 1), true);
  if (suffix === "") {
    return prefix;
  }
  return prefix + '.' + suffix;
}
export function 解析科学计数法(a: string): string {
  const prefix = 解析普通数字(a);
  let i = prefix.length;
  if (a.length == 0) {
    return prefix;
  }
  if (a[i] === undefined) {
    return prefix;
  }
  if (a[i].toLowerCase() !== `e`) {
    return prefix;
  }
  const suffix = 解析整数(a.slice(i + 1));
  if (suffix === "") {
    throw `e后面应有数字`;
  }
  return prefix + 'e' + suffix;
}
function 解析数字(a: string): 解析令牌响应 {
  const res: 解析令牌响应 = {
    index_increment: 0,
    result: "",
    type: 令牌类型.数字
  }
  const temp = 解析科学计数法(a);
  res.result = temp;
  res.index_increment = temp.length;
  return res;
}

解析字符串

export function 解析字符串(a: string): 解析令牌响应 {
  const res: 解析令牌响应 = {
    index_increment: 1,
    result: ``,
    type: 令牌类型.字符串
  }
  for (const i of a.slice(1)) {
    if (i !== `"`) {
      if ('\r\n\b\t'.includes(i)) {
        throw `字符串不应该有空白符`;
      }
      res.result += i;
      res.index_increment += 1;
    } else {
      res.index_increment += 1;
      break;
    }
  }
  if (res.index_increment === 1) {
    res.error = "字符串异常停止"
    throw `字符串异常停止 ${res}`;
  }
  return res;
}

解析逻辑值

export function 解析逻辑值(a: string): 解析令牌响应 {
  const res: 解析令牌响应 = {
    index_increment: 0,
    result: ``,
    type: 令牌类型.逻辑值
  }
  for (let i = 0; i < a.length;) {
    if (a.slice(i, i + 4) === "true") {
      res.result = "true";
      res.index_increment = 4;
      i += 4;
      return res;
    } else if (a.slice(i, i + 5) === "false") {
      res.result = "false";
      res.index_increment = 5;
      i += 5;
      return res;
    } else {
      throw `不正确的逻辑值${a}`;
    }
  }
  return res;
}

解析null

export function 解析null(a: string): 解析令牌响应 {
  const res: 解析令牌响应 = {
    index_increment: 0,
    result: ``,
    type: 令牌类型.null
  }
  if (a.slice(0, 4) === "null") {
    res.result = null;
    res.index_increment = 4;
  } else {
    throw `不正确的null`;
  }
  return res;
}

解析数组

export interface 解析对象响应 {
  index_increment: number,
  result: any
}
function 解析数组(arr: Array<令牌>): 解析对象响应 {
  const res: 解析对象响应 = {
    index_increment: 1,
    result: [],
  }
  let flag = true;
  for (let i = 1; i < arr.length;) {
    const curr_token = arr[i];
    if (!flag && curr_token.type !== ']') {
      if (curr_token.type !== ",") {
        throw `缺少逗号!`;
      }
      i++;
    }
    if (curr_token.type === ']') {
      res.index_increment = i + 1;
      break;
    }
    const current_array_res = 通用解析(arr.slice(i));
    res.result.push(current_array_res.result);
    // console.log("12",res.result);
    i += current_array_res.index_increment;
    flag = false;
    // i的结果最后要返回给res的i的增量,这样主函数才能拿到i的增加数
    res.index_increment = i;
  }
  if (arr.length === 1 && arr[0].type === '[') {
    throw `语法错误: 预期外的字符 [`;
  }
  if (arr[res.index_increment - 1].type !== ']') {
    throw `语法错误: 数组未能正常停止`;
  }
  return res;
}

解析键值对

export function 解析pair(arr: 令牌[]): 解析对象响应 {
  // "a": 2, "asd": ["zxc"]
  const res: 解析对象响应 = {
    index_increment: 2,
    result: {
      key: undefined,
      value: undefined
    }
  }
  if (arr.length < 3) {
    throw `key of pair 长度至少为三`;
  }
  if (arr[0].type !== 令牌类型.字符串) {
    throw `key of pair must be string`;
  }
  res.result.key = arr[0].content;
  if (arr[1].type !== 令牌类型.冒号) {
    throw `键值对连接必须为冒号`;
  }
  const value_res = 通用解析(arr.slice(2));
  res.index_increment += value_res.index_increment;
  res.result.value = value_res.result;
  return res;
}

解析对象

export function 解析对象(arr: Array<令牌>): 解析对象响应 {
  const res: 解析对象响应 = {
    index_increment: 1,
    result: {},
  }
  for (let i = 1; i < arr.length;) {
    const token = arr[i];
    if (arr[i].type === `,`) {
      res.index_increment += 1;
      i += 1;
    } else if (arr[i].type === `}`) {
      res.index_increment += 1;
      break;
    }
    const pair = 解析pair(arr.slice(res.index_increment));
    res.result[pair.result.key] = pair.result.value;
    res.index_increment += pair.index_increment;
    i = res.index_increment;
  }
  if (arr.length === 1 && arr[0].type === '{') {
    throw `语法错误: 预期外的字符 {`;
  }
  if (arr[res.index_increment - 1].type !== '}') {
    throw `语法错误: 对象未能正常停止`;
  }
  if (arr[res.index_increment - 2].type === ",") {
    throw `语法错误: 对象不应当有裸逗号`;
  }
  return res;
}

通用解析

export function 通用解析(tokens: 令牌[]): 解析对象响应 {
  const arr = tokens;
  const res: 解析对象响应 = {
    index_increment: 0,
    result: undefined
  }
  const curr_token = arr[res.index_increment];
  depth_limiter.check_loop_depth();
  if (curr_token.type === "[") {
    const current_array_res = 解析数组(arr);
    res.result = current_array_res.result;
    res.index_increment = current_array_res.index_increment;
  } else if (curr_token.type === "{") {
    const current_array_res = 解析对象(arr);
    res.result = current_array_res.result;
    res.index_increment = current_array_res.index_increment;
  }
  else if (curr_token.type === "字符串") {
    res.result = curr_token.content;
    res.index_increment = 1;
  } else if (curr_token.type === "数字") {
    if (curr_token.content === "") {
      throw `数字错误`;
    }
    res.result = Number(curr_token.content);
    res.index_increment = 1;
  } else if (curr_token.type === '}') {
    throw `语法错误: 预期外的字符 ${curr_token.content} 在位置 ${curr_token.index}`;
  } else if (curr_token.type === "null") {
    res.result = curr_token.content
    res.index_increment = 1;
  } else if (curr_token.type === '逻辑值') {
    res.result = curr_token.content === "true" ? true : false;
    res.index_increment = 1;
  } else {
    throw `未知的类型 ${curr_token}`;
  }
  return res;
}
export function 解析(tokens: 令牌[]): 解析对象响应 {
  const res: 解析对象响应 = {
    index_increment: 0,
    result: undefined
  }
  if (tokens.length === 0) {
    throw `空的令牌数组`
  }
  depth_limiter.clear();
  const curr_res = 通用解析(tokens.slice(res.index_increment));
  res.index_increment += curr_res.index_increment;
  res.result = curr_res.result;
  if (res.index_increment !== tokens.length) {
    throw `JSON应该只有一个主体`;
  }
  return res.result;
}

4、参考:

JSON解析器能够正确接受几乎所有符合RFC的JSON, 拒绝unicode以外的几乎所有JSON(测试套件地址:链接 )
使用jest测试框架,测试套件.结果如下:

Snipaste_2022-08-18_20-48-20.png