本文授权转载:神兽gcc(简书)
除了XML和Json,文中还涉及到的一些知识:第三方类库的使用,获取本地文件内容,网站API使用,GCD多线程编程,不做详细介绍,在代码出现的地方会注明。
先安利一波:
大数据时代,我们需要从网络中获取海量的新鲜的各种信息,就不免要跟着两个家伙打交道,这是两种结构化的数据交换格式。一般来讲,我们会从网络获取XML或者Json格式的数据,这些数据有着特定的数据结构,必须对其进行解析,得到我们可以处理的数据。所谓“解析”,就是从事先规定好的格式串中提取数据。解析的前提是数据的提供方与获取方提前约定好格式,数据提供方按照格式提供数据,数据获取方按照格式获取数据。
iOS开发中,几乎只要是与网络相关的应用,都离不开对网络数据的解析与应用。现总结几种常用方式来解析网络数据:
Json格式:
NSJSONSerialization,官方提供的Json数据格式解析类,iOS5以后支持
JSONKit(第三方类库)
SBJson
TouchJson
XML格式:
NSXMLParse,官方自带
GDataXML,Google提供的开元XML解析库
按照目前的发展,Json正在逐步取代XML成为网络数据的通用格式,所以我们重点来看Json格式的数据解析先。
准备工作
在看如何使用Json和XML之前,我们还有些事情要做,一是准备我们要解析的数据,二是搭建一个界面来看实际效果,毕竟我们解析了数据就是要在应用中展示出来的。
Json数据准备
关于如何获取网络的数据在这里就不多赘述了,你只需要获得一个从网站为开发者提供的API接口中获得我们想要的url就好了。我这里调用了豆瓣电影的API,随便选了在豆瓣电影首页的电影——《前任2:备胎反击战》,来看看豆瓣对这部电影的描述,由于标签太多,我这里只打算从中获取电影名称,体裁和剧情简介三部分打印出来。
我们可以先提前在浏览器中打开看一下这个待会我们将要得到的东西:
是不是很乱。。。没错,网站返回的东西虽然看上去好像有点规律,但是还是难以辨别,这里不用担心,我们可以使用一个叫做Json校验格式化工具的东西来优化一下它的显示,这里有一个在线的。我们把网站返回给我们的数据copy到这里,点击校验,如果没有什么问题的话,为了方便展示,我把它copy到了Sublime中,我们看一下结果你会发现它变成了下面这个样子,这样看起来就舒服多了,我们也可以非常清楚地看到每一对“Key——Velue”对,以及每个Velue的类型,弄清楚了,待会儿方便我们查询和显示。
找到了目标,下一步我们先做个界面的模子出来,展示我们解析过的数据。大概就是下面这个样子,点击不同的按钮,可以以不同的方式解析获得的数据并在TextView中打印。
界面搭好之后不要忘了关联到代码。
XML数据准备
我们在项目中新建一个xml文件,编写其中的内容,待会儿解析内容并打印到TextView。
XML内容为Person,有几个学生的信息,包括学号,姓名,性别和年龄,一会儿根据这些创建模型。
NSJSONSerialization
接下来就正式开始。苹果官方给出的解析方式是性能最优越的,虽然用起来稍显复杂。
首先我们在上面已经有了我希望得到的信息的网站的API给我们的URL,在OC中,我要加载一个NSURL对象,来向网站提交一个Request。到这里需要特别注意了,iOS9的时代已经来临,我们先前在旧版本中使用的某些类或者方法都已经被苹果官方弃用了。刚刚我们向网站提交了一个Request,在以往,我们是通过NSURLConnection中的sendSynchronousRequest方法来接受网站返回的Response的,但是在iOS9中,它已经不再使用了。从官方文档中,我们追根溯源,找到了它的替代品——NSURLSession类。这个类是iOS7中新的网络接口,苹果力推之,并且现在用它完全替代了NSURLConnection。关于它的具体用法,还是蛮简单的,直接上代码(ViewController.m文件):
#import "ViewController.h" @interface ViewController () @property (retain, nonatomic) IBOutlet UITextView *textView; @property (nonatomic, strong) NSMutableDictionary *dic; @property (nonatomic,strong) NSString *text; @end @implementation ViewController - (IBAction)NSJson:(UIButton *)sender { //GCD异步实现 dispatch_queue_t q1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(q1, ^{ //加载一个NSURL对象 NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://api.douban.com/v2/movie/subject/25881786"]]; //使用NSURLSession获取网络返回的Json并处理 NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error){ //从网络返回了Json数据,我们调用NSJSONSerialization解析它,将JSON数据转换为Foundation对象(这里是一个字典) self.dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil]; NSString *title = [self.dic objectForKey:@"original_title"]; NSMutableArray *genresArray = [self.dic objectForKey:@"genres"]; NSString *genres = [NSString stringWithFormat:@"%@/%@", [genresArray objectAtIndex:0], [genresArray objectAtIndex:1]]; NSString *summary = [self.dic objectForKey:@"summary"]; self.text = [NSString stringWithFormat:@"电影名称:/n%@/n体裁:/n%@/n剧情简介:/n%@", title, genres, summary]; //更新UI操作需要在主线程 dispatch_async(dispatch_get_main_queue(), ^{ self.textView.text = self.text; }); }]; //调用任务 [task resume]; }); }
还是要再提一下,因为涉及到了网络请求,我们在这里用了一点关于使用GCD实现多线程的内容,以后再专门介绍吧。我们运行程序,点击NSJSONSerialization按钮,就看到我们要的内容啦!
SBJson
事实上上面的解析过程还是挺复杂的,主要是牵扯到了NSURLSession的使用。那接下来来看看一些第三方Json解析库的使用。SBJson用起来就简单多了。首先我们去下载这个类库,Github啊,CSDN啊,51啊哪里的任何一个地方都有,很好找。下载下来后导入我们的项目就可以直接运行了。有些第三方类库由于年代久远可能是不支持ARC的,SBJson还好,下面那个JsonKit可就不这么和谐了,这个待会再讲。我们这次点击第二个按钮来实现它。为了以示区分,这次我换了一部电影,来看看《移动迷宫2 Maze Runner: The Scorch Trials》吧!
//上面先导入包: #import "ViewController.h" #import "SBJson.h" //实现: - (IBAction)SBJson:(UIButton *)sender { //GCD异步实现 dispatch_queue_t q1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(q1, ^{ //还是先获取url NSURL *url = [NSURL URLWithString:@"https://api.douban.com/v2/movie/subject/25995508"]; //返回上面url的内容,格式为Json放在了字符串里 NSString *jsonString = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:nil]; //实例化SBJson对象,将Json格式字符串解析,转化为字典。 SBJsonParser *parser = [[SBJsonParser alloc] init]; self.dic = [parser objectWithString:jsonString error:nil]; NSString *title = [self.dic objectForKey:@"original_title"]; NSMutableArray *genresArray = [self.dic objectForKey:@"genres"]; NSString *genres = [NSString stringWithFormat:@"%@/%@", [genresArray objectAtIndex:0], [genresArray objectAtIndex:1]]; NSString *summary = [self.dic objectForKey:@"summary"]; self.text = [NSString stringWithFormat:@"电影名称:/n%@/n体裁:/n%@/n剧情简介:/n%@", title, genres, summary]; //更新UI操作需要在主线程 dispatch_async(dispatch_get_main_queue(), ^{ self.textView.text = self.text; }); }); }
看剧情好像很不错呢,打算去看一下~~
JsonKit
事实上,它虽然不支持ARC,但JsonKit是在性能上仅次于苹果原生解析器的第三方类库。我们在导入它的包以后编译会出现一大堆报错,这时候不用慌,我们会发现大部分是ARC的问题,解决方法也挺简单,我们进入项目的Target,找到Build Phases里面的Compile Sources,接着找我们的问题源头JsonKit.m,双击更改它的Compiler Flags标签为“-fno-objc-arc”,再次编译,就好啦~
//上面先导入包: #import "ViewController.h" #import "JsonKit.h" //实现 - (IBAction)JsonKit:(UIButton *)sender { //GCD异步实现 dispatch_queue_t q1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(q1, ^{ //还是先获取url NSURL *url = [NSURL URLWithString:@"https://api.douban.com/v2/movie/subject/26279433"]; NSString *jsonString = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:nil]; //代码越来越简单了有木有!!就一个方法搞定~ self.dic = [jsonString objectFromJSONStringWithParseOptions:JKParseOptionLooseUnicode]; NSString *title = [self.dic objectForKey:@"original_title"]; NSMutableArray *genresArray = [self.dic objectForKey:@"genres"]; NSString *genres = [NSString stringWithFormat:@"%@/%@", [genresArray objectAtIndex:0], [genresArray objectAtIndex:1]]; NSString *summary = [self.dic objectForKey:@"summary"]; self.text = [NSString stringWithFormat:@"电影名称:/n%@/n体裁:/n%@/n剧情简介:/n%@", title, genres, summary]; //更新UI操作需要在主线程 dispatch_async(dispatch_get_main_queue(), ^{ self.textView.text = self.text; }); }); }
虽然我们只用了一个方法,但是这可不代表JsonKit类库里就只有这一个解析的方法,我们可以去看看它的源码来找寻一番。一般来讲,如果json是“单层”的,即value都是字符串、数字,可以使用objectFromJSONString方法,这个也比较简单。如果json有嵌套,即value里有array、object,如果再使用objectFromJSONString,程序可能会报错,这时我们最好使用objectFromJSONStringWithParseOptions也就是我代码里使用的这个方法,因为电影体裁的Value是数组类型的。
这部电影叫《剩者为王》,好可怕。。。
TouchJson
来看看最后一个:
//导入包: #import "ViewController.h" #import "CJSONSerializer.h" #import "CJSONDeserializer.h" // - (IBAction)TouchJson:(UIButton *)sender { //GCD异步实现 dispatch_queue_t q1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(q1, ^{ //还是先获取url NSURL *url = [NSURL URLWithString:@"https://api.douban.com/v2/movie/subject/22265299"]; NSString *jsonString = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:nil]; //还是一句话的事儿 self.dic = [[CJSONDeserializer deserializer] deserialize:[jsonString dataUsingEncoding:NSUTF8StringEncoding] error:nil]; NSString *title = [self.dic objectForKey:@"original_title"]; NSMutableArray *genresArray = [self.dic objectForKey:@"genres"]; NSString *genres = [NSString stringWithFormat:@"%@/%@", [genresArray objectAtIndex:0], [genresArray objectAtIndex:1]]; NSString *summary = [self.dic objectForKey:@"summary"]; self.text = [NSString stringWithFormat:@"电影名称:/n%@/n体裁:/n%@/n剧情简介:/n%@", title, genres, summary]; //更新UI操作需要在主线程 dispatch_async(dispatch_get_main_queue(), ^{ self.textView.text = self.text; }); }); }
《绝命海拔 Everest》,冒险类电影,本人不是很感冒,据说是根据真实事件改编的。
Json解析总结
呐,上述四种方式已经很清楚了,从代码量上来看,除了那些废话,原生的解析类库是实现起来最复杂的,其他三种倒是挺简单,通过封装,只对外提供一个简单地接口调用就能实现解析功能,性能上都还可以接受。不过从我亲身提回来讲,觉得JsonKit是里面最快的,可能是代码写的不够好,原生的解析方式如果好好优化一下的话应该是性能最好的。在实际的使用过程中选择一种方式就好。
NSXMLParse
关于XML,有两种解析方式,分别是SAX(Simple API for XML,基于事件驱动的解析方式,逐行解析数据,采用协议回调机制)和DOM(Document Object Model ,文档对象模型。解析时需要将XML文件整体读入,并且将XML结构化成树状,使用时再通过树状结构读取相关数据,查找特定节点,然后对节点进行读或写)。苹果官方原生的NSXMLParse类库采用第一种方式,即SAX方式解析XML,它基于事件通知的模式,一边读取文档一边解析数据,不用等待文档全部读入以后再解析,所以如果你正打印解析的数据,而解析过程中间出现了错误,那么在错误节点之间的数据会正常打印,错误后面的数据不会被打印。解析过程由NSXMLParserDelegate协议方法回调。
插句题外话先,我在写这种方式解析XML数据的Demo时折腾了整整一天,说起来都有些不好意思了。程序运行的时候一直出现不能完成解析的情况,各种查各种试,真的是整了整整一天的时间。就在崩溃的边缘的时候,我竟然发现在我自己写XML文件时少写了一个“/。。。瞬间感觉整个世界都崩塌了。所以特地记下来警示自己也顺便给大家提个醒,在这种低级失误上浪费整整一天的时间,要多不值有多不值。谨记,谨记。
我们遵循MVC,首先我们创建模型,新建一个person类,存放XML文件中描述的person属性。再来一个解析XML文件的工具类XMLUtil,我们在里面实现文件的获取,代理方法的实现。
先来看这两个类的代码:
//person.h #import @interface person : NSObject @property (nonatomic, copy) NSString *pid; @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *sex; @property (nonatomic, copy) NSString *age; @end //XMLUtil.h #import #import "person.h" //声明代理 @interface XMLUtil : NSObject//添加属性 @property (nonatomic, strong) NSXMLParser *par; @property (nonatomic, strong) person *person; //存放每个person @property (nonatomic, strong) NSMutableArray *list; //标记当前标签,以索引找到XML文件内容 @property (nonatomic, copy) NSString *currentElement; //声明parse方法,通过它实现解析 -(void)parse; @end //XMLUtil.m #import "XMLUtil.h" @implementation XMLUtil - (instancetype)init{ self = [super init]; if (self) { //获取事先准备好的XML文件 NSBundle *b = [NSBundle mainBundle]; NSString *path = [b pathForResource:@"test" ofType:@".xml"]; NSData *data = [NSData dataWithContentsOfFile:path]; self.par = [[NSXMLParser alloc]initWithData:data]; //添加代理 self.par.delegate = self; //初始化数组,存放解析后的数据 self.list = [NSMutableArray arrayWithCapacity:5]; } return self; } //几个代理方法的实现,是按逻辑上的顺序排列的,但实际调用过程中中间三个可能因为循环等问题乱掉顺序 //开始解析 - (void)parserDidStartDocument:(NSXMLParser *)parser{ NSLog(@"parserDidStartDocument..."); } //准备节点 - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(nullable NSString *)namespaceURI qualifiedName:(nullable NSString *)qName attributes:(NSDictionary *)attributeDict{ self.currentElement = elementName; if ([self.currentElement isEqualToString:@"student"]){ self.person = [[person alloc]init]; } } //获取节点内容 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{ if ([self.currentElement isEqualToString:@"pid"]) { [self.person setPid:string]; }else if ([self.currentElement isEqualToString:@"name"]){ [self.person setName:string]; }else if ([self.currentElement isEqualToString:@"sex"]){ [self.person setSex:string]; }else if ([self.currentElement isEqualToString:@"age"]){ [self.person setAge:string]; } } //解析完一个节点 - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(nullable NSString *)namespaceURI qualifiedName:(nullable NSString *)qName{ if ([elementName isEqualToString:@"student"]) { [self.list addObject:self.person]; } self.currentElement = nil; } //解析结束 - (void)parserDidEndDocument:(NSXMLParser *)parser{ NSLog(@"parserDidEndDocument..."); } //外部调用接口 -(void)parse{ [self.par parse]; } @end
OK,总算是大功告成,如果对代理的使用比较熟悉的话,这部分内容其实还蛮简单的。如果被代码转来转去弄晕了的话可以在每个block的最后都加一个打印输出,做好标记,你就能弄懂程序的执行顺序了。
我们点击NSXMLParse,有了!
GDataXML
来看GDataXML,它是一种DOM方式的解析类库。DOM实现的原理是把整个xml文档一次性读出,放在一个树型结构里。在需要的时候,查找特定节点,然后对节点进行读或写。
在使用之前呢,我们还是先从网上下载GDataXML包,里面两个文件GDataXMLNode.h和GDataXMLNode.m导入到项目中来,编译,发现报错了,这是因为GDataXML是依赖libmxl2的,我们要去项目的Target中做一些设置。
找到项目的Tarfet,进入Build Phases里面的Link Binary With Libraries,点击“加号”,搜索libxml,把出现的包添加进去,这里最新版的XCode7和iOS9中,是libxml.2.2.tbd。
再来到Build Settings,我们可以搜索一下,找到Header Search Paths,添加路径“/usr/include/libxml2”。
再找到Other Link Flags,添加“-libxml2“
还有就是如果你下载的GDataXML是不支持ARC的,那么你就要像上面那样去添加“-fno-objc-arc”,这个视你下载的GDataXML包版本而定。
再次编译,就顺利通过了。
接下来看看我们怎么用这个东西。贴代码之前我真的想说一句,比起苹果原生的类库,这些开源的第三方类库真的在用起来的时候不知道有多舒服,懒人必备啊。在实际的开发中可以为我们节省很多的时间与精力,但是还是要搞懂人家原生的东西,这样才叫学会了么。
//ViewController.m - (IBAction)GDataXML:(id)sender { NSString *path = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"xml"]; NSData *data = [[NSData alloc]initWithContentsOfFile:path]; //对象初始化 GDataXMLDocument *doc = [[GDataXMLDocument alloc]initWithData:data error:nil]; //获取根节点 GDataXMLElement *rootElement = [doc rootElement]; //获取其他节点 NSArray *students = [rootElement elementsForName:@"student"]; //初始化可变数组,用来显示到textView self.GDatatext = [[NSMutableString alloc]initWithString:@""]; for (GDataXMLElement *student in students) { //获取节点属性 GDataXMLElement *pidElement = [[student elementsForName:@"pid"] objectAtIndex:0]; NSString *pid = [pidElement stringValue]; GDataXMLElement *nameElement = [[student elementsForName:@"name"] objectAtIndex:0]; NSString *name = [nameElement stringValue]; GDataXMLElement *sexElement = [[student elementsForName:@"sex"] objectAtIndex:0]; NSString *sex = [sexElement stringValue]; GDataXMLElement *ageElement = [[student elementsForName:@"age"] objectAtIndex:0]; NSString *age = [ageElement stringValue]; //调整一下姿势,添加到可变长字符串~~ NSString *t = [NSString stringWithFormat:@"学号:%@ 姓名:%@ 性别:%@ 年龄:%@/n", pid, name, sex, age]; [self.GDatatext appendString:t]; } self.textView.text = self.GDatatext; }
就一段,是不是看起来非常的舒服呢!
跑一下,跟我们刚才使用的NSXMLParse是不是一样呢?
哈,搞定!
XML解析总结
上述两种解析用到的类库分别代表了两种典型的XML数据解析方式,SAX和DOM,各有优势,比如在应对比较大数据量的XML文件时,后者由于需要先读取整个文档,性能和速度上就必然不及前者了。
其实现在在实际应用中XML已经越来越少了,但是说起iOS中的网络编程,就免不了和XML格式的数据打交道。还有就是,我们在这里仅仅介绍了两种常用的XML解析方式,如同解析Json数据一样,解析XML文件也有很多种方法,除了上述两种,还有比如像TBXML, TouchXML, KissXML, TinyXML等等,具体的使用方法可以去Github上找,都有使用方法的说明的。