从零实现一个JSON解析器(Python)

JSON 解析器是学习编译原理基础概念的绝佳练手项目——规范小、边界明确、不需要代码生成。本文用 Python 从零实现一个完整的 JSON 解析器,覆盖词法分析(Lexer)和语法分析(Parser)两个阶段。

整体思路

JSON 解析分为两步:

JSON 字符串 → Lexer(词法分析) → Token 流 → Parser(语法分析) → Python 对象

Lexer 把原始字符串切分为 Token(标记),比如 {"name":42true

Parser 根据 JSON 语法规则,把 Token 流组装成对应的数据结构。

Token 定义

from enum import Enum, auto
from dataclasses import dataclass
from typing import Any


class TokenType(Enum):
    LEFT_BRACE = auto()    # {
    RIGHT_BRACE = auto()   # }
    LEFT_BRACKET = auto()  # [
    RIGHT_BRACKET = auto() # ]
    COLON = auto()         # :
    COMMA = auto()         # ,
    STRING = auto()        # "..."
    NUMBER = auto()        # 42, 3.14, -1, 1e10
    TRUE = auto()          # true
    FALSE = auto()         # false
    NULL = auto()          # null
    EOF = auto()           # 输入结束


@dataclass
class Token:
    type: TokenType
    value: Any
    pos: int  # 在原始字符串中的位置,用于错误提示

Lexer(词法分析)

Lexer 逐字符扫描,跳过空白,识别出 Token:

class JSONLexError(Exception):
    def __init__(self, msg: str, pos: int):
        super().__init__(f"Lex error at position {pos}: {msg}")
        self.pos = pos


class Lexer:
    def __init__(self, text: str):
        self.text = text
        self.pos = 0

    def _current(self) -> str | None:
        if self.pos < len(self.text):
            return self.text[self.pos]
        return None

    def _advance(self) -> str:
        ch = self.text[self.pos]
        self.pos += 1
        return ch

    def _skip_whitespace(self):
        while self.pos < len(self.text) and self.text[self.pos] in ' \t\n\r':
            self.pos += 1

    def _read_string(self) -> str:
        '''读取 JSON 字符串(处理转义)'''
        start = self.pos
        self._advance()  # 跳过开头的 "
        chars = []
        while True:
            ch = self._current()
            if ch is None:
                raise JSONLexError("Unterminated string", start)
            if ch == '"':
                self._advance()
                return ''.join(chars)
            if ch == '\\':
                self._advance()
                esc = self._advance()
                escape_map = {
                    '"': '"', '\\': '\\', '/': '/',
                    'b': '\b', 'f': '\f', 'n': '\n',
                    'r': '\r', 't': '\t',
                }
                if esc in escape_map:
                    chars.append(escape_map[esc])
                elif esc == 'u':
                    # Unicode 转义: \uXXXX
                    hex_str = self.text[self.pos:self.pos + 4]
                    if len(hex_str) < 4:
                        raise JSONLexError("Invalid unicode escape", self.pos)
                    self.pos += 4
                    chars.append(chr(int(hex_str, 16)))
                else:
                    raise JSONLexError(f"Invalid escape: \\{esc}", self.pos)
            else:
                chars.append(self._advance())
        
    def _read_number(self) -> float | int:
        '''读取 JSON 数字'''
        start = self.pos
        # 可选负号
        if self._current() == '-':
            self._advance()
        # 整数部分
        if self._current() == '0':
            self._advance()
        elif self._current() and self._current().isdigit():
            while self._current() and self._current().isdigit():
                self._advance()
        else:
            raise JSONLexError("Invalid number", start)
        # 小数部分
        is_float = False
        if self._current() == '.':
            is_float = True
            self._advance()
            if not (self._current() and self._current().isdigit()):
                raise JSONLexError("Invalid number: digit expected after '.'", self.pos)
            while self._current() and self._current().isdigit():
                self._advance()
        # 指数部分
        if self._current() and self._current() in 'eE':
            is_float = True
            self._advance()
            if self._current() and self._current() in '+-':
                self._advance()
            if not (self._current() and self._current().isdigit()):
                raise JSONLexError("Invalid number: digit expected in exponent", self.pos)
            while self._current() and self._current().isdigit():
                self._advance()

        num_str = self.text[start:self.pos]
        return float(num_str) if is_float else int(num_str)

    def _read_keyword(self, keyword: str, token_type: TokenType) -> Token:
        '''读取关键字 (true/false/null)'''
        start = self.pos
        for ch in keyword:
            if self._current() != ch:
                raise JSONLexError(f"Expected '{keyword}'", start)
            self._advance()
        return Token(token_type, keyword, start)

    def tokenize(self) -> list[Token]:
        '''将输入字符串转换为 Token 列表'''
        tokens = []
        while True:
            self._skip_whitespace()
            if self.pos >= len(self.text):
                tokens.append(Token(TokenType.EOF, None, self.pos))
                break

            ch = self._current()
            pos = self.pos

            simple_tokens = {
                '{': TokenType.LEFT_BRACE,
                '}': TokenType.RIGHT_BRACE,
                '[': TokenType.LEFT_BRACKET,
                ']': TokenType.RIGHT_BRACKET,
                ':': TokenType.COLON,
                ',': TokenType.COMMA,
            }

            if ch in simple_tokens:
                tokens.append(Token(simple_tokens[ch], ch, pos))
                self._advance()
            elif ch == '"':
                s = self._read_string()
                tokens.append(Token(TokenType.STRING, s, pos))
            elif ch == '-' or ch.isdigit():
                num = self._read_number()
                tokens.append(Token(TokenType.NUMBER, num, pos))
            elif ch == 't':
                tokens.append(self._read_keyword("true", TokenType.TRUE))
            elif ch == 'f':
                tokens.append(self._read_keyword("false", TokenType.FALSE))
            elif ch == 'n':
                tokens.append(self._read_keyword("null", TokenType.NULL))
            else:
                raise JSONLexError(f"Unexpected character: '{ch}'", pos)

        return tokens

Parser(语法分析)

递归下降解析器,每个 JSON 类型对应一个解析方法:

class JSONParseError(Exception):
    def __init__(self, msg: str, pos: int):
        super().__init__(f"Parse error at position {pos}: {msg}")
        self.pos = pos


class Parser:
    def __init__(self, tokens: list[Token]):
        self.tokens = tokens
        self.pos = 0

    def _current(self) -> Token:
        return self.tokens[self.pos]

    def _eat(self, expected: TokenType) -> Token:
        '''消费一个期望类型的 Token'''
        token = self._current()
        if token.type != expected:
            raise JSONParseError(
                f"Expected {expected.name}, got {token.type.name}",
                token.pos
            )
        self.pos += 1
        return token

    def parse(self) -> Any:
        '''解析入口'''
        result = self._parse_value()
        self._eat(TokenType.EOF)
        return result

    def _parse_value(self) -> Any:
        '''解析一个 JSON 值'''
        token = self._current()

        if token.type == TokenType.LEFT_BRACE:
            return self._parse_object()
        elif token.type == TokenType.LEFT_BRACKET:
            return self._parse_array()
        elif token.type == TokenType.STRING:
            self.pos += 1
            return token.value
        elif token.type == TokenType.NUMBER:
            self.pos += 1
            return token.value
        elif token.type == TokenType.TRUE:
            self.pos += 1
            return True
        elif token.type == TokenType.FALSE:
            self.pos += 1
            return False
        elif token.type == TokenType.NULL:
            self.pos += 1
            return None
        else:
            raise JSONParseError(
                f"Unexpected token: {token.type.name}", token.pos
            )

    def _parse_object(self) -> dict:
        '''解析 JSON 对象: { key: value, ... }'''
        self._eat(TokenType.LEFT_BRACE)
        obj = {}

        if self._current().type == TokenType.RIGHT_BRACE:
            self._eat(TokenType.RIGHT_BRACE)
            return obj

        # 解析第一个 key-value
        key_token = self._eat(TokenType.STRING)
        self._eat(TokenType.COLON)
        obj[key_token.value] = self._parse_value()

        # 后续 key-value
        while self._current().type == TokenType.COMMA:
            self._eat(TokenType.COMMA)
            key_token = self._eat(TokenType.STRING)
            self._eat(TokenType.COLON)
            obj[key_token.value] = self._parse_value()

        self._eat(TokenType.RIGHT_BRACE)
        return obj

    def _parse_array(self) -> list:
        '''解析 JSON 数组: [ value, ... ]'''
        self._eat(TokenType.LEFT_BRACKET)
        arr = []

        if self._current().type == TokenType.RIGHT_BRACKET:
            self._eat(TokenType.RIGHT_BRACKET)
            return arr

        arr.append(self._parse_value())

        while self._current().type == TokenType.COMMA:
            self._eat(TokenType.COMMA)
            arr.append(self._parse_value())

        self._eat(TokenType.RIGHT_BRACKET)
        return arr

整合与测试

def json_parse(text: str) -> Any:
    '''解析 JSON 字符串,返回 Python 对象'''
    lexer = Lexer(text)
    tokens = lexer.tokenize()
    parser = Parser(tokens)
    return parser.parse()


# ---- 测试 ----
if __name__ == "__main__":
    # 基本类型
    assert json_parse("42") == 42
    assert json_parse("-3.14") == -3.14
    assert json_parse("1e10") == 1e10
    assert json_parse('"hello"') == "hello"
    assert json_parse("true") is True
    assert json_parse("false") is False
    assert json_parse("null") is None

    # 字符串转义
    assert json_parse(r'"hello\nworld"') == "hello\nworld"
    assert json_parse(r'"tab\there"') == "tab\there"
    assert json_parse(r'"\u0041"') == "A"

    # 数组
    assert json_parse("[]") == []
    assert json_parse("[1, 2, 3]") == [1, 2, 3]
    assert json_parse('[1, "two", true, null]') == [1, "two", True, None]

    # 对象
    assert json_parse("{}") == {}
    assert json_parse('{"a": 1, "b": "hello"}') == {"a": 1, "b": "hello"}

    # 嵌套结构
    nested = '{"users": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]}'
    result = json_parse(nested)
    assert result["users"][0]["name"] == "Alice"
    assert result["users"][1]["age"] == 25

    # 错误处理
    try:
        json_parse('{"key": }')
    except JSONParseError:
        pass  # 期望抛出错误

    try:
        json_parse('"unterminated string')
    except JSONLexError:
        pass

    print("All tests passed!")

解析流程图解

{"name": "Alice", "age": 30} 为例:

Lexer 输出的 Token 流:
  { → STRING("name") → : → STRING("Alice") → , → STRING("age") → : → NUMBER(30) → }

Parser 递归调用:
  parse()
    └─ _parse_value()
         └─ _parse_object()
              ├─ eat({)
              ├─ eat(STRING "name") → eat(:) → _parse_value() → "Alice"
              ├─ eat(,)
              ├─ eat(STRING "age") → eat(:) → _parse_value() → 30
              └─ eat(})

最终结果: {"name": "Alice", "age": 30}  (Python dict)

递归下降解析器的特点

递归下降是最直观的解析方法——每个语法规则对应一个函数,函数之间互相调用形成递归。JSON 的语法规则:

value  → object | array | string | number | "true" | "false" | "null"
object → "{" (pair ("," pair)*)? "}"
pair   → string ":" value
array  → "[" (value ("," value)*)? "]"

每个规则直接对应 Parser 中的一个方法。这种一一对应的关系使得代码非常容易理解和维护。

局限性:递归下降要求语法是 LL(1) 的——即看一个 Token 就能决定走哪个分支。JSON 天然满足这个条件(看到 { 就知道是对象,看到 [ 就知道是数组),所以非常适合用递归下降来解析。

总结

完整代码大约 200 行。这个实现覆盖了 JSON 规范的所有数据类型,包括字符串转义和 Unicode 转义。如果要做到生产级别,还需要处理:数字精度问题(大整数)、重复 key 策略、最大嵌套深度限制、更友好的错误信息等。但作为理解词法分析和语法分析的入门练习,JSON 解析器是一个复杂度恰到好处的项目。