本文授權轉載:神獸gcc(簡書)
除了XML和Json,文中還涉及到的一些知識:第三方類庫的使用,獲取本地文件內容,網站API使用,GCD多線程編程,不做詳細介紹,在代碼出現的地方會注明。
先安利一波:
Json
XML
大數據時代,我們需要從網絡中獲取海量的新鮮的各種信息,就不免要跟著兩個家伙打交道,這是兩種結構化的數據交換格式。一般來講,我們會從網絡獲取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上找,都有使用方法的說明的。