iOS开发之数据解析:Json、XML


JSON

  • JSON是一种轻量级的数据格式,一般用于数据交互。
  • 服务器返回给客户端的数据,一般都是JSON格式或者XML格式(文件下载除外)。
  • JSON的格式很像OC中字典数组
{"name" : "jack", "age" : 10}
{"names" : ["jack", "rose", "jim"]}
  • 标准JSON格式的注意点key必须用双引号

  • 要想从JSON中挖掘出具体数据,得对JSON进行解析。

JSON 转换为 OC数据类型

  • JSON – OC 转换对照表
JSON OC
大括号 { } NSDictionary
中括号 [ ] NSArray
双引号" " NSString
数字 10、10.8 NSNumber
  • JSON – OC 转换练习

    json.png

  • JSON 解析方案

  • iOS中,JSON的常见解析方案有4种.
    a、第三方框架:JSONKit、SBJson、TouchJSON(性能从左到右,越差) 。
    b、苹果原生(自带):NSJSONSerialization性能最好)。
  • NSJSONSerialization的常见方法:
  • JSON数据 -->OC对象(字典|数组)[反序列化处理]

+(id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError **)error;


//JSONData - >OC中的对象(字典|数组) [反序列化处理]
-(void)jsonToOC
{
    [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/video?type=JSON"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        //处理服务器返回的数据
        //01 JSONData - > NSString
        NSLog(@"%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
        
        //02 JSONData - >OC中的对象(字典|数组)
        /* 参数说明
         *
         * 第一个参数:要解析的json数据
         * 第二个参数:解析数据时候附加的选项 默认传0
         * NSJSONReadingMutableContainers = (1UL << 0),  得到的对象是可变的
         * NSJSONReadingMutableLeaves = (1UL << 1),      得到的对象中字符串是可变的
         * NSJSONReadingAllowFragments = (1UL << 2)      ! 当返回的对象既不是字典也不是数组的时候(null)
         * 第三个参数:错误信息
         */
        NSDictionary * obj = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
        
        NSLog(@"%@--%@",[obj class],obj);
        
        /*
         [NSNull null]; //空对象
         nil //空
         //NSNull *null = [NSNull null];
         //NSDictionary *dict = @{@"name":@"xiaoma",@"age":[NSNull null]};
         */
    }] resume];
}

  • OC对象(字典|数组) -->JSON数据[序列化处理]

+(NSData *)dataWithJSONObject:(id)obj options:(NSJSONWritingOptions)opt error:(NSError **)error;


//OC中的对象(字典|数组) - >JSONData[序列化处理]
-(void)ocToJSON
{
    //注意:并不是所有的OC对象都支持转换为JSON
    
    //NSDictionary *dict = @{@"name":@"xiaoming",@"age":@25};
    //NSArray *array  = @[@"wen",@"xioamage",@"dangdang"];
    NSString *string = @"我恨你!";
    
    /*
     - 最外层的对象必须是 an NSArray or NSDictionary
     - 字典或数组中所有的元素只能是NSString, NSNumber, NSArray, NSDictionary, or NSNull
     - 字典中所有的key都必须是 NSStrings
     - NSNumbers 是标准的并且不能是无穷大
     */
    if (![NSJSONSerialization isValidJSONObject:string]) {
        NSLog(@"该对象不支持转换");
        return;
    }
    
    /* 参数说明
     *
     * 第一个参数:要序列化的OC对象
     * 第二个参数:选项
     */
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:string options:kNilOptions error:nil];
    
    NSLog(@"%@",[[NSString alloc]initWithData:jsonData encoding:NSUTF8StringEncoding]);
}

  • 将数据写成Json格式的文件

-(void)other
{
    //01 加载数据
    NSArray *array = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"apps.plist" ofType:nil]];
    
    //02 把数据以json的方式来保存
    //写plist文件
    //[array writeToFile:@"/Users/xiaomage/Desktop/123.plist" atomically:YES];
    //写json文件 错误的写法
    //[array writeToFile:@"/Users/xiaomage/Desktop/123.json" atomically:YES];
    //需要先把OC对象转换成jsonData 然后再写文件
    
    //NSJSONWritingPrettyPrinted 排版美化结构
   NSData *jsonData = [NSJSONSerialization dataWithJSONObject:array options:NSJSONWritingPrettyPrinted error:nil];
    
    //03 写文件
    [jsonData writeToFile:@"/Users/nana/Desktop/123.json" atomically:YES];
}

  • Json文件中的数据转换成OC对象
-(void)other2
{
    //01 加载数据
    NSData *jsonData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"123.json" ofType:nil]];
    
    //02 jsonData - >OC对象
    NSArray *array = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:nil];
    
    //03 打印输出
    NSLog(@"%@",array);
}
  • 解析来自服务器的JSON
json01.png

XML

  • 什么是XML

a.全称是Extensible MarkupLanguage,译作“可扩展标记语言”
b.跟JSON一样,也是常用的一种用于交互的数据格式
c.一般也叫XML文档(XML Document)。

  • XML举例:

<videos>
<video name="小黄人 第01部" length="30" />
<video name="小黄人 第02部" length="19" />
<video name="小黄人 第03部" length="33" />
</videos>

  • XML语法:
  • 一个常见的XML文档一般由以下部分组成:
    1.文档声明
    2.元素(Element)
    3.属性(Attribute)
  • 1.XML语法 – 文档声明

1.1、在XML文档的最前面,必须编写一个文档声明,用来声明XML文档的类型
最简单的声明
<?xml version="1.0" ?>
1.2、用encoding属性说明文档的字符编码
<?xml version="1.0" encoding="UTF-8" ?>

  • 2.XML语法 – 元素(Element
  • 2.1、一个元素包括了开始标签和结束标签
    a.拥有内容的元素:<video>小黄人</video>
    b.没有内容的元素:<video></video>
    c.没有内容的元素简写:<video/>
  • 2.2、一个元素可以嵌套若干个子元素(不能出现交叉嵌套
    <videos>
    <video>
    <name>小黄人 第01部</name>
    <length>30</length>
    </video>
    </videos>
  • 2.3、规范的XML文档最多只有1个根元素其他元素都是根元素子孙元素
  • XML语法 –元素的注意:
    XML中的所有空格换行,都会当做具体内容处理。
    下面两个元素的内容是不一样的:
    第1个
    <video>小黄人</video>
    第2个
    <video>
    小黄人
    </video>
  • 3.XML语法 – 属性(Attribute
  • 3.1、 一个元素可以拥有多个属性:
    a.<video name="小黄人 第01部" length="30" />。
    b.video元素拥有namelength两个属性。
    c.属性值必须用 双引号""或者 单引号''括住。
  • 3.2、实际上,属性表示的信息也可以用子元素来表示,比如
    <video>
    <name>小黄人 第01部</name>
    <length>30</length>
    </video>
XML解析
  • 要想从XML中提取有用的信息,必须得学会解析XML

a、提取name元素里面的内容。
<name>小黄人 第01部</name>
b、提取video元素namelength 属性的值。
<video name="小黄人 第01部"length="30" />

  • XML的解析方式有2种

DOM:一次性将整个XML文档加载进内存,比较适合解析小文件
SAX:从根元素开始,按顺序一个元素一个元素往下解析,比较适合解析大文件

  • iOS中的XML解析
  • 在iOS中,解析XML的手段有很多:
    1、苹果原生
    NSXMLParserSAX方式解析,使用简单。
    2、第三方框架
    libxml2:纯C语言,默认包含在iOS SDK中,同时支持DOMSAX方式解析。
    GDataXMLDOM方式解析,由Google开发,基于libxml2
  • XML解析方式的选择建议:
    大文件NSXMLParserlibxml2
    小文件GDataXMLNSXMLParserlibxml2

-NSXMLParser

  • 使用步骤

01.传入XML数据,创建解析器
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
02.设置代理,监听解析过程
parser.delegate = self;
03.开始解析
[parser parse];

  • NSXMLParser采取的是SAX方式解析,特点是事件驱动,下面情况都会通知代理

1、当扫描到文档Document)的开始与结束。
2、当扫描到元素Element)的开始与结束。

  • NSXMLParserDelegate

01、当扫描到文档开始时调用(开始解析
- (void)parserDidStartDocument:(NSXMLParser *)parser;

02、当扫描到文档结束时调用(解析完毕
- (void)parserDidEndDocument:(NSXMLParser *)parser;

03、当扫描到元素开始时调用(attributeDict存放着元素的属性
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict;

04、当扫描到元素结束时调用
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName;

#import "ViewController.h"
#import "UIImageView+WebCache.h"
#import <MediaPlayer/MediaPlayer.h>
#import "XMGVideo.h"
#import "MJExtension.h"

#define KbaseUrlString @"http://120.25.226.186:32812/"

@interface ViewController ()<NSXMLParserDelegate>

@property (nonatomic,strong) NSMutableArray *videos;

@end

@implementation ViewController

-(NSMutableArray *)videos
{
    if (_videos == nil) {
        _videos = [NSMutableArray array];
    }
    return _videos;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    //01 确定请求路径
    NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/video?type=XML"];
    
    //02 创建会话对象
    NSURLSession *session = [NSURLSession sharedSession];
    
    //03 创建TASK 执行Task
    [[session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        //在进行字典转模型之前手动的设置替换 ID - id
        [XMGVideo mj_setupReplacedKeyFromPropertyName:^NSDictionary *{
            
            return @{@"ID":@"id"};
        
        }];
        
        //04 解析服务器返回的数据
        //001 创建XML解析器(NSXMLParser-SAX)
        NSXMLParser *parser = [[NSXMLParser alloc]initWithData:data];
        
        //002 设置代理
        parser.delegate = self;
        
        //003 开始解析 本身是阻塞式的
        [parser parse];
        
        //06 刷新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            
            [self.tableView reloadData];
        
        }];
        
    }] resume];

}

#pragma mark -----------------------
#pragma mark UItableViewDataSource
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.videos.count;
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //01 获得cell
    static NSString *ID = @"video";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
    
    //02 设置cell
    //001 得到该行cell对应的数据
    XMGVideo *videoM = self.videos[indexPath.row];
    //002 设置标题和子标题
    cell.textLabel.text = videoM.name;
    cell.detailTextLabel.text = [NSString stringWithFormat:@"播放时间:%@",videoM.length];
    //003 设置图片
   
    NSString *urlString = [KbaseUrlString stringByAppendingString:videoM.image];
    NSURL *url = [NSURL URLWithString:urlString];
    
    [cell.imageView sd_setImageWithURL:url placeholderImage:[UIImage imageNamed:@"Snip20161125_180"]];
    
    NSLog(@"%@",videoM.ID);
    //03 返回cell
    return cell;
}

#pragma mark -----------------------
#pragma mark UItableViewDelegate
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    //01 得到对应的数据
    XMGVideo *videoM = self.videos[indexPath.row];
    
    NSString *urlString = [KbaseUrlString stringByAppendingString:videoM.url];
    NSURL *url = [NSURL URLWithString:urlString];
    
    //02 创建视频播放控制器
    MPMoviePlayerViewController *vc = [[MPMoviePlayerViewController alloc]initWithContentURL:url];
    
    //03 弹出播放器
    [self presentViewController:vc animated:YES completion:nil];
}

#pragma mark -----------------------
#pragma mark NSXMLParserDelegate

//01 开始解析XML文档的时候调用
-(void)parserDidStartDocument:(NSXMLParser *)parser
{
    NSLog(@"parserDidStartDocument");
}

//02 开始解析XML文档中某个元素的时候调用 该方法会调用多次
-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary<NSString *,NSString *> *)attributeDict
{
    NSLog(@"didStartElement--开始解析的元素名称:%@\n%@",elementName,attributeDict);
    
    //过滤掉根元素
    if ([elementName isEqualToString:@"videos"]) {
        return;
    }
    
    //001 把字典转换为模型
    XMGVideo *video =  [XMGVideo mj_objectWithKeyValues:attributeDict];
    
    //002 把模型添加到全局的可变数组中保存起来
    [self.videos addObject:video];
}

//03 某个元素解析完毕之后会调用该方法
-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
{
    NSLog(@"didEndElement");
}

//04 整个XML文档解析完毕
-(void)parserDidEndDocument:(NSXMLParser *)parser
{
    NSLog(@"parserDidEndDocument");
}
@end
GDataXML
  • GDataXML基于libxml2库,得做以下配置:

1、导入libxml2

xml01.png

2、设置libxml2的头文件搜索路径(为了能找到libxml2库的所有头文件)
a、在Head Search Path中加入/usr/include/libxml2

3、设置链接参数(自动链接libxml2库)
a、在Other Linker Flags中加入-lxml2

4、由于GDataXML非ARC的,因此得设置编译参数

xml02.png

  • GDataXML使用

1、GDataXML中常用的类:
1.1、GDataXMLDocument:代表整个XML文档

1、 把整个XML文档加载进内存
GDataXMLDocument *doc = [[GDataXMLDocument alloc]initWithData:data options:kNilOptions error:nil];

2、GDataXMLElement
2.1、代表文档中的每个元素
2.2、使用attributeForName:方法可以获得属性值

2 先得到根元素,然后获取根元素内部所有名称为video的子元素
GDataXMLElement *rootElement = doc.rootElement;
   NSArray *eles = [rootElement elementsForName:@"video"];
        
        //03 遍历整个子元素数组,然后得到数组中每个元素的内部属性
        for (GDataXMLElement *ele in eles) {
            
            //创建模型
            XMGVideo *video = [[XMGVideo alloc]init];
            
            //得到子元素的属性
            video.name = [ele attributeForName:@"name"].stringValue;
            video.ID =[ele attributeForName:@"id"].stringValue;
            video.length =[ele attributeForName:@"length"].stringValue;
            video.image = [ele attributeForName:@"image"].stringValue;
            video.url = [ele attributeForName:@"url"].stringValue;
            
            //把模型保存到数据源
            [self.videos addObject:video];
        }

示例代码:


#import "ViewController.h"
#import "UIImageView+WebCache.h"
#import <MediaPlayer/MediaPlayer.h>
#import "XMGVideo.h"
#import "MJExtension.h"
#import "GDataXMLNode.h"

#define KbaseUrlString @"http://120.25.226.186:32812/"
@interface ViewController ()
@property (nonatomic,strong) NSMutableArray *videos;
@end

@implementation ViewController

-(NSMutableArray *)videos
{
    if (_videos == nil) {
        _videos = [NSMutableArray array];
    }
    return _videos;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    //01 确定请求路径
    NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/video?type=XML"];
    
    //02 创建会话对象
    NSURLSession *session = [NSURLSession sharedSession];
    
    //03 创建TASK 执行Task
    [[session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        //在进行字典转模型之前手动的设置替换 ID - id
        [XMGVideo mj_setupReplacedKeyFromPropertyName:^NSDictionary *{
            return @{@"ID":@"id"};
        }];
        
        //04 解析服务器返回的数据 XML
        
        //001 把整个XML文档加载进内存
        GDataXMLDocument *doc = [[GDataXMLDocument alloc]initWithData:data options:kNilOptions error:nil];
        
        //002 先得到根元素,然后获取根元素内部所有名称为video的子元素
        GDataXMLElement *rootElement = doc.rootElement;
        
        NSArray *eles = [rootElement elementsForName:@"video"];
        
        //03 遍历整个子元素数组,然后得到数组中每个元素的内部属性
        for (GDataXMLElement *ele in eles) {
            
            //创建模型
            XMGVideo *video = [[XMGVideo alloc]init];
            
            //得到子元素的属性
            video.name = [ele attributeForName:@"name"].stringValue;
            video.ID =[ele attributeForName:@"id"].stringValue;
            video.length =[ele attributeForName:@"length"].stringValue;
            video.image = [ele attributeForName:@"image"].stringValue;
            video.url = [ele attributeForName:@"url"].stringValue;
            
            //把模型保存到数据源
            [self.videos addObject:video];
        }
        
        //06 回到主线程刷新UI
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            [self.tableView reloadData];
        }];
        
    }] resume];

}

#pragma mark -----------------------
#pragma mark UItableViewDataSource
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.videos.count;
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //01 获得cell
    static NSString *ID = @"video";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
    
    //02 设置cell
    //001 得到该行cell对应的数据
    XMGVideo *videoM = self.videos[indexPath.row];
    //002 设置标题和子标题
    cell.textLabel.text = videoM.name;
    cell.detailTextLabel.text = [NSString stringWithFormat:@"播放时间:%@",videoM.length];
    //003 设置图片
   
    NSString *urlString = [KbaseUrlString stringByAppendingString:videoM.image];
    NSURL *url = [NSURL URLWithString:urlString];
    
    [cell.imageView sd_setImageWithURL:url placeholderImage:[UIImage imageNamed:@"Snip20161125_180"]];
    
    NSLog(@"%@",videoM.ID);
    //03 返回cell
    return cell;
}

#pragma mark -----------------------
#pragma mark UItableViewDelegate
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    //01 得到对应的数据
    XMGVideo *videoM = self.videos[indexPath.row];
    
    NSString *urlString = [KbaseUrlString stringByAppendingString:videoM.url];
    NSURL *url = [NSURL URLWithString:urlString];
    
    //02 创建视频播放控制器
    MPMoviePlayerViewController *vc = [[MPMoviePlayerViewController alloc]initWithContentURL:url];
    
    //03 弹出播放器
    [self presentViewController:vc animated:YES completion:nil];
}

@end

JSON和XML比较
  • 同一份数据 ,既可以用JSON 来表示,也可以用XML 来表示。
    解析.png

相比之下,JSON的体积小于XML,所以服务器返回给移动端的数据格式JSON居多。

以上内容,为本人结合相关课程的总结解析的相关的知识点,如有错误敬请批正......

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345