[source] 深入理解PHP源代码JSON解析

https://github.com/openparallel/php-src/blob/6025d2973786ac5be80bcd7d276274d46fab3add/ext/json/json.c

json_c.png

1 主题

主要从源码角度分析php的 json_encode && json_decode
php版本: 5.5.23

2 基本概念

首先,json串的 key必须是字符串 , int类型的key会被转换成字符串
如果传入的是浮点数,则会被取整,变成字符串,或者key 为数组之类的,则该key会被过滤

其次,php的json函数只支持utf字符,不支持gbk字符

3 json_encode

php源码的实现其实很简单,就是 遍历传入的参数(一般是array),找出 key && value,拼接成字符串

3.1 Description

函数的原型如下,有两个可选参数

  1. string json_encode ( mixed $value [, int $options = 0 [, int $depth = 512 ]] )
    value : 这个就是要进行encode的参数,支持任何类型的变量,一般是数组
    depth : 限制最大的层数
    options : 一些特殊需求,比如可以设置是否对某些特殊字符进行特殊编码
  • JSON_HEX_AMP : 在encode的时候把 & 变成 \u0026
  • JSON_HEX_APOS : 在encode的时候把 ' 变成 \u0027
  • JSON_HEX_QUOT : 在encode的时候把 " 变成 \u0022
  • JSON_FORCE_OBJECT : 强制输出为object形式,针对非关联数组的时候
    比如 arr = array(‘x’) , json_encode(arr) => [“x”], json_encode($arr, JSON_FORCE_OBJECT ) => {“0”:”x”}
  • JSON_NUMERIC_CHECK : 在encode 的时候,把整数字符串变成整数
  • JSON_PRETTY_PRINT : 使输出的json串更可读
  • JSON_UNESCAPED_SLASHES : 不对 / 进行转义
  • JSON_UNESCAPED_UNICODE : 使多字节更可读,默认是转换成 \uXXXX
  • JSON_PRESERVE_ZERO_FRACTION : 确保浮点数被encode成浮点数,这个选项在5.6.6里才被添加
    例如: json_encode(12.0) => 12, json_encode(12.0, JSON_PRESERVE_ZERO_FRACTION) => 12.0

3.2 源码

3.2.1 主体

json_encode的入口很简单,接受三个参数,然后调用 php_json_encode 进行处理,结果存储在 buf 里, php自己封装了一套字符串处理的函数

static PHP_FUNCTION(json_encode)
    {
        zval* parameter;
        smart_str buf = {0};
        long options = 0;
        long depth = JSON_PARSER_DEFAULT_DEPTH;
        //step1 取参数
        if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|ll", &parameter, &options, &depth) == FAILURE) {
            return;
        }
 
        JSON_G(error_code) = PHP_JSON_ERROR_NONE;
 
        JSON_G(encode_max_depth) = depth;
 
        //step2 调用处理函数
        php_json_encode(&buf, parameter, options TSRMLS_CC);
 
        //step3 设置返回的内容
        if (JSON_G(error_code) != PHP_JSON_ERROR_NONE && !(options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR)) {
            ZVAL_FALSE(return_value);
        } else {
            ZVAL_STRINGL(return_value, buf.c, buf.len, 1);
        }
 
        smart_str_free(&buf);
    }

3.2.2 主要处理流程

php_json_encode 里主要是根据输入参数 val的类型 进行处理,并把结果 append到buf 里,具体有这么几种情况:

  • NULL BOOL LONG 的处理比较简单,直接转换成相应的字符串append到buf里

  • DOUBLE 浮点数类型的,会 判断是否越界,同时还会截断取前面EG(precision)位 , precision 在php.ini里可以设置,默认是14位

  • STRING 字符串类型的,会调用 json_escape_string 进行处理,比如加转义字符,特殊字符是否需要替换之类的

  • OBJECT 这种对象类型的,会先判断一下该对象是否 implementsJsonSerializable 这个抽象类,并重载了 jsonSerialize 函数

    如果是的话,则调用类自定义的 jsonSerialize 函数进行encode,否则调用 jsonencode_array 进行处理,具体可以参考 中文版 <[http://www.laruence.com/2011/10/10/2204.html](http://www.laruence.com/2011/10/10/2204.html)> 或者 英文版 <[http://schlueters.de/blog/archives/135-Jason,-let-me-help-you!.html](http://schlueters.de/blog/archives/135-Jason,-let-me-help-you!.html)>_ 里关于 JsonSerializable接口 的介绍,这个是5.4才加入的新功能

  • ARRAY 这种数组类型的,则会调用 json_encode_array 进行处理,在 jsonencode_array 里会遍历数组,先插入 array 的key,然后再插入value(其实是通过调用php_json_encode,相互递归调用--||)

PHP_JSON_API void php_json_encode(smart_str* buf, zval* val, int options TSRMLS_DC)
   {
       switch (Z_TYPE_P(val))
       {
           case IS_NULL: //NULL 类型
               smart_str_appendl(buf, "null", 4);
               break;
 
           case IS_BOOL: //BOOL 类型
               if (Z_BVAL_P(val)) {
                   smart_str_appendl(buf, "true", 4);
               } else {
                   smart_str_appendl(buf, "false", 5);
               }
               break;
 
           case IS_LONG: //整数
               smart_str_append_long(buf, Z_LVAL_P(val));
               break;
 
           case IS_DOUBLE: //浮点数
               {
                   char* d = NULL;
                   int len;
                   double dbl = Z_DVAL_P(val);
 
                   if (!zend_isinf(dbl) && !zend_isnan(dbl)) { //判断是否异常
                       len = spprintf(&d, 0, "%.*k", (int) EG(precision), dbl);
                       smart_str_appendl(buf, d, len);
                       efree(d);
                   } else {
                       JSON_G(error_code) = PHP_JSON_ERROR_INF_OR_NAN;
                       smart_str_appendc(buf, '0');
                   }
               }
               break;
 
           case IS_STRING: //字符串类型
               json_escape_string(buf, Z_STRVAL_P(val), Z_STRLEN_P(val), options TSRMLS_CC);
               break;
 
           case IS_OBJECT: //对象类型,先判断类是否implements了JsonSerializable这个抽象类,并重载了jsonSerialize函数
               if (instanceof_function(Z_OBJCE_P(val), php_json_serializable_ce TSRMLS_CC)) {
                   json_encode_serializable_object(buf, val, options TSRMLS_CC); //使用对象的 jsonSerialize函数 进行encode
                   break;
               }
               //没有定义的话,则调用 json_encode_array 函数 encode 这个对象
               // fallthrough -- Non-serializable object
           case IS_ARRAY: //数组对象
               json_encode_array(buf, &val, options TSRMLS_CC);
               break;
 
           default: //出错
               JSON_G(error_code) = PHP_JSON_ERROR_UNSUPPORTED_TYPE;
               smart_str_appendl(buf, "null", 4);
               break;
       }
 
       return;
   }

3.2.3 对对象的处理

之前也说了,对 OBJECT 这种对象类型的,会先判断一下该对象是否 implementsJsonSerializable 这个抽象类,并重载了 jsonSerialize 函数

如果是的话,则调用类自定义的 jsonSerialize 函数进行encode,否则调用 json_encode_array 进行处理

具体细节可以参考 中文版 <[http://www.laruence.com/2011/10/10/2204.html](http://www.laruence.com/2011/10/10/2204.html)>* 或者 英文版 <[http://schlueters.de/blog/archives/135-Jason,-let-me-help-you!.html](http://schlueters.de/blog/archives/135-Jason,-let-me-help-you!.html)>* 里关于 JsonSerializable接口 的介绍,这个是5.4才加入的新功能

再从源码角度看看是如何调用对象自定义的 jsonSerialize 函数的

static void json_encode_serializable_object(smart_str* buf, zval* val, int options TSRMLS_DC)
    {   
      zend_class_entry* ce = Z_OBJCE_P(val);
      zval * retval = NULL, fname;   
      HashTable* myht;
 
      if (Z_TYPE_P(val) == IS_ARRAY) {
          myht = HASH_OF(val);
      } else {
          myht = Z_OBJPROP_P(val);      
      }
 
      if (myht && myht->nApplyCount > 1) {
          JSON_G(error_code) = PHP_JSON_ERROR_RECURSION;
          smart_str_appendl(buf, "null", 4);
          return;
      }
 
      ZVAL_STRING(&fname, "jsonSerialize", 0);
 
      //调用 jsonSerialize 函数 
      if (FAILURE == call_user_function_ex(EG(function_table), &val, &fname, &retval, 0, NULL, 1, NULL TSRMLS_CC) || !retval) {
          zend_throw_exception_ex(NULL, 0 TSRMLS_CC, "Failed calling %s::jsonSerialize()", ce->name);
          smart_str_appendl(buf, "null", sizeof("null") - 1);
          return;
      }
 
      if (EG(exception)) {//调用中是否抛异常
          // Error already raised  
          zval_ptr_dtor(&retval);       
          smart_str_appendl(buf, "null", sizeof("null") - 1);
          return;
      }
 
      if ((Z_TYPE_P(retval) == IS_OBJECT) &&
          (Z_OBJ_HANDLE_P(retval) == Z_OBJ_HANDLE_P(val))) { //如果返回的是一个对象,同时返回结果还是要encode的对象,则直接调用 json_encode_array 进行处理
          // Handle the case where jsonSerialize does: return $this; by going straight to encode array
          json_encode_array(buf, &retval, options TSRMLS_CC);
      } else {
          // All other types, encode as normal
          php_json_encode(buf, retval, options TSRMLS_CC);//否则继续调用 php_json_encode 递归处理
      }
 
      zval_ptr_dtor(&retval);
    }

3.2.4 对数组的处理

我们再来看一下如何对数组进行encode

之前也说过了对 ARRAY 这种数组类型的,则会调用 json_encode_array 进行处理,在 json_encode_array 里会遍历数组,先插入 array 的key,然后再插入value(其实是通过调用php_json_encode,相互递归调用)

具体实现上有两个步骤

首先判断一下是以对象形式(k1:v1, k2:v2…)输出,还是以数组形式输出(k1,k2…)

如果val的类型非数组,则以对象形式输出
否则如果val的类型是数组,看一下 options 参数是否有设置强制以对象形式输出
如果没有的话,则要看val的内容判断了,通过 json_determine_array_type 函数进行判断,判断val是否是关联性数组
遍历数组,根据以对象形式输出还是数组形式输出进行处理

以数组形式输出,插入分隔符 , ,然后调用 php_json_encode 插入value
以对象形式输出,插入key(如果key非字符串,则强制转换为整数( 如果key非字符串,非整数,那么呵呵 ),再转换为字符串插入),然后调用 php_json_encode 插入value

static void json_encode_array(smart_str* buf, zval** val, int options TSRMLS_DC)
    {
        //判断是以对象形式(k1:v1, k2:v2...)输出,还是以数组形式输出(k1,k2...)
        if (Z_TYPE_PP(val) == IS_ARRAY) {
            r = (options & PHP_JSON_FORCE_OBJECT) ? PHP_JSON_OUTPUT_OBJECT : json_determine_array_type(val TSRMLS_CC);
        } else {
            r = PHP_JSON_OUTPUT_OBJECT;
        }
        //..........................
        //循环处理每个key && value
        zend_hash_internal_pointer_reset_ex(myht, &pos);
        for (;; zend_hash_move_forward_ex(myht, &pos)) {
            i = zend_hash_get_current_key_ex(myht, &key, &key_len, &index, 0, &pos);
            if (i == HASH_KEY_NON_EXISTENT)
                break;
 
            if (zend_hash_get_current_data_ex(myht, (void ** ) &data, &pos) == SUCCESS) {
                if (r == PHP_JSON_OUTPUT_ARRAY) { //以数组形式输出,不需要插入key,直接调用php_json_encode插入value
                    //..................
                    php_json_encode(buf, * data, options TSRMLS_CC); //数组形式,没有key(其实是0,1,2这种数字),则直接调用 php_json_encode插入value
                } else if (r == PHP_JSON_OUTPUT_OBJECT) { //以对象形式输出,先插入key(对key是否为string做一些处理),然后调用php_json_encode插入value
                    if (i == HASH_KEY_IS_STRING) { //如果key为字符串类型的,则直接插入
                        //..................
                        json_escape_string(buf, key, key_len - 1, options & ~PHP_JSON_NUMERIC_CHECK TSRMLS_CC);//插入key
                        //..................
                        php_json_encode(buf, * data, options TSRMLS_CC);//调用php_json_encode插入value
                    } else {
                        //..................
                        smart_str_append_long(buf, (long) index);//把key转换为long,再转换为string类型的,然后插入,但是如果key非字符串,非整数,那么呵呵
                        //..................
                        php_json_encode(buf, * data, options TSRMLS_CC);//调用php_json_encode插入value
                    }
                }
            }
        }
        //..................
    }

记得以前有一个题目,就是给定一个array,从php代码上怎么判断是关联型的还是非关联型的
http://stackoverflow.com/questions/173400/how-to-check-if-php-array-is-associative-or-sequential
再来看一下php内部是怎么判断一个数组是关联型的还是非关联型的
做法其实也是一样的,遍历每个key,判断是否等于对应的下标

static int json_determine_array_type(zval** val TSRMLS_DC)
{
    int i;
    HashTable* myht = HASH_OF(* val);
 
    i = myht ? zend_hash_num_elements(myht) : 0;
    if (i > 0) {
        char* key;
        ulong index, idx;
        uint key_len;
        HashPosition pos;
 
        zend_hash_internal_pointer_reset_ex(myht, &pos);
        idx = 0;
        for (;; zend_hash_move_forward_ex(myht, &pos)) {
            i = zend_hash_get_current_key_ex(myht, &key, &key_len, &index, 0, &pos);
            if (i == HASH_KEY_NON_EXISTENT) {
                break;
            }
 
            if (i == HASH_KEY_IS_STRING) {
                return 1;
            } else {
                if (index != idx) {
                    return 1;
                }
            }
            idx++;
        }
    }
 
    return PHP_JSON_OUTPUT_ARRAY;
}

4 json_decode

4.1 Description

函数的原型如下,有两个可选参数

mixed json_decode ( string json [, boolassoc = false [, int depth = 512 [, intoptions = 0 ]]] )
json : 这个就是要进行decode的json字符串
assoc : 当该参数为 true 时,将返回 array 而非 object
depth : 递归处理的层数
options : 一些特殊需求,目前只有一个
JSON_BIGINT_AS_STRING : 当value是大整数的时候,如果设置了这个选项,则> value的类型是字符串,避免精度缺失,默认是浮点数
json = '12345678901234567890'; var_dump(json_decode(json));
var_dump(json_decode($json, false, 512, JSON_BIGINT_AS_STRING));
输出为::
float(1.2345678901235E+19)
string(20) "12345678901234567890"

4.2 源码

4.2.1 主体

json_decode主函数,其实就接受四个参数,然后调用 php_json_decode_ex 进行处理

static PHP_FUNCTION(json_decode) { 
        char * str;
        int str_len;
       zend_bool assoc = 0; // return JS objects as PHP objects by default
        long depth = JSON_PARSER_DEFAULT_DEPTH;
        long options = 0; 
        if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|bll", &str,&str_len, &assoc, &depth, &options) == FAILURE) {
              return;
        }
       JSON_G(error_code) = 0;
        if (!str_len) {
              RETURN_NULL();
        }
        // For BC reasons, the bool $assoc overrides the long $options bit for        PHP_JSON_OBJECT_AS_ARRAY
        if (assoc) {
              options ||= PHP_JSON_OBJECT_AS_ARRAY;
        } else {
              options &= ~PHP_JSON_OBJECT_AS_ARRAY;
        }
       php_json_decode_ex(return_value, str, str_len, options, depth TSRMLS_CC);
 }

让我们来看看在 php_json_decode_ex 里都做了什么事情
其实主要包括三个步骤

  1. 判断是否为utf8编码,如果是的话,同时转换为utf16
  2. 先调用 parse_JSON_ex 去解析这个json串,这个是整个解析的核心部分
  3. 如果失败,则尽量而为去解析这个非标准的json串
PHP_JSON_API void php_json_decode_ex(zval * return_value, char * str, int str_len, int options, long depth TSRMLS_DC)
 {
         int utf16_len;
        zval * z;
         unsigned short * utf16;
        JSON_parser jp;
         //step1 判断是否是utf8字符
        utf16 = (unsigned short * ) safe_emalloc((str_len+1), sizeof(unsigned short), 1);
        utf16_len = json_utf8_to_utf16(utf16, str, str_len);
         //...........
        ALLOC_INIT_ZVAL(z);
        jp = new_JSON_parser(depth);
         //step2 进行处理
         if (parse_JSON_ex(jp, z, utf16, utf16_len, options TSRMLS_CC)) {
                 * return_value = * z;
         }
         else //step3 对一些不规范的json串,尽力而为去decode,比如输入的字符串是一个整数,两边没有被 " 包含
         {
                 //去掉左右两边多余的 空格 table 换行 回车
                 //.......
                RETVAL_NULL();
                 //处理NULL or BOOL类型
                 if (trim_len == 4) {
                         if (!strncasecmp(trim, "null", trim_len)) {
                                jp->error_code = PHP_JSON_ERROR_NONE;
                                RETVAL_NULL();
                         } else if (!strncasecmp(trim, "true", trim_len)) {
                                RETVAL_BOOL(1);
                         }
                 } else if (trim_len == 5 && !strncasecmp(trim, "false", trim_len)) {
                        RETVAL_BOOL(0);
                 }
                 //处理数字
                 if ((type = is_numeric_string_ex(trim, trim_len, &p, &d, 0, &overflow_info)) != 0) {
                         if (type == IS_LONG) {
                                RETVAL_LONG(p); //返回整数
                         } else if (type == IS_DOUBLE) { //返回浮点数
                                 //.......
                         }
                 }
                 //返回错误
                 if (Z_TYPE_P(return_value) != IS_NULL) {
                        jp->error_code = PHP_JSON_ERROR_NONE;
                 }
                zval_dtor(z);
         }
        FREE_ZVAL(z);
        efree(utf16);
        JSON_G(error_code) = jp->error_code;
        free_JSON_parser(jp);
 }

4.2.2 json解析器

来看看php是如何解析json串的

首先来看看解析的时候,需要用到的这个结构体

  1. typedef struct JSON_parser_struct {
  2. int state; //当前的状态
  3. int depth; //最大的深度
  4. int top; //当前处于第几层
  5. int error_code; //错误码
  6. int* stack; //记录每一层正在解析的状态,取值有MODE_ARRAY, MODE_DONE, MODE_KEY, MODE_OBJECT
  7. zval ** the_zstack; //记录每一层正在decode中的值的stack,如果 depth 小于 JSON_PARSER_DEFAULT_DEPTH,则用 the_static_zstack,否则重新申请内存
  8. zval * the_static_zstack[JSON_PARSER_DEFAULT_DEPTH]; // 默认使用的stack
  9. } * JSON_parser;

主要是通过状态机来处理的

int parse_JSON_ex(JSON_parser jp, zval * z, unsigned short utf16_json[], int length, int options TSRMLS_DC)
 {
     //...............
     for (the_index = 0; the_index < length; the_index += 1) {
         next_char = utf16_json[the_index];
         //....................
 
         next_state = state_transition_table[jp->state][next_class];
 
         if (next_state >= 0) {
             //...............
             //主要是获取每个key or value的值,存储在buf变量里
             jp->state = next_state;
         } else {
             //主要是处理json里的特殊标志
             // { or [ : 一个层次的开始标识
             // } or ] : 一个层次的结束标志
             // " :一个字符串取值的开始或者结束标志
             // : : 一个value的取值开始标志,当然这个value 可以是任何类型的
             // , : 另外一个k/v or k的开始标志
 
             switch (next_state) {
             case -9: // empty }
                 if (!pop(jp, MODE_KEY)) {
                     FREE_BUFFERS();
                     return false;
                 }
                 jp->state = OK;
                 break;
             case -8: // } 一个层次的结束标志
                 //...................
                 jp->state = OK;
                 break;
             case -7: // ]
             {
                 //...................
                 jp->state = OK;
             }
             break;
             case -6: // {
                 //...................
                 break;
             case -5: // [
                 //...................
                 break;
             case -4: // "
                 //...................
                 break;
             case -3: // ,
                 //...................
             break;
             case -2: // :
                 //...................
             default: //  syntax error
                 {
                     jp->error_code = PHP_JSON_ERROR_SYNTAX;
                     FREE_BUFFERS();
                     return false;
                 }
             }
         }
     }
 
     FREE_BUFFERS();
     if (jp->state == OK && pop(jp, MODE_DONE)) {
         return true;
     }
 
     jp->error_code = PHP_JSON_ERROR_SYNTAX;
     return false;
 }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容