Python 实现 JSON 解析器

news2024/9/22 21:20:49

Json 解析

文章目录

  • Json 解析
    • Json 的组成
      • 对象结构
      • 数组结构
    • 词法分析
    • 逻辑性解析
      • 解析对象类型
      • 解析数组类型
    • 完整代码
    • 小结

Json 的组成

JSON结构共有2种

  1. 对象结构
  2. 数组结构

一个合法的JSON字符串可以包含这几种元素:

  1. 特殊符号,如"{" “}“表示一个JSON Object,”[” "]“表示一个JSON Array,”:“用于分隔key-value,”,"用于分隔两个元素
  2. 字符串,用引号引起来
  3. 数字,包含0-9,浮点数带有".“,表示符号可带有”+" “-”
  4. 常量有true, false, null

对象结构

对象结构是使用大括号“{”括起来的,大括号是由0个或多个用英文逗号分隔的“关键字:值”对(key:value)构成。

语法:

{
    "健名1""值1",
    "健名2""值2"
}

说明:

jsonObj指的是json对象。对象结构是以"{“开始,到”}"结束。其中"键值"和"值"之间英文冒号构成对,两个"键名:值"之间用英文逗号分隔。

注意,这里的键名是字符串,但是值可以是数值、字符串、对象、数组或逻辑true和false。

数组结构

JSON数组结构是用中括号"[]“括起来,中括号内部由0个或多个英文逗号”,"分隔的值列表组成。

语法:

[
    "name": "ywh",
    "age": 18
]

说明:

数组结构是以"{“开始,到”]“结束,这一点跟JSON对象不同。在JSON数组中,每一对”{}"相当于一个JSON对象,大家看看像不像?而且语法都非常类似。

注意,这里的键名是字符串,但是值可以是数值、字符串、对象、数组或逻辑true和false。

词法分析

词法分析的主要目的是将字符串解析成一小组一小组的合法元素,要将那些是结构所需的符号,数据表达的符号识别出来。比如要将字符串+12.0解析为一个数字12.0等等。

可以分析json的组成发现,特殊的元素都只包含一个字符,常量,数字的表示中间则不会出现空格或其他不相关字符,因此可以轻易地用表达的特征区分,如:

  • 数字 可以匹配一段连续的且所有字符都在0-9或者是("." "+" "-")的范围内。

  • 字符串 对于字符串我们则只需要考虑在双引号"之间的任何字符。特殊的话我们需要考虑到字符串中的特殊的转义字符比如字符\"转义之后的意思其实就是"

  • 常量 对于(true, false, null)这些常量与字符串的不同之处就是它们不会被"所包裹。所以我们只需要读取结构分隔符(例如:(":", ",", "]","}"))之间的字符串,并在匹配字符后查看是否有对应的常量。

  • 空格字符 空格字符一般,让我们的json数据看起来结构更加清晰。但是在解析到相应语言的数据结构的时候则不需要考虑,所以遇到有效字符片段后的空格都跳过。

  • 结构标志性字符 一般单独出现,并且位置比较特殊。比如{作为对象的开始,在对象中:的下一个有效字符片段作为相应value

后面将这些特殊的元素组合称为 合法字符组

所以json解析器要做的就是,如何识别出字符串中一小组一小组的合法元素,同时要根据 结构标志性字符 将得到的合法元素组逻辑拼接起来并检查其中是否有非法格式。

因此编写了下面这个Tokener类:

class Tokener():
  """
    ## TODO: 字符串中的各种 Token 解析
  """
  def __init__(self, json_str):
    self.__str = json_str    # json 字符串
    self.__i = 0   #  当前读到的字符位置
    self.__cur_token = None    # 当前的字符
  
  def __cur_char(self):
    """
      ## 读取当前的字符的位置
    """
    if self.__i < len(self.__str):
      return self.__str[self.__i]
    return ''
  
  def __move_i(self, step=1):
    """
      ## 读取下一个字符
    """
    if self.__i < len(self.__str): self.__i += step
  
  def __next_string(self):
    """
      ## str 字符片段读取
    """
    outstr = ''
    trans_flag = False
    self.__move_i()
    while  self.__cur_char() != '':
      ch = self.__cur_char()
      if ch == "\\": trans_flag = True  # 处理转义
      else:
        if not trans_flag:
          if ch == '"':
            break
        else:
          trans_flag = False
      outstr += ch
      self.__move_i()
    return outstr

  def __next_number(self):
    """
      ## number 字符片段读取
    """
    expr = ''
    while  self.__cur_char().isdigit() or self.__cur_char() in ('.', '+', '-'):
      expr += self.__cur_char()
      self.__move_i()
    self.__move_i(-1)
    if "." in expr: return float(expr)
    else: return int(expr)
  
  def __next_const(self):
    """
      ## bool 字符片段读取
    """
    outstr = ''
    while self.__cur_char().isalpha() and len(outstr) < 5:
      outstr += self.__cur_char()
      self.__move_i()
    self.__move_i(-1)
    
    if outstr in ("true", "false", "null"):
      return {
        "true": True,
        "false": False,
        "null": None,
      }[outstr]
    raise Exception(f"Invalid symbol {outstr}")
  
  def next(self):
    """
      ## 解析这段 json 字符串
      ## TODO: 获得下一个字符片段(合法字符组)
    """
    is_white_space = lambda a_char: a_char in ("\x20", "\n", "\r", "\t")
    while is_white_space(self.__cur_char()):
      self.__move_i()
    
    ch = self.__cur_char()
    if ch == '':
      cur_token = None
    elif ch in ('{', '}', '[', ']', ',', ':'):
      # 这些特殊的包裹性、分隔性字符作为单独的token
      cur_token = ch
    elif ch == '"':
      # 以 “ 开头代表是一个字符串
      cur_token = self.__next_string()
    elif ch.isalpha():
      # 直接以字母开头的话检查是不是 bool 类型的
      cur_token = self.__next_const()
    elif ch.isdigit() or ch in ( '-', '+'):
      # 以数字开头或者是+-符号开头
      cur_token = self.__next_number()
    
    self.__move_i()
    self.__cur_token = cur_token
    
    return cur_token is not None
  
  def cur_token(self):
    # 当前的合法元素组
    return self.__cur_token

代码注释中的 Token 就可以理解为合法字符组。

调用这个类中的next函数就可以获得下一个合法的字符组。也是将其中的字符串、数字、常量、特殊性分隔符识别出来。

逻辑性解析

首先我们需要去区分两种json类型

image-20230118224614303

上图做的就是确定,这个json最外层属于哪种json类型的,然后根据两种类型的特征去解析。

解析对象类型

image-20230118225511356

解析数组类型

image-20230118230042946

完整代码

class Tokener():
  """
    ## TODO: 字符串中的各种 Token 解析
  """
  def __init__(self, json_str):
    self.__str = json_str    # json 字符串
    self.__i = 0   #  当前读到的字符位置
    self.__cur_token = None    # 当前的字符
  
  def __cur_char(self):
    """
      ## 读取当前的字符的位置
    """
    if self.__i < len(self.__str):
      return self.__str[self.__i]
    return ''
  
  def __move_i(self, step=1):
    """
      ## 读取下一个字符
    """
    if self.__i < len(self.__str): self.__i += step
  
  def __next_string(self):
    """
      ## str 字符片段读取
    """
    outstr = ''
    trans_flag = False
    self.__move_i()
    while  self.__cur_char() != '':
      ch = self.__cur_char()
      if ch == "\\": trans_flag = True  # 处理转义
      else:
        if not trans_flag:
          if ch == '"':
            break
        else:
          trans_flag = False
      outstr += ch
      self.__move_i()
    return outstr

  def __next_number(self):
    """
      ## number 字符片段读取
    """
    expr = ''
    while  self.__cur_char().isdigit() or self.__cur_char() in ('.', '+', '-'):
      expr += self.__cur_char()
      self.__move_i()
    self.__move_i(-1)
    if "." in expr: return float(expr)
    else: return int(expr)
  
  def __next_const(self):
    """
      ## bool 字符片段读取
    """
    outstr = ''
    while self.__cur_char().isalpha() and len(outstr) < 5:
      outstr += self.__cur_char()
      self.__move_i()
    self.__move_i(-1)
    
    if outstr in ("true", "false", "null"):
      return {
        "true": True,
        "false": False,
        "null": None,
      }[outstr]
    raise Exception(f"Invalid symbol {outstr}")
  
  def next(self):
    """
      ## 解析这段 json 字符串
      ## TODO: 获得下一个字符片段
    """
    is_white_space = lambda a_char: a_char in ("\x20", "\n", "\r", "\t")
    while is_white_space(self.__cur_char()):
      self.__move_i()
    
    ch = self.__cur_char()
    if ch == '':
      cur_token = None
    elif ch in ('{', '}', '[', ']', ',', ':'):
      # 这些特殊的包裹性、分隔性字符作为单独的token
      cur_token = ch
    elif ch == '"':
      # 以 “ 开头代表是一个字符串
      cur_token = self.__next_string()
    elif ch.isalpha():
      # 直接以字母开头的话检查是不是 bool 类型的
      cur_token = self.__next_const()
    elif ch.isdigit() or ch in ( '-', '+'):
      # 以数字开头或者是+-符号开头
      cur_token = self.__next_number()
    
    self.__move_i()
    self.__cur_token = cur_token
    
    return cur_token is not None
  
  def cur_token(self):
    return self.__cur_token


class JsonDecoder():
  """
    TODO: json 字符串解析
  """
  def __init__(self):
    pass
  
  def __json_object(self, tokener):
    """
      ## json 解析json中的对象结构
    """
    obj = {}
    # 判断是否为 object 起始字符 
    if tokener.cur_token() != "{":
      raise Exception('Json must start with "{"')
    
    while True:  # 循环中每次都去解析一组键值对
      tokener.next()
      tk_temp = tokener.cur_token()
      # 如果直接遇到闭回的 } 则直接返回空结构体
      if tk_temp == "}":
        return {}
    
      if not isinstance(tk_temp, str):
        raise Exception(f'invalid key {tk_temp}')

      # 解析得到 键值对中  key
      key = tk_temp
      tokener.next()
      if tokener.cur_token() != ":":
        raise Exception(f'expect ":" after {key} ')
      
      # 解析得到 键值对中 value
      tokener.next()
      val = tokener.cur_token()
      if val == "[":
        val = self.__json_array(tokener)
      elif val == "{":
        val = self.__json_object(tokener)
      obj[key] = val

      # 解析与下一组键值对的
      tokener.next()
      tk_split = tokener.cur_token()
      if tk_split == ",":    # 若遇到多个元素则重新进入循环
        continue
      elif tk_split == "}":    # 如果为 } 则说明对象闭合
        break
      else:
        if tk_split is None:
          print(f"tk_split {tk_split}")
          raise Exception('missing "}" at the end of object')
        raise Exception(f'unexpected token "{tk_split}" at key "{tk_split}" ')
    return obj
    
  def __json_array(self, tokener):
    if tokener.cur_token() != "[":
      raise Exception('Json array must start with "["')
    arr = []
    while True:
      # 每次遍历都能得到数组中的一个元素
      tokener.next()
      tk_temp = tokener.cur_token()
      if "tk_temp" == "]": return arr
      if tk_temp == "{":
        val = self.__json_object(tokener)
      elif tk_temp == "[":
        val = self.__json_array(tokener)
      elif tk_temp in (',', ":", "}"):
        raise  Exception(f"unexpected token {tk_temp}")
      else:
        val = tk_temp
      arr.append(val)
      
      # 解析获得与数值的逻辑连接符
      tokener.next()
      tk_end = tokener.cur_token()
      if tk_end == ",":
        continue
      if tk_end == "]":
        break
      else:
        if tk_end is None:
          raise  Exception('missing "]" at the end of array')
    return arr
  
  def decode(self, json_str:str):
    tokener = Tokener(json_str)
    if not tokener.next():
      return None
    first_token = tokener.cur_token()
    
    if first_token == "{":
      decode_val = self.__json_object(tokener)
    elif first_token == "[":
      decode_val = self.__json_array(tokener)
    else:
      raise Exception('Json must start with "{"')
    
    if tokener.next():
      raise Exception(f"unexpected token {tokener.cur_token()}")
    return decode_val

def get_testData():
  """
    ## 获得测试数据
  """
  with open("./data.json", "r", encoding="utf-8") as file:
    return file.read()

def test():
  data = get_testData()
  decoder = JsonDecoder()
  print(decoder.decode(data))

if __name__ == '__main__':
  test()

小结

总结一下解析中的要点:

  • 分析元始格式化数据的格式特点

  • 解析字符串中的合法字符组

  • 通过逻辑符将解析得到的合法字符组逻辑串联起来

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/171757.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

将DataFrame进行转置的DataFrame.transpose()方法

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 将DataFrame进行转置 DataFrame.transpose() 选择题 关于以下python代码说法错误的一项是? import pandas as pd dfpd.DataFrame({a:[a1,a2],b:[b1,b2]}) print("【显示】df:\n"…

高德地图红绿灯读秒是怎么实现的?(一)

关于这个读秒实现功能众说风云&#xff0c;目前有两种说法&#xff0c;一种说是靠大数据分析&#xff0c;一种说是靠交管部门数据。 我们先看一下官方的回应&#xff1a;可以自行去抖音看官方号的解释。 以下为原答&#xff1a; 有人说是接入了地方交管数据&#xff0c;其实政策…

2022年度 FinClip 扩展 SDK 推荐!

2022年&#xff0c;FinClip 团队进行了24个产品迭代&#xff0c;为了丰富FinClip 的平台能力&#xff0c;除了核心SDK之外&#xff0c;我们还为开发者们提供了扩展SDK&#xff0c;扩展SDK是一个依赖核心SDK的库&#xff0c;里面提供了核心SDK中所没有的各种小程序API。 官方希…

arduino和物联网云端平台系列---物模型之事件

事件&#xff0c;先下个简单的定义就是发生了什么事件 系列文章都是已经完成了基本的库安装和使用为前提 物模型之事件 基本的添加步骤不描述了&#xff0c;设置一个测试用例 事件我已经设定好了&#xff0c;输出参数代表的是在云端得到的输出&#xff0c;需要我们在设备进行…

【程序环境和预处理】C语言

前言&#xff1a; 到此节便是我们C语言学习的终章了&#xff0c;对C语言的学习便告一段落了&#xff0c;到学完这一章节我们便要进入下一个主题的学习了。 目录1. 程序的翻译环境和执行环境2. 详解编译链接2.1 翻译环境2.2 编译本身也分为几个阶段2.3 运行环境3. 预处理详解3.1…

ESP32设备驱动-L3GD20三轴角速率传感器驱动

L3GD20三轴角速率传感器驱动 1、L3GD20介绍 L3GD20 是一款低功耗三轴角速率传感器。 它包括一个传感元件和一个 I2C 接口,能够通过数字接口 (I2C/SPI) 向外部世界提供测量的角速率。传感元件采用意法半导体开发的专用微加工工艺制造,用于在硅晶片上生产惯性传感器和执行器。…

高通Qualcomm处理器的手机或设备进EDL 9008模式的办法

适用于变砖的设备 由于我们有很多基于 Qualcomm 的设备&#xff0c;其中一些设备可能会古怪地猜测如何进入 EDL 模式&#xff0c;或者如何正确进入。 例如&#xff0c;对于 Alcatel&#xff0c;您必须先按住两个音量键&#xff0c;然后再按住其中一个&#xff0c;对于 CAT B35…

DocPrompt代码实现细节

数据预处理阶段 PaddleOCR PP-Structure&#xff1a;这个库其实是用于版面分析的一个开源库&#xff0c;参见&#xff1a;github: Layout-Parser/layout-parserhttps://github.com/Layout-Parser/layout-parser 代码推理阶段 Paddle-Inferencehttps://paddle-inference.readt…

[JavaEE]定时器

专栏简介: JavaEE从入门到进阶 题目来源: leetcode,牛客,剑指offer. 创作目标: 记录学习JavaEE学习历程 希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长. 学历代表过去,能力代表现在,学习能力代表未来! 目录: 1.定时器的概念 2.标准库中的定时器 3.实现定时…

团灭LeetCode跳跃游戏(相关话题:贪心,BFS)

目录 LeetCode55跳跃游戏 LeetCode45. 跳跃游戏 II LeetCode1306. 跳跃游戏 III LeetCode1345. 跳跃游戏 IV LeetCode55跳跃游戏 给定一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。 数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否…

win32com操作word 第三集:Range精讲(一)

本课程《win32com操作word API精讲&项目实战》&#xff0c;本公众号以文字分享为主&#xff0c;B站与视频号则发布视频分享&#xff0c;ID均为&#xff1a;一灯编程 本集开始&#xff0c;将会深入Document接口。打开或创建一个文档都会产生一个Document对象&#xff0c;它代…

十大排序(Java版本)

排序分为比较排序和非比较排序两种&#xff0c;常见的排序为比较排序&#xff0c;共有七类&#xff1a;直接插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序以及归并排序。另有三种非基于比较类的排序&#xff1a;计数排序、基数排序和桶排序。基于比较的排序直接插…

TreeMap和TreeSet的介绍

目录 1、认识 TreeMap 和 TreeSet 2、TreeMap 的主要成员变量 3、TreeMap 的主要构造方法 4、TreeMap 和 TreeSet 的元素必须可比较 5、TreeMap 和 TreeSet 关于 key 有序 6、TreeMap 和 TreeSet 的关系 7、总结 1、认识 TreeMap 和 TreeSet TreeMap 和 TreeSet 是Ja…

探索SpringMVC-组件之ViewResolver

前言 ViewResolver也就是视图解析器&#xff0c;他将是我们《探索SpringMVC》系列要介绍的最后一个常用的组件。其他组件&#xff1a;MultipartResolver、LocaleResolver、ThemeResolver、RequestToViewNameTranslator、FlashMapManager&#xff0c;相对简单&#xff0c;大家可…

一个想活得简单的程序猿的2022年终总结!

前言 今年的总结相比以往来说&#xff0c;可写的太少了&#xff0c;但看到我17年开始写的年终总结&#xff0c;已定下每年写下的承诺&#xff0c;因此即便可写的不多&#xff0c;但是还是写下吧&#xff0c;毕竟又过了一年&#xff0c;总有东西会留下&#xff01; 今年事件 疫…

【Linux杂篇】Windows远程登陆Linux、Linux静态IP配置

前言 如果要长期连接Linux环境&#xff0c;就需要给Linux配置一个静态IP&#xff0c;否则可能每次连接的IP都不一样而且还很麻烦。 除此之外&#xff0c;我们使用ssh远程登录的时候&#xff0c;每次都要输入密码&#xff0c;也很麻烦&#xff0c;所以建议配置ssh密钥&#xff…

执行 java -jar xxx.jar 的时候底层到底做了什么?

大家都知道我们常用的 SpringBoot 项目最终在线上运行的时候都是通过启动 java -jar xxx.jar 命令来运行的。那你有没有想过一个问题&#xff0c;那就是当我们执行 java -jar 命令后&#xff0c;到底底层做了什么就启动了我们的 SpringBoot 应用呢&#xff1f;或者说一个 Sprin…

Redis删除了大量数据后,为什么内存占用还是很高?

前言 上周刚来了个应届小师弟&#xff0c;组长说让我带着&#xff0c;周二问了我这样一个问题&#xff1a;师兄啊&#xff0c;我用top命令看了下服务器的内存占用情况&#xff0c;发现Redis内存占用严重&#xff0c;于是我就删除了大部分不用的keys&#xff0c;为什么内存占用…

文件操作【C语言】

目录 一、为什么使用文件 二、什么是文件 1、程序文件 2、数据文件 3、文件名 三、文件的打开和关闭 1、文件指针 2、文件的打开和关闭 四、文件的顺序读写 五、文件的随机读写 1、fseek 2、ftell 3、rewind 七、文件读取结束的判定 1、被错误使用的feof 1、文…

unocss原子化

文章目录1. 安装2. 配置3. Unocss预设3.1 presetUno3.2 presetAttributify3.3 presetIcons了解什么是UnoCSS请看&#xff1a;重新构想原子化CSS - 知乎 github地址&#xff1a;UnoCSS UnoCSS搜索引擎 1. 安装 npm i -D unocss2. 配置 vite.config.ts import { defineConf…