终于可以接触 XML 了,虽然大多数公司的服务器返回给客户端的数据格式是 JSON,万一是 XML 呢?

XML 初识

XML 的全称是 Extensible Markup Language,译作『可扩展标记语言』,跟 JSON 一样,也是常用的一种用于交互的数据格式,一般也叫 XML文档(XML Document)。举个例子:

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

XML 语法

一个常见的 XML文档一般由 文档声明、元素(Element)、属性(Attribute) 组成。

1. 文档声明

在 XML 文档的最前面,必须 编写一个文档声明,用来声明 XML 文档的类型。

  • 最简单的声明:
1
<?xml version="1.0"?>
  • 用 encoding 属性说明文档的字符编码:
1
<?xml version="1.0" encoding="UTF-8"?>

2. 元素(Element)

一个元素包括了开始标签和结束标签:

  • 拥有内容的元素:<video>小黄人</video>
  • 没有内容的元素:<video></video>
  • 没有内容的元素简写:<video/>

一个元素可以嵌套若干个子元素(不能出现交叉嵌套),如下:

1
2
3
4
5
6
<videos>
<video>
<name>小黄人 第01部</name>
<length>30</length>
</video>
</videos>

规范的 XML文档最多只有 1 个根元素,其他元素都是根元素的子孙元素。

注意:XML 中的所有空格和换行,都会当做具体内容处理,比如下面两个元素的内容是不一样的:

1
2
3
4
5
6
7
// 第 1 个
<video>小黄人</video>
// 第 2 个
<video>
小黄人
</video>

3. 属性(Attribute)

一个元素可以拥有多个属性,属性值 必须用 双引号”” 或者 单引号’‘ 括住:

1
2
3
4
5
6
7
8
// 元素 video 拥有 name 和 length 两个属性
<video name="小黄人 第01部" length="30" />
// 实际上,属性表示的信息也可以用子元素来表示,比如:
<video>
<name>小黄人 第01部</name>
<length>30</length>
</video>

XML 解析方案

要想从 XML 中提取有用的信息,必须得学会解析 XML,比如:

  • 提取 name 元素里面的内容:<name>小黄人 第01部</name>
  • 提取 video 元素中 name 和 length 属性的值:<video name="小黄人 第01部" length="30" />

XML 的解析方式有 2 种:

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

在 iOS 中,XML 解析的方法有几种:

  • Apple 原生提供的 NSXMLParser:SAX 方式解析 XML,使用简单。
  • 第三方框架 libxml2:纯 C语言,默认包含在 iOS SDK 中,同时支持 DOM 和 SAX 方式解析 XML。
  • 第三方框架 GDataXML:DOM 方式解析,由 Google 开发,基于 libxml2 封装的 OC 版本。

所以,XML 解析方案选择建议:

  • 如果希望用 DOM 方式来解析 XML,可以使用 GDataXML;如果希望用 SAX 方式来解析 XML,可以使用 NSXMLParser。
  • 大文件:NSXMLParser、libxml2;小文件:以上 3 个都可以。

使用 NSXMLParser 解析

例子请求下来的 XML 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<videos>
<video id="1" name="小黄人 第01部" length="10" image="resources/images/minion_01.png" url="resources/videos/minion_01.mp4"/>
<video id="2" name="小黄人 第02部" length="12" image="resources/images/minion_02.png" url="resources/videos/minion_02.mp4"/>
<video id="3" name="小黄人 第03部" length="14" image="resources/images/minion_03.png" url="resources/videos/minion_03.mp4"/>
<video id="4" name="小黄人 第04部" length="16" image="resources/images/minion_04.png" url="resources/videos/minion_04.mp4"/>
<video id="5" name="小黄人 第05部" length="18" image="resources/images/minion_05.png" url="resources/videos/minion_05.mp4"/>
<video id="6" name="小黄人 第06部" length="20" image="resources/images/minion_06.png" url="resources/videos/minion_06.mp4"/>
<video id="7" name="小黄人 第07部" length="22" image="resources/images/minion_07.png" url="resources/videos/minion_07.mp4"/>
<video id="8" name="小黄人 第08部" length="24" image="resources/images/minion_08.png" url="resources/videos/minion_08.mp4"/>
<video id="9" name="小黄人 第09部" length="26" image="resources/images/minion_09.png" url="resources/videos/minion_09.mp4"/>
<video id="10" name="小黄人 第10部" length="28" image="resources/images/minion_10.png" url="resources/videos/minion_10.mp4"/>
<video id="11" name="小黄人 第11部" length="30" image="resources/images/minion_11.png" url="resources/videos/minion_11.mp4"/>
<video id="12" name="小黄人 第12部" length="32" image="resources/images/minion_12.png" url="resources/videos/minion_12.mp4"/>
<video id="13" name="小黄人 第13部" length="34" image="resources/images/minion_13.png" url="resources/videos/minion_13.mp4"/>
<video id="14" name="小黄人 第14部" length="36" image="resources/images/minion_14.png" url="resources/videos/minion_14.mp4"/>
<video id="15" name="小黄人 第15部" length="38" image="resources/images/minion_15.png" url="resources/videos/minion_15.mp4"/>
<video id="16" name="小黄人 第16部" length="40" image="resources/images/minion_16.png" url="resources/videos/minion_16.mp4"/>
</videos>

模型就暂时先用最简单的 KVC 来转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
ZLVideo.h
#import <Foundation/Foundation.h>
@interface ZLVideo : NSObject
/** id,用 XML 解析的时候,这里用 NSUInteger 会报错找不到方法 */
@property (assign, nonatomic) NSInteger id;
/** image */
@property (strong, nonatomic) NSString *image;
/** length,用 XML 解析的时候,这里用 NSUInteger 会报错找不到方法 */
@property (assign, nonatomic) NSInteger length;
/** name */
@property (strong, nonatomic) NSString *name;
/** url */
@property (strong, nonatomic) NSString *url;
// dict -> model
+ (instancetype)videoWithDict:(NSDictionary *)dict;
@end
//"id": 1,
//"image": "resources/images/minion_01.png",
//"length": 10,
//"name": "小黄人 第01部",
//"url": "resources/videos/minion_01.mp4"
ZLVideo.m
#import "ZLVideo.h"
@implementation ZLVideo
+ (instancetype)videoWithDict:(NSDictionary *)dict {
ZLVideo *video = [[self alloc] init];
[video setValuesForKeysWithDictionary:dict];
return video;
}
@end

然后,用起来可能会比较麻烦,也有可能很容易 crash 没有处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#import "ViewController.h"
#import "ZLVideo.h"
@interface ViewController ()<NSXMLParserDelegate>
// 所有视频数据(每个元素为 ZLVideo)
@property (strong, nonatomic) NSMutableArray<ZLVideo *> *videos;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.videos = [NSMutableArray array];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/video?type=XML"]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"str = \n%@", str);
// 创建一个 XML 解析器
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
// 通过代理来取得解析的数据
parser.delegate = self;
// 开始解析(阻塞式,解析没有完毕之前不会往下执行)
BOOL success = [parser parse];
if (success) {
NSLog(@"解析成功~");
} else {
NSLog(@"解析失败~");
}
// 解析完毕之后刷新数据
[self.tableView reloadData];
}];
}
#pragma mark - <UITableViewDataSource>
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.videos.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *kReusableCellIdentifier = @"VideoCell"; // 已经在 StoryBoard 注册好
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kReusableCellIdentifier forIndexPath:indexPath];
ZLVideo *video = self.videos[indexPath.row];
cell.textLabel.text = video.name;
// 注意 id 和 time 都是 NSNumber 对象
cell.detailTextLabel.text = [NSString stringWithFormat:@"id = %zd, time = %zd", video.id, video.length];
return cell;
}
#pragma mark - <NSXMLParserDelegate>
// 1. 开始解析一个 XML 文档
- (void)parserDidStartDocument:(NSXMLParser *)parser {
NSLog(@"parserDidStartDocument");
}
// 2. 解析到某个元素的开头,比如:<video>
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary<NSString *,NSString *> *)attributeDict {
NSLog(@"didStartElement:%@, %@", elementName, attributeDict);
// 如果是根节点,不用解析,我只需要 video 子节点
if ([elementName isEqualToString:@"videos"]) {
return;
}
ZLVideo *video = [ZLVideo videoWithDict:attributeDict];
[self.videos addObject:video];
}
// 3. 解析到某个元素的结尾,比如:</video>
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
NSLog(@"didEndElement:%@", elementName);
}
// 4. 成功解析完一个 XML 文档
- (void)parserDidEndDocument:(NSXMLParser *)parser {
NSLog(@"parserDidEndDocument");
}
@end

注意:NSXMLParserDelegate 的第 2 个方法的 attributeDict 就是以下数据,这个时候就可以像解析 JSON 那样来搞了:

1
2
3
4
5
6
7
{
id = 14;
image = "resources/images/minion_14.png";
length = 36;
name = "\U5c0f\U9ec4\U4eba \U7b2c14\U90e8";
url = "resources/videos/minion_14.mp4";
}

使用 GDataXML 解析

先去 GitHub 下载 GDataXML,地址是:https://github.com/google/gdata-objectivec-client/tree/master/Source/XMLSupport,这样我们得到两个文件 GDataXMLNode.h 和 GDataXMLNode.m,把它们拷贝到项目里,发现报错:

导入 GDataXML 错误信息

libxml2 已经默认包含在 iOS SDK 中,所以我们不需要再次导入 libxml2,但是由于它是静态库,所以我们还得做一些额外的处理(上面绿色的注释)。

GDataXML Configuration1

GDataXML Configuration2

以上两个步骤都差不多。配置完毕发现这个 GDataXML 不是 ARC 的:

GDataXML ARC

这就尴尬了,我们可以尝试利用 Xcode 工具来帮我们转换到 ARC:

GDataXML To ARC

发现不行,因为 GDataXML 用的不是纯 OC 语言,含有 C 语言的,所以无法转换。设置编译参数,重新编译,OK。

GDataXML add flag fno-objc-arc

GDataXML 中常用的类

  • GDataXMLDocument:代表整个 XML 文档
  • GDataXMLElement:代表文档中的每个元素,使用 attributeForName: 方法可以获得属性值

使用 GDataXML 来解析 XML 大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#import "ViewController.h"
#import "ZLVideo.h"
#import "GDataXMLNode.h"
@interface ViewController ()
// 所有视频数据(每个元素为 ZLVideo)
@property (strong, nonatomic) NSMutableArray<ZLVideo *> *videos;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.videos = [NSMutableArray array];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/video?type=XML"]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
// 获取整个 XML 文档
GDataXMLDocument *doc = [[GDataXMLDocument alloc] initWithData:data options:0 error:nil];
// 获得根节点下的所有子节点
NSArray *elements = [doc.rootElement elementsForName:@"video"];
// 一个 GDataXMLElement 就对应一个 Video 视频
for (GDataXMLElement *element in elements) {
ZLVideo *video = [[ZLVideo alloc] init];
video.id = [element attributeForName:@"id"].stringValue.integerValue;
video.name = [element attributeForName:@"name"].stringValue;
video.image = [element attributeForName:@"image"].stringValue;
video.length = [element attributeForName:@"length"].stringValue.integerValue;
video.url = [element attributeForName:@"url"].stringValue;
[self.videos addObject:video];
}
[self.tableView reloadData];
}];
}
... ... 省略显示 tableView 部分代码

XML -> Model

上面已经有涉及,一般来说使用第三方框架来转换成 Model。

附录:JSON 和 XML 比较

同一份数据,既可以用 JSON 来表示,也可以用 XML来表示:

JSON 和 XML 比较

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

附:多值参数、字典数组的中文输出

多值参数

就是一个参数有多个值。比如下面的 place 参数对应两个值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 0.请求路径
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/weather?place=Beijing&place=Shanghai"];
// 1.创建请求对象
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// 2.发送请求
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSLog(@"\n%@", [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]);
}];
}
// 打印输出:
{
weathers = (
{
city = Beijing;
status = "\U6674\U8f6c\U591a\U4e91";
},
{
city = Shanghai;
status = "\U6674\U8f6c\U591a\U4e91";
}
);
}

字典数组的中文输出

从打印可以看到,在输出字典含有中文时,是输出 unicode 的,如果我们想查看中文是什么,我们要直接取这个字段。或者写一个分类,重写一些方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#import <Foundation/Foundation.h>
@implementation NSDictionary (Log)
- (NSString *)descriptionWithLocale:(id)locale {
NSMutableString *string = [NSMutableString string];
// 开头有个{
[string appendString:@"{\n"];
// 遍历所有的键值对
[self enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
[string appendFormat:@"\t%@", key];
[string appendString:@" : "];
[string appendFormat:@"%@,\n", obj];
}];
// 结尾有个}
[string appendString:@"}"];
// 查找最后一个逗号
NSRange range = [string rangeOfString:@"," options:NSBackwardsSearch];
if (range.location != NSNotFound)
[string deleteCharactersInRange:range];
return string;
}
@end
@implementation NSArray (Log)
- (NSString *)descriptionWithLocale:(id)locale {
NSMutableString *string = [NSMutableString string];
// 开头有个[
[string appendString:@"[\n"];
// 遍历所有的元素
[self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
[string appendFormat:@"\t%@,\n", obj];
}];
// 结尾有个]
[string appendString:@"]"];
// 查找最后一个逗号
NSRange range = [string rangeOfString:@"," options:NSBackwardsSearch];
if (range.location != NSNotFound)
[string deleteCharactersInRange:range];
return string;
}
@end

只需要写一个 .m 文件,使用的时候也不需要导入任何头文件(这样也导致所有打印 NSDictionary 和 NSArray 都是通过这种方式)。