本文主要介绍了XML和JSON数据解析的基本知识,并展示了NSXMLParser方法、GDataXML第三方库以及NSJSONSerialization方法的实际运用案例。
XML和JSON是目前Web开发中经常用到的标记语言。这两者主要用于Web开发中标记数据结构。以微博为例,每一条微博都有Logo, 作者, 时间, 正文, 转发数, 回复, 点赞数 等项目。这些数据在网络中都是按一定的结构存储的。
在iOS开发中,往往需要将网络中的数据下载到本地,然后按一定的逻辑予以呈现。这个过程就是常说的 解析。
一、语言简介
1.XML
XML是可扩展标记语言(Extensible Markup Language)的缩写,其中的标记(markup)是关键部分。XML语言将文件内容用限定标记进行标记,从而使每个单词、短语或段落成为可识别、可分类的信息。更多内容可参考XML 新手入门基础知识和XML 简介。举个栗子,这个栗子节选自这里。
Empire Burlesque
Bob Dylan
USA
Columbia
10.90
1985
Hide your heart
Bonnie Tyler
UK
CBS Records
9.90
1988
。。。
这是一个CD专辑列表,其中包含很多CD专辑,每张专辑给出了标题, 作者, 国家, 出版方, 价格, 年份 等信息。其中每一个信息块的前后两端都被标记。标题被
和标记为元素属性,专辑被
和标记为子元素,而整张列表被
和标记为根元素。整个XML文件可以是TXT文本文件,可以跨系统读写。
2.JSON
JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式。它基于ECMAScript的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C、C++、C#、Java、JavaScript、Perl、Python等)。这些特性使JSON成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成(网络传输速率)。来自百度百科
{
"book1": {
"type": "textbook",
"pages": "256",
"title": "Programming Pearls 2nd Edition",
"description": "The first edition of Programming Pearls was one of the most influential books I read early in my career...",
"rating": "4.5",
"coverType": "paperback",
"genre": "Computer Science",
"author": "Jon Bentley",
"publisher": "Addison-Wesley Professional",
"copyright": "1999"
},
"book2": {
...
},
...
}
如上例所示,JSON采用 数组 和 键值对 的形式标记数据结构。
3.区别在哪
(1)JSON的效率更高
在这篇文章中,作者对XML和JSON的解析效率进行了测试。结果表明相对XML,JSON的解析速度提高了30%,占用空间少30%。
(2)XML有更多的解析方式
XML目前设计了两种解析方式:
DOM(Document Object Model文档对象模型)方式。解析时需要将XML文件整体读入,并且将XML结构化成树状,使用时再通过树状结构读取相关数据,查找特定节点,然后对节点进行读或写。该方式把一个数据交换格式XML看成一个DOM对象,需要把XML文件整个读入内存,这一点上JSON和XML的原理是一样的,但是XML要考虑父节点和子节点,这一点上JSON的解析难度要小很多,因为JSON构建于两种结构:key/value,键值对的集合;值的有序集合,可理解为数组;
SAX(Simple API for XML)方式。基于事件驱动的解析方式,逐行解析数据。这一方式不需要整个读入文档就可以对解析出的内容进行处理,是一种逐步解析的方法。程序也可以随时终止解析。这样,一个大的文档就可以逐步的、一点一点的展现出来,所以SAX适合于大规模的解析。这一点,JSON目前是做不到得。
总体而言:JSON只提供整体解析方案,而这种方法只在解析较少的数据时才能起到良好的效果;XML提供了对大规模数据的逐步解析方案,这种方案很适合于对大量数据的处理。
二、在iOS中的解析
1.XML解析方式简介
iOS中苹果官方提供了NSXMLParser和libxml2两种XML解析方式,同时也有第三方库TBXML、TouchXML、KissXML、TinyXML、GDataXML可以执行XML解析。
其中NSXMLParser采用SAX方式解析;libxml2为基于C语言API的开源库,可以提供DOM和SAX两种解析方式,但使用比NSXMLParser麻烦。TBXML、TouchXML、KissXML、TinyXML、GDataXML等第三方库均采用DOM方式。GDataXML由Google基于libxml2重新封装得到。大神Ray Wenderlich在文章XML Tutorial for iOS: How To Choose The Best XML Parser for Your iPhone Project中描述了对各个方法的测试。作者总结认为:对于读取小型XML文档,TouchXML, KissXML, GDataXML足矣;如果是读写小型XML文档,KissXML和GDataXML都不错;对于大型XML文档,libxml2 SAX, TBXML, libxml2 DOM更好。
尽管NSXMLParser表现逊于libxml2 SAX,但胜在方便,无需调用第三方库。根据原文的推荐,加上手边现有的资料,最后我决定学习NSXMLParser和GDataXML两种方式。
(1)NSXMLParser解析的代码实现
任务目标:(下同)采用NSXMLParser方法解析前文XML样例,并将得到的CD信息在UITableView中列出来。
主控制器头文件,注意声明代理
#import
@interface CDListTableViewController : UITableViewController
@property (strong, nonatomic) NSMutableArray *dataSource;//存放解析得到的数据
@property (strong, nonatomic) NSString *startTag;
@end
根据要获得的CD信息,自定义了CD类
#import
@interface CD : NSObject
@property (strong, nonatomic) NSString *title;
@property (strong, nonatomic) NSString *artist;
@property (strong, nonatomic) NSString *country;
@property (strong, nonatomic) NSString *company;
@property (strong, nonatomic) NSString *price;
@property (strong, nonatomic) NSString *year;
@end
@implementation CD
@synthesize title,artist,country,company,price,year;
@end
主控制器的实现文件
#import "CDListTableViewController.h"
#import "CD.h"
@implementation CDListTableViewController
- (void)viewDidLoad {
[super viewDidLoad];
UITableView *tableView = (UITableView *)[self.view viewWithTag:1];
UIEdgeInsets contentInset = tableView.contentInset;
contentInset.top = 20;
tableView.contentInset = contentInset;
tableView.delegate = self;
tableView.dataSource = self;
self.dataSource = [[NSMutableArray array]init];
NSXMLParser *aParser = [[NSXMLParser alloc]initWithContentsOfURL:[NSURL URLWithString:@"http://www.w3school.com.cn/example/xmle/cd_catalog.xml"]];
aParser.delegate = self;
[aParser parse];//开始解析
aParser = nil;//释放内存
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - XML Parse
- (void)parserDidStartDocument:(NSXMLParser *)parser {
//开始解析整个文档时调用
NSLog(@"-START-");
}
- (void)parserDidEndDocument:(NSXMLParser *)parser {
//结束解析整个文档时调用
NSLog(@"-END-");
}
- (void)parser:(NSXMLParser *)parser didStartElement:(nonnull NSString *)elementName namespaceURI:(nullable NSString *)namespaceURI qualifiedName:(nullable NSString *)qName attributes:(nonnull NSDictionary *)attributeDict {
//当解析遇到开始标签时调用
NSLog(@"Did-START");
self.startTag = elementName;
if ([elementName isEqual:@"CD"]) {
CD *newCD = [[CD alloc]init];
[self.dataSource addObject:newCD];
NSLog(@"self.dataSource has %lx Objects",[self.dataSource count]);
}
}
- (void)parser:(NSXMLParser *)parser didEndElement:(nonnull NSString *)elementName namespaceURI:(nullable NSString *)namespaceURI qualifiedName:(nullable NSString *)qName attributes:(nonnull NSDictionary *)attributeDict {
//当解析遇到结束标签时调用
NSLog(@"Did-END");
self.startTag = nil;
}
- (void)parser:(NSXMLParser *)parser foundCharacters:(nonnull NSString *)string {
//为新建的CD类实例添加信息
NSLog(@"FOUND %@",string);
CD *currentCD = [self.dataSource lastObject];
if ([self.startTag isEqualToString:@"TITLE"]) {
currentCD.title = string;
}else if ([self.startTag isEqualToString:@"ARTIST"]){
currentCD.artist = string;
}else if ([self.startTag isEqualToString:@"COUNTRY"]){
currentCD.country = string;
}else if ([self.startTag isEqualToString:@"COMPANY"]){
currentCD.company = string;
}else if ([self.startTag isEqualToString:@"PRICE"]){
currentCD.price = string;
}else if ([self.startTag isEqualToString:@"YEAR"]){
currentCD.year = string;
}
self.startTag = nil;
}
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.dataSource count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CDCellID" forIndexPath:indexPath];
if (cell == nil) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"CDCellID"];
}
CD *currentCD = [[CD alloc]init];
currentCD = [self.dataSource objectAtIndex:indexPath.row];
cell.textLabel.text = currentCD.title;
cell.detailTextLabel.text = currentCD.artist;
//这里只提取了两组数据,如要显示更多信息请自定义cell
return cell;
}
@end
(2)GDataXML解析的代码实现
准备工作
- 首先从这里得到GDataXMLNode.h和GDataXMLNode.m文件,并导入到当前工程中。
-
其次,因为GDataXML是基于libxml2封装得到,因此还要导入libxml2库文件:在Linked Frameworks and Libraries点击加号然后搜索libxml2,双击文件即可导入。
- 接下来,在Build Settings中搜索“Header Search Paths”,将其值设置为 ${SDK_DIR}/usr/include/libxml2。否则会收到报错:
libxml/tree.h not found
。 -
最后一步,要将BuildSettings中的Objective-C Automatic Reference Counting设置为NO。否则会收到有关ARC的报错。
开始工作
刚才关闭了ARC,注意做好代码的内存管理工作。
#import "CDListTableViewController.h"
#import "GDataXMLNode.h" //导入第三方库
#import "CD.h"
@implementation CDListTableViewController
- (void)viewDidLoad {
[super viewDidLoad];
UITableView *tableView = (UITableView *)[self.view viewWithTag:1];
UIEdgeInsets contentInset = tableView.contentInset;
contentInset.top = 20;
tableView.contentInset = contentInset;
tableView.delegate = self;
tableView.dataSource = self;
self.dataSource = [[NSMutableArray array]init];
//导入整个XML文件
GDataXMLDocument *aDocument = [[GDataXMLDocument alloc]initWithXMLString:[NSString stringWithContentsOfURL:[NSURL URLWithString:@"http://www.w3school.com.cn/example/xmle/cd_catalog.xml"] encoding:NSUTF8StringEncoding error:nil] options:0 error:nil];
//标记根元素和子元素
GDataXMLElement *rootElement = [aDocument rootElement];
NSArray *subElement = [rootElement elementsForName:@"CD"];
//读取子元素
for (GDataXMLElement *anElement in subElement) {
CD *newCD = [[CD alloc]init];
newCD.title = [[[anElement elementsForName:@"TITLE"] firstObject] stringValue];
newCD.artist = [[[anElement elementsForName:@"ARTIST"] firstObject] stringValue];
newCD.country = [[[anElement elementsForName:@"COUNTRY"] firstObject] stringValue];
newCD.company = [[[anElement elementsForName:@"COMPANY"] firstObject] stringValue];
newCD.price = [[[anElement elementsForName:@"PRICE"] firstObject] stringValue];
newCD.year = [[[anElement elementsForName:@"YEAR"] firstObject] stringValue];
[self.dataSource addObject:newCD];
[newCD release];
}
}
/////////////////////////////////////////////
//其余代码与上例类似,只要注意做好内存管理工作即可///
/////////////////////////////////////////////
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.dataSource count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CDCellID" forIndexPath:indexPath];
if (cell == nil) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"CDCellID"];
}
CD *currentCD = [[CD alloc]init];
currentCD = [self.dataSource objectAtIndex:indexPath.row];
cell.textLabel.text = currentCD.title;
cell.detailTextLabel.text = currentCD.artist;
[currentCD release];
return [cell autorelease];
}
@end
更多有关GDataXML解析的代码实现,可以参考XML Tutorial for iOS: How To Read and Write XML Documents with GDataXML。
2.JSON解析方式简介
苹果原生提供NSJSONSerialization解析方式,也有第三方选择:JSONKit,SBJSON,TouchJSON。根据iOS中四种JSON解析效率比较一文,原生的NSJSONSerialization方法是最佳选择,JSONKit是次优选择。
NSJSONSerialization解析的代码实现
任务目标:利用娱乐花边API实现NSJSONSerialization解析。
目标分析:
JSON文档有两种结构: 对象:以“{“开始,以”}”结束,是“名称/值”对儿的集合。名称和值中间用“:”隔开。多个“名称/值”对之间用“,”隔开。类似Objective-C的NSDictionary。 数组:以“["开始,以“]”结束,中间是数据。数据以“,”分割。类似Objective-C的NSArray。不同的JSON结构有不同的转化方式。
JSON格式与Objective-C转化对照表
JSON | Objective-C |
---|---|
大括号{} | NSDictionary |
中括号[] | NSArray |
双引号 "" | NSString |
数字{} | NSNumber |
该API返回的JSON格式数据如下所示。
{
"0": {
"time": "2015-07-21 19:51",
"title": "太忙找不到好男人?你要学学Angelababy",
"description": "太忙找不到好男人?你要学学Angelababy...",
"picUrl": "http://img1.gtimg.com/ent/pics/hv1/33/0/1885/122572158_small.png",
"url": "http://ent.qq.com/a/20150721/049132.htm"
},
"1": {
"time": "2015-07-21 19:13",
"title": "刘昊然晒中戏录取通知书 意外暴露接地气本名",
"description": "刘昊然晒中戏录取通知书 意外暴露接地气本名...",
"picUrl": "http://img1.gtimg.com/ent/pics/hv1/187/252/1884/122571547_small.jpg",
"url": "http://ent.qq.com/a/20150721/048494.htm"
},
。。。
"code": 200,
"msg": "ok"
}
考察本例,返回对象是一个对象的两级嵌套,转换为Objective-C应该是两级字典。
代码实现
针对返回数据,新建了item类。简单起见,就只提取返回数据的标题和URL两个属性。
#import
@interface item : NSObject
@property (strong, nonatomic) NSString *title;
@property (strong, nonatomic) NSString *url;
@end
@implementation item
@synthesize title,url;
@end
向主控制器代码中导入item.h文件。这代码关闭了ARC,因此有手动管理内存代码。
#import
#import "item.h"
@interface ListTableViewController : UITableViewController
@property (strong, nonatomic) NSMutableArray *dataSource;
@end
@implementation ListTableViewController
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)viewDidLoad {
[super viewDidLoad];
UITableView *tableView = (UITableView *)[self.view viewWithTag:1];
UIEdgeInsets contentInset = tableView.contentInset;
contentInset.top = 20;
tableView.contentInset = contentInset;
tableView.delegate = self;
tableView.dataSource = self;
self.dataSource = [[NSMutableArray array]init];
//API提供了URL和 request: withHttpArg: 方法
NSString *httpUrl = @"http://apis.baidu.com/txapi/huabian/newtop";
NSString *httpArg = @"num=10&page=1";
[self request: httpUrl withHttpArg: httpArg];
}
- (void)request: (NSString*)httpUrl withHttpArg: (NSString*)HttpArg {
NSString *urlStr = [[NSString alloc]initWithFormat: @"%@?%@", httpUrl, HttpArg];
NSURL *url = [NSURL URLWithString: urlStr];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL: url cachePolicy: NSURLRequestUseProtocolCachePolicy timeoutInterval: 10];
[request setHTTPMethod: @"GET"];
//下句中(MY-API-KEY)应为使用者自己的API Key
[request addValue: @" (MY-API-KEY) " forHTTPHeaderField: @"apikey"];
[NSURLConnection sendAsynchronousRequest: request
queue: [NSOperationQueue mainQueue]
completionHandler: ^(NSURLResponse *response, NSData *data, NSError *error){
if (error) {
NSLog(@"Httperror: %@%ld", error.localizedDescription, error.code);
} else {
NSInteger responseCode = [(NSHTTPURLResponse *)response statusCode];
NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"HttpResponseCode:%ld", responseCode);
NSLog(@"HttpResponseBody %@",responseString);
//这句是自己添加的,执行对返回数据的处理
[self reLoadTableViewWith:data];
}
}];
}
- (void)reLoadTableViewWith:(NSData *)data {
//生成一级字典
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
//遍历一级字典
for (int k = 0; k<= [dict count]; k++) {
NSString *count =[NSString stringWithFormat:@"%i",k];
NSDictionary *subDict = dict[count];//生成二级字典
item *newItem = [[item alloc]init];
newItem.title = subDict[@"title"];
newItem.url = subDict[@"url"];
[self.dataSource addObject:newItem];
[newItem release];
}
[self.tableView reloadData];
}
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.dataSource count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"itemCellID" forIndexPath:indexPath];
if (cell == nil) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"itemCellID"];
}
item *currentItem = [[item alloc]init];
currentItem = [self.dataSource objectAtIndex:indexPath.row];
cell.textLabel.text = currentItem.title;
cell.detailTextLabel.text = currentItem.url;
[currentItem release];
return [cell autorelease];
}
@end