自己手撸一个 JSON 解析器

用Python手写一个JSON解析程序 JSON的组成 一个合法的JSON字符串可以包含这几种元素: 特殊符号,如 "{"  "}" 表示一个JSON Object,"[" "]" 表示一个JSON Array,":"用于分隔key-value,"," 用于分隔两个元素 字符串,用引号引起来 数字,包含0-9,浮点数带有".",表示符号可带有"+" "-" 常 阅读详情

作者:omgleoo

链接:https://gyl-coder.top/JSONParser/

JSON

JSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。这些特性使JSON成为理想的数据交换语言。

JSON与JS的区别以及和XML的区别具体请参考百度百科:

https://baike.baidu.com/item/JSON/2462549?fr=aladdin

JSON有两种结构:

第一种:对象

“名称/值”对的集合不同的语言中,它被理解为对象(object),纪录(record),结构(struct),字典(dictionary),哈希表(hash table),有键列表(keyed list),或者关联数组 (associative array)。

对象是一个无序的“‘名称/值’对”集合。一个对象以“{”(左括号)开始,“}”(右括号)结束。每个“名称”后跟一个“:”(冒号);“‘名称/值’ 对”之间使用“,”(逗号)分隔。

{"姓名": "张三", "年龄": "18"}

第二种:数组

值的有序列表(An ordered list of values)。在大部分语言中,它被理解为数组(array)。

数组是值(value)的有序集合。一个数组以“[”(左中括号)开始,“]”(右中括号)结束。值之间使用“,”(逗号)分隔。

值(value)可以是双引号括起来的字符串(string)、数值(number)、true、false、 null、对象(object)或者数组(array)。这些结构可以嵌套。

[
    { 
    "姓名": "张三",          
    "年龄":"18"    
    },

    {        
    "姓名": "里斯",          
    "年龄":"19"   

    }
]

通过上面的了解可以看出,JSON存在以下几种数据类型(以Java做类比):

jsonjava
stringJava中的String
numberJava中的Long或Double
true/falseJava中的Boolean
nullJava中的null
[array]Java中的List或Object[]
{“key”:”value”}Java中的Map<String, Object>

解析JSON

JSON解析器的基本原理

输入一串JSON字符串,输出一个JSON对象。

步骤

JSON解析的过程主要分以下两步:

第一步:对于输入的一串JSON字符串我们需要将其解析成一组token流。

例如 JSON字符串{“姓名”: “张三”, “年龄”: “18”} 我们需要将它解析成

{、 姓名、 :、 张三、 ,、 年龄、 :、 18、 }

这样一组token流

第二步:根据得到的token流将其解析成对应的JSON对象(JSONObject)或者JSON数组(JSONArray)

下面我们来详细分析下这两个步骤:

获取token流

根据JSON格式的定义,token可以分为以下几种类型

token含义
NULLnull
NUMBER数字
STRING字符串
BOOLEANtrue/false
SEP_COLON:
SEP_COMMA,
BEGIN_OBJECT{
END_OBJECT}
BEGIN_ARRAY[
END_ARRAY]
END_DOCUMENT表示JSON数据结束

根据以上的JSON类型,我们可以将其封装成enum类型的TokenType

package com.json.demo.tokenizer;
/**
 BEGIN_OBJECT({)
 END_OBJECT(})
 BEGIN_ARRAY([)
 END_ARRAY(])
 NULL(null)
 NUMBER(数字)
 STRING(字符串)
 BOOLEAN(true/false)
 SEP_COLON(:)
 SEP_COMMA(,)
 END_DOCUMENT(表示JSON文档结束)
 */

public enum TokenType {
    BEGIN_OBJECT(1),
    END_OBJECT(2),
    BEGIN_ARRAY(4),
    END_ARRAY(8),
    NULL(16),
    NUMBER(32),
    STRING(64),
    BOOLEAN(128),
    SEP_COLON(256),
    SEP_COMMA(512),
    END_DOCUMENT(1024);

    private int code;    // 每个类型的编号

    TokenType(int code) {
        this.code = code;
    }

    public int getTokenCode() {
        return code;
    }
}

在TokenType中我们为每一种类型都赋一个数字,目的是在Parser做一些优化操作(通过位运算来判断是否是期望出现的类型)

在进行第一步之前JSON串对计算机来说只是一串没有意义的字符而已。第一步的作用就是把这些无意义的字符串变成一个一个的token,上面我们已经为每一种token定义了相应的类型和值。所以计算机能够区分不同的token,并能以token为单位解读JSON数据。

下面我们封装一个token类来存储每一个token对应的值

package com.json.demo.tokenizer;

/**
 * 存储对应类型的字面量
 */

public class Token {
    private TokenType tokenType;
    private String value;

    public Token(TokenType tokenType, String value) {
        this.tokenType = tokenType;
        this.value = value;
    }

    public TokenType getTokenType() {
        return tokenType;
    }

    public void setTokenType(TokenType tokenType) {
        this.tokenType = tokenType;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Token{" +
                "tokenType=" + tokenType +
                ", value='" + value + '\'' +
                '}';
    }
}

在解析的过程中我们通过字符流来不断的读取字符,并且需要经常根据相应的字符来判断状态的跳转。所以我们需要自己封装一个ReaderChar类,以便我们更好的操作字符流。

package com.json.demo.tokenizer;

import java.io.IOException;
import java.io.Reader;

public class ReaderChar {
    private static final int BUFFER_SIZE = 1024;
    private Reader reader;
    private char[] buffer;
    private int index;      // 下标
    private int size;

    public ReaderChar(Reader reader) {
        this.reader = reader;
        buffer = new char[BUFFER_SIZE];
    }

    /**
     * 返回 pos 下标处的字符,并返回
     * @return
     */
    public char peek() {
        if (index - 1 >= size) {
            return (char) -1;
        }

        return buffer[Math.max(0, index - 1)];
    }

    /**
     * 返回 pos 下标处的字符,并将 pos + 1,最后返回字符
     * @return
     * @throws IOException
     */
    public char next() throws IOException {
        if (!hasMore()) {
            return (char) -1;
        }

        return buffer[index++];
    }

    /**
     * 下标回退
     */
    public void back() {
        index = Math.max(0, --index);
    }

    /**
     * 判断流是否结束
     */
    public boolean hasMore() throws IOException {
        if (index < size) {
            return true;
        }

        fillBuffer();
        return index < size;
    }

    /**
     * 填充buffer数组
     * @throws IOException
     */
    void fillBuffer() throws IOException {
        int n = reader.read(buffer);
        if (n == -1) {
            return;
        }

        index = 0;
        size = n;
    }
}

另外我们还需要一个TokenList来存储解析出来的token流

package com.json.demo.tokenizer;

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

/**
 * 存储词法解析所得的token流
 */
public class TokenList {
    private List<Token> tokens = new ArrayList<Token>();
    private int index = 0;

    public void add(Token token) {
        tokens.add(token);
    }

    public Token peek() {
        return index < tokens.size() ? tokens.get(index) : null;
    }

    public Token peekPrevious() {
        return index - 1 < 0 ? null : tokens.get(index - 2);
    }

    public Token next() {
        return tokens.get(index++);
    }

    public boolean hasMore() {
        return index < tokens.size();
    }

    @Override
    public String toString() {
        return "TokenList{" +
                "tokens=" + tokens +
                '}';
    }
}

JSON解析比其他文本解析要简单的地方在于,我们只需要根据下一个字符就可知道接下来它所期望读取的到的内容是什么样的。如果满足期望了,则返回 Token,否则返回错误。

为了方便程序出错时更好的debug,程序中自定义了两个exception类来处理错误信息。(具体实现参考exception包)

下面就是第一步中的重头戏(核心代码):

public TokenList getTokenStream(ReaderChar readerChar) throws IOException {
        this.readerChar = readerChar;
        tokenList = new TokenList();

        // 词法解析,获取token流
        tokenizer();

        return tokenList;
    }

    /**
     * 将JSON文件解析成token流
     * @throws IOException
     */
    private void tokenizer() throws IOException {
        Token token;
        do {
            token = start();
            tokenList.add(token);
        } while (token.getTokenType() != TokenType.END_DOCUMENT);
    }

    /**
     * 解析过程的具体实现方法
     * @return
     * @throws IOException
     * @throws JsonParseException
     */
    private Token start() throws IOException, JsonParseException {
        char ch;
        while (true){   //先读一个字符,若为空白符(ASCII码在[0, 20H]上)则接着读,直到刚读的字符非空白符
            if (!readerChar.hasMore()) {
                return new Token(TokenType.END_DOCUMENT, null);
            }

            ch = readerChar.next();
            if (!isWhiteSpace(ch)) {
                break;
            }
        }

        switch (ch) {
            case '{':
                return new Token(TokenType.BEGIN_OBJECT, String.valueOf(ch));
            case '}':
                return new Token(TokenType.END_OBJECT, String.valueOf(ch));
            case '[':
                return new Token(TokenType.BEGIN_ARRAY, String.valueOf(ch));
            case ']':
                return new Token(TokenType.END_ARRAY, String.valueOf(ch));
            case ',':
                return new Token(TokenType.SEP_COMMA, String.valueOf(ch));
            case ':':
                return new Token(TokenType.SEP_COLON, String.valueOf(ch));
            case 'n':
                return readNull();
            case 't':
            case 'f':
                return readBoolean();
            case '"':
                return readString();
            case '-':
                return readNumber();
        }

        if (isDigit(ch)) {
            return readNumber();
        }

        throw new JsonParseException("Illegal character");
    }

在start方法中,我们将每个处理方法都封装成了单独的函数。主要思想就是通过一个死循环不停的读取字符,然后再根据字符的期待值,执行不同的处理函数。

下面我们详解分析几个处理函数:

private Token readString() throws IOException {
        StringBuilder sb = new StringBuilder();
        while(true) {
            char ch = readerChar.next();
            if (ch == '\\') {   // 处理转义字符
                if (!isEscape()) {
                    throw new JsonParseException("Invalid escape character");
                }
                sb.append('\\');
                ch = readerChar.peek();
                sb.append(ch);
                if (ch == 'u') {   // 处理 Unicode 编码,形如 \u4e2d。且只支持 \u0000 ~ \uFFFF 范围内的编码
                    for (int i = 0; i < 4; i++) {
                        ch = readerChar.next();
                        if (isHex(ch)) {
                            sb.append(ch);
                        } else {
                            throw new JsonParseException("Invalid character");
                        }
                    }
                }
            } else if (ch == '"') {     // 碰到另一个双引号,则认为字符串解析结束,返回 Token
                return new Token(TokenType.STRING, sb.toString());
            } else if (ch == '\r' || ch == '\n') {     // 传入的 JSON 字符串不允许换行
                throw new JsonParseException("Invalid character");
            } else {
                sb.append(ch);
            }
        }
    }

该方法也是通过一个死循环来读取字符,首先判断的是JSON中的转义字符。

JSON中允许出现的有以下几种

\"
\\
\b
\f
\n
\r
\t
\u four-hex-digits
\/

具体的处理方法封装在了isEscape()方法中,处理Unicode 编码时要特别注意一下u的后面会出现四位十六进制数。当读取到一个双引号或者读取到了非法字符(’\r’或’、’\n’)循环退出。

判断数字的时候也要特别小心,注意负数,frac,exp等等情况。

通过上面的解析,我们可以得到一组token,接下来我们需要以这组token作为输入,解析出相应的JSON对象

解析出JSON对象

解析之前我们需要定义出JSON对象(JSONObject)和JSON数组(JSONArray)的实体类。

package com.json.demo.jsonstyle;

import com.json.demo.exception.JsonTypeException;
import com.json.demo.util.FormatUtil;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * JSON的对象形式
 * 对象是一个无序的“‘名称/值’对”集合。一个对象以“{”(左括号)开始,“}”(右括号)结束。每个“名称”后跟一个“:”(冒号);“‘名称/值’ 对”之间使用“,”(逗号)分隔。
 */
public class JsonObject {
    private Map<String, Object> map = new HashMap<String, Object>();

    public void put(String key, Object value) {
        map.put(key, value);
    }

    public Object get(String key) {
        return map.get(key);
    }
    ...

}

package com.json.demo.jsonstyle;

import com.json.demo.exception.JsonTypeException;
import com.json.demo.util.FormatUtil;

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

/**
 * JSON的数组形式
 * 数组是值(value)的有序集合。一个数组以“[”(左中括号)开始,“]”(右中括号)结束。值之间使用“,”(逗号)分隔。
 */
public class JsonArray {
    private List list = new ArrayList();

    public void add(Object obj) {
        list.add(obj);
    }

    public Object get(int index) {
        return list.get(index);
    }

    public int size() {
        return list.size();
    }
    ...
}

之后我们就可以写解析类了,由于代码较长,这里就不展示了。有兴趣的可以去GitHub上下载。实现逻辑比较简单,也易于理解。

解析类中的parse方法首先根据第一个token的类型选择调用parseJsonObject()或者parseJsonArray(),进而返回JSON对象或者JSON数组。上面的解析方法中利用位运算来判断字符的期待值既提高了程序的执行效率也有助于提高代码的ke’du’xi

完成之后我们可以写一个测试类来验证下我们的解析器的运行情况。我们可以自己定义一组JSON串也可以通过HttpUtil工具类从网上获取。最后通过FormatUtil类来规范我们输出。

具体效果如下图所示:

参考文章

http://www.cnblogs.com/absfree/p/5502705.html
https://www.liaoxuefeng.com/article/0014211269349633dda29ee3f29413c91fa65c372585f23000?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
https://segmentfault.com/a/1190000010998941#articleHeader6
http://json.org/json-zh.html

(完)
【推荐阅读】

用Spring Security, JWT, Vue实现一个前后端分离无状态认证Demo
面试时写不出排序算法?看这篇就够了
图解Spring解决循环依赖,认清IOC!

是真的猛!SQL 语法速成手册

面试官:BigDecimal一定不会丢失精度吗?

通俗易懂讲布隆过滤器
C++ 手写一个高性能json生成与解析器 本文主要详细介绍如果用C++,自己手写一个json格式数据的生成与解析库(高效、易用) 阅读详情

相关推荐

非常漂亮的影视解析源码.zip

非常漂亮的前端页面 可直接前端输入链接解析 可自用可当作影视站接口 接口格式:你的域名/?url= 后台自带解析接口,超清无广告,支持超前点播,如失效可在后台自行更改 无需安装,直接上传源码至服务器或虚拟主机即可,后台:域名/admin 密码:123456

Java之手写JSON解析器

一个算是高性能的json解析器

一夜梦惊人的博客 1820

tv_ZYPLAYERjson资源_zyplayer影视源_zyplayer源json_ZY-Player-master_

直播、影视源

撸一个JSON解析器

原文地址 JSON JSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。这些...

weixin_34195546的博客 775

OpenClaw:深度学习驱动的智能编程工具实战解析

智能编程工具通过深度学习技术革新传统开发流程,其核心在于代码语义理解与实时协作能力。这类工具采用AI代码补全、错误预测等技术,显著降低开发者认知负荷,尤其适用于复杂业务逻辑开发和遗留系统维护场景。以OpenClaw为例,其智能填充和上下文感知功能可提升40%编码效率,减少65%调试时间。关键技术实现包含自然语言处理、代码静态分析等,在金融科技、电商系统等领域具有广泛应用前景。热词提示:代码理解引擎、智能工作流整合展现了当前AI辅助开发的最新趋势。

chubaisheng8627的博客 320

Ansys Lumerical FDTD仿真加速策略与高性能硬件选型指南

本文深入探讨了Ansys Lumerical FDTD仿真的核心加速策略,涵盖算法优化与硬件选型两大维度。通过合理运用非均匀网格、对称边界条件等算法技巧,并精准匹配CPU/GPU计算资源与高性能硬件配置,可显著提升仿真效率,将计算时间从数天缩短至数小时。

a0b1c2d3的专栏 620

自己手撸一个JSON解析器,撸撸更健康

JSONJSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。采用完全独立于语言的文本...

hnjsjsac的博客 583

使用CubeMX生成的项目在Keil下添加DSP库并包含arm_math头文件后报错的解决办法

问题描述: 最近使用Cube生成工程后,在Keil中使用Manage Runtime Environment即绿色小饼干按钮添加CMSIS的DSP库后编译报错。很有意思的是,STM32论坛上有外国朋友报告的同样的错误,都是30个Errors,5个Warning。 问题分析: 参考国内论坛某帖子(具体想不起来了),认为因该是Cube复制了库后,Keil从原始Packs目录下引用DSP库导致和Cub...

qq_32256951的博客 7003

js json数组_自己手撸一个 JSON 解析器

作者:omgleoo链接:https://gyl-coder.top/JSONParser/今日推荐:有了这个开源项目,再也不会忘记 Linux 命令啦个人原创博客:点击前往JSONJSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。采用完全独立于语言的文本格式,但是也使用了类似于C语...

weixin_39765339的博客 119

yolov8的Detect层详解(输出维度改动)

对于部署端需要拿到onnx的output进行后处理,但是基于anchor_free的yolov8输出是1*(4+cls)*8400,不是我们常见的ncwh格式,所以对Detect层进行了学习,分享一下体会。

yjcccccc的博客 2万+

YOLOv8【第十一章:视频追踪与流处理篇·第16节】一文搞懂,DeepStream 部署入门:NVIDIA 视频分析 SDK 与 YOLOv8 结合!

🏆 本文收录于 《YOLOv8实战:从入门到深度优化》 专栏。该专栏系统复现并梳理全网各类 YOLOv8 改进与实战案例(当前已覆盖分类 / 检测 / 分割 / 追踪 / 关键点 / OBB 检测等方向),坚持持续更新 + 深度解析,质量分长期稳定在 97 分以上,可视为当前市面上 覆盖较全、更新较快、实战导向极强 的 YOLO 改进系列内容之一。

**My Coding Family** 841

手把手教你撸一个JSON解析器

作者:omgleoo原文:https://gyl-coder.top/JSONParser/JSONJSON(JavaScript Object Notation, JS...

程序员之家 462

电子电器架构刷写方案——General Flash Bootloader

### 文章大体有如下内容: ### 1、简介 ### 2、刷写系统概述 ### 3、Flash Bootloader刷写流程 ### 4、Flash Bootloader架构介绍 ### 5、关于刷写的思考

Soly_kun的博客 2214

自己动手实现一个简单的JSON解析器

介绍 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。相对于另一种数据交换格式 XML,JSON 有着诸多优点。比如易读性更好,占用空间更少等。在 web 应用开发领域内,得益于 JavaScript 对 JSON 提供的良好支持,JSON 要比 XML 更受开发人员青睐。所以作为开发人员,如果有兴趣的话,还是应该深入了解一下 JSON 相关的知识。...

qq_39235296的博客 1019

AI快速变现之路,AI视频创作

初期投入$3000(工具订阅+样片制作),重点攻克跨境电商和本地生活服务市场。关注2025年AIGC视频工具的API降价趋势(如Pika企业版折扣可达50%),建议与云服务商签订长期合作协议。

qq_29896351的博客 1983

Natural Language Processing

作者:禅与计算机程序设计艺术 1.简介 自然语言处理(NLP)是研究如何使计算机理解和处理自然语言的理论和方法,目的是让电脑更好的理解、获取、生成、存储和沟通人类语言。20世纪80年代末,IBM的三位博士陈天奇、周博磊和王力雄在贝尔实验室创建了首个自然语言处理系统——“布莱克多·艾森豪威尔”(Bletc

AI天才研究院 731

用C++实现的JSON解析器

用C++实现的JSON解析器 A simple C++11 json parser. 用C++11实现的json解析器。 源码地址(github):SimpleJSON About SimpleJSON is a lightweight json library written in C++.It surpports basic operations including reading and writting json documents. SimpleJSON是一个轻量的由C++实现的json解析库

lpy13699855019的博客 542

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

手写JSON.parse和JSON.stringify

搞懂了有限状态机,手写各种解析器都不在话下,主要区别也就是考虑怎么去设计各种状态流转。如果不理解有限状态机建议先去阅读:用有限状态机实现一个简版html解析器,然后再来阅读本文就很容易理解了。手写JSON.parse有两种实现方式,第1种初级版没啥难度,第2种利用状态机自己去解析字符流,需要先学习下编译原理相关的知识,否则理解起来可能有点蒙。初级版本 JSON parse直接通过 eval 函数实...

cafehaus的博客 639

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

Java之手写JSON解析器(二)

首先就是这个SIZE,(1)一个int为4bytes,那么1024*64*4=256Kb,但其实我是想要设置1024个就可以,这样子刚好4Kb,是一个page cache的大小,但是可能1000还是不太够用吧,于是就网大了写。3.关于OOM的优化,之前是采用的LinkedList来收集Token的,但是这样子由于Token很小,利用率很低,过大的文件会导致OOM,所以我这里进行了优化。PS:打脸操作来了,小文件时这个操作没有太多的性能损耗,但是一旦到了50MB的就显示出来了,所以改回正常操作。

一夜梦惊人的博客 449

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

撸一个 JSON 解析器

程序员的成长之路互联网/程序员/成长/职场关注阅读本文大概需要 8 分钟。来源:http://t.cn/EamWhIfJSONJSON(JavaScript Object Notati...

良月柒 923
上一篇: B站30K播放量,SpringBoot+Vue前后端分离完整入门教程!
下一篇: 你还在从零搭建项目 ?
Java思维导图
博客等级 码龄10年 306粉丝 71原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值