取得遠端 API 資料並儲存

這一小節會介紹如何取得遠端 API 的資料,並將資料儲存成本地檔案,以供後續使用。

我們使用 Data.Taipei 臺北市政府資料開放平台 的 API 資料作為示範,這個平台開放的資料有很多項,這邊以取得臺北市臺北旅遊網-住宿資料(中文)臺北市臺北旅遊網-景點資料(中文)作為示範。

進入資料頁後,點擊 使用資料 > API ,如下圖:

taipeitravel03

接著會進入到 API 說明頁,看到下圖標示起來的網址,便是這個 API 的資料:

taipeitravel04

將上圖中標示的網址以瀏覽器開啟後,會發現取得的資料是 JSON 格式,可以使用桌機的 Chrome 瀏覽器的擴充套件 JSONView 來將這個 JSON 資料結構化呈現在畫面中,比較方便檢視內容,如下圖:

taipeitravel05

以上為前置作業的介紹,接著會開始介紹如何在應用程式中獲取遠端 API 資料,並將這資料儲存為本地的一個 JSON 檔案,最後會說明如何解析這個 JSON 檔案並讀取其內資訊。

Hint
  • JSON 是一種輕量級的資料交換格式,以純文字為基礎來儲存與傳送結構資料,可以經由特定的格式儲存任何文字資料(像是字串、數字、陣列或物件), JSON 可以讓你很簡單的與其他程式交換資料。
  • 有些 API 資料會需要事先向擁有者申請 ID ,以便在接收資料前辨別獲取資料者的身分。

首先在 Xcode 裡,新建一個 Single View Application 類型的專案,取名為 ExFetchDataAndStorage 。

一開始先為ViewController建立三個屬性:

class ViewController: UIViewController {
    var taipeiDataUrl :String!
    var documentsPath :String!
    var touringSiteTargetUrl :String!

    // 省略
}

以及在viewDidLoad()中設置儲存檔案的目錄路徑與 API 網址,以供後續使用,如下:

// 應用程式儲存檔案的目錄路徑
let urls = 
  NSFileManager.defaultManager().URLsForDirectory(
  .DocumentDirectory, inDomains: .UserDomainMask)
self.documentsPath = urls[urls.count-1].absoluteString

self.taipeiDataUrl = 
  "http://data.taipei/opendata/datalist"
  + "/apiAccess?scope=resourceAquire&rid="

獲取遠端 API 資料

應用程式中要與遠端交換資料必須使用 NSURLSession 相關函式庫,這邊會介紹兩種方式。

在介紹如何獲取資料之前,請先了解 iOS 9 之後預設為只能載入 https 的網頁(也就是加密過的),要如何設定成可開啟 http 網頁請參考前面章節無法載入 http 的網址的說明。

基本獲取遠端資訊方式

先介紹基本獲取方式,這種方式沒有用到委任模式,會單純的下載遠端檔案下來以供使用,下面將其寫在一個方法中:

func simpleGet(myUrl :String, targetPath :String) {
    if let url = NSURL(string: myUrl) {

      NSURLSession.sharedSession()
        .dataTaskWithURL(url) {
          data, response, error in

            print(
              NSString(
                data: data!, 
                encoding: NSUTF8StringEncoding))

      }.resume()

    }
}

上述程式中,先使用NSURLSession.sharedSession()獲得一個共用的 NSURLSession 實體以供連線,接著帶入要接收資料的網址urldataTaskWithURL()方法,最後帶一個閉包來處理獲得的資料。

請注意到後面還接了一個方法resume(),因為這個連線必須手動執行,所以在設置完後必須接著使用方法resume()來送出連線。

閉包的第一個參數data便為獲得的資訊,這邊先轉成字串印出來,後續會再做更多處理。

普通獲取遠端資訊方式

如果想要在下載資料的各個階段執行動作,就需要實作委任方法,首先為ViewController加上委任模式需要遵循的協定:

class ViewController: UIViewController,
  NSURLSessionDelegate, NSURLSessionDownloadDelegate {

  // 省略
}

接著是可以實作的委任方法:

// 下載完成
func URLSession(
  session: NSURLSession, 
  downloadTask: NSURLSessionDownloadTask,
  didFinishDownloadingToURL location: NSURL) {
    print("下載完成")
}

// 下載過程中
func URLSession(
    session: NSURLSession, 
    downloadTask: NSURLSessionDownloadTask,
    didWriteData bytesWritten: Int64,
    totalBytesWritten: Int64,
    totalBytesExpectedToWrite: Int64) {
    // 如果 totalBytesExpectedToWrite 一直為 -1
    // 表示遠端主機未提供完整檔案大小資訊
    print("下載進度: \(totalBytesWritten)")
    print("/\(totalBytesExpectedToWrite)")
}

最後將獲取方式寫在一個方法中:

// 普通獲取遠端資訊的方式
func normalGet(myUrl :String) {
    if let url = NSURL(string: myUrl) {
        // 設置為預設的 session 設定
        let sessionWithConfigure = 
          NSURLSessionConfiguration.
          defaultSessionConfiguration()

        // 設置委任對象
        let session = NSURLSession(
          configuration: sessionWithConfigure,
          delegate: self, 
          delegateQueue: nil)

        // 設置遠端 API 網址
        let dataTask =
          session.downloadTaskWithURL(url)

        // 執行動作
        dataTask.resume()
    }
}

上述程式首先使用NSURLSessionConfiguration設置一個 session 的設定,並使用defaultSessionConfiguration()設置為預設模式。另外還可以使用ephemeralSessionConfiguration(),這個模式不會將連線中的快取、Cookie 或認證資訊做儲存,就像是瀏覽器的隱私模式。或是使用backgroundSessionConfiguration(),讓應用程式被切換到背景時仍然可以執行連線工作。

接著使用NSURLSession(configuration:delegate:delegateQueue:)設置一個 NSURLSession 實體(相較於基本獲取方式的共用實體,這邊設置為一個新的 NSURLSession 實體。),參數傳入前面設置的 session 設定,以及設置委任對象。設置委任對象後,在下載過程中與完成時,都會執行委任方法。

最後使用downloadTaskWithURL()填入遠端 API 網址,及執行動作resume()

執行獲取資訊

viewDidload()中,執行獲取兩個示範 API 資料,分別使用前面介紹的基本獲取資訊方式普通獲取資訊方式

// 台北住宿資料 中文
let strHotelID = 
  "6f4e0b9b-8cb1-4b1d-a5c4-febd90f62469"
self.simpleGet(taipeiDataUrl + strHotelID,
  targetPath: self.documentsPath + "hotel.json")

// 台北景點資料 中文
let strTouringSiteID = 
  "36847f3f-deff-4183-a5bb-800737591de5"
self.touringSiteTargetUrl = 
  self.documentsPath + "touringSite.json"
self.normalGet(taipeiDataUrl + strTouringSiteID)

以上便會開始獲取遠端資料。方法simpleGet()的第二個參數targetPath後續會再做說明使用。

儲存為本地檔案

在前面順利獲得資料後,接著將資料存成本地檔案以供後續使用。

首先是稍前建立的方法simpleGet(),獲得的資料data為 NSData 型別,以下為儲存方式:

// 建立檔案
let fileurl = NSURL(string: targetPath)
if let result = data?.writeToURL(fileurl!, atomically: true) {
    if result {
        print("簡單方式獲取遠端資訊:儲存資訊成功")
    } else {
        print("簡單方式獲取遠端資訊:儲存資訊失敗")
    }
}

方法simpleGet()傳入的第二個參數targetPath型別為 String ,所以先將其轉為 NSURL ,並帶入型別為 NSData 的閉包參數data的方法writeToURL(),以建立一個新的本地檔案。會返回一個 Bool? 的值表示儲存成功或失敗。

接著是方法normalGet(),會在下載完成的委任方法中獲得資料location,其型別為 NSURL,是一個本地的暫存檔案路徑,以下為儲存方式:

let targetUrl = NSURL(
  string: self.touringSiteTargetUrl)!
let data = NSData(contentsOfURL: location)
if ((data?.writeToURL(targetUrl, atomically: true)) 
  != nil) {
    print("普通獲取遠端資訊的方式:儲存資訊成功")
} else {
    print("普通獲取遠端資訊的方式:儲存資訊失敗")
}

使用一開始設置的屬性touringSiteTargetUrl來生成一個 NSURL ,用來表示新的本地檔案路徑。接著將location轉為型別 NSData 的資料後,再以方法writeToURL()建立一個新的本地檔案。

以上如果都順利儲存成功,會在本地的 Documents 目錄中,分別建立 hotel.json 與 touringSite.json 檔案。

解析 JSON 檔案

前面建立好兩個 JSON 檔案後,必須再將其做解析以取得其內的資料。首先使用先前介紹的瀏覽器擴充套件檢視一下這個 JSON 內容:

taipeitravel06

可以發現格式如下:

{
  result: {
    offset: 0,
    limit: 10000,
    count: 517,
    sort: "",
    results: [
      // 省略
    ]
  }
}

最外層可以轉換成一個字典( Dictionary ),其內只有一筆資料, key 值為result,對應著其內的資料也是一個字典,其內有五筆資料,代表意思分別為:

  • offset:獲取資料的偏移量,如果設置為 3 ,則表示獲取資料要跳過前面 3 筆,從第四筆開始取得。
  • limit:獲取資料的最多數量,如果設置為 10 ,則最多只會取得 10 筆資料。
  • count:全部的資料數量。
  • sort:排序方式,與 SQL 指令類似,如果設置為 "id asc, RowNumber desc",則是以 id 從小到大排序,以及以 RowNumber 從大到小排序。
  • results:獲取的資料,會依照前面設置的設定取得資料。

如果要使用這些功能,可以在稍前提到的 API 網址後面加上,像是要設置 offset 為 5 以及 limit 為 10 ,則是在該網址後面加上&offset=5&limit=10即可。

依照上述的格式,可以將其轉換為一個型別為 [String:[String:AnyObject]] 的字典,以供後續使用。接著這邊將解析 JSON 的功能寫在一個方法中,如下:

// 解析 json 檔案
func jsonParse(url :NSURL) {
    do {
        let dict = 
          try NSJSONSerialization
            .JSONObjectWithData(
              NSData(contentsOfURL: url)!, 
              options:
                NSJSONReadingOptions.AllowFragments) 
          as! [String:[String:AnyObject]]

        print(dict.count)

        let dataArr = 
          dict["result"]!["results"] as! [AnyObject]

        print(dataArr.count)

        print(dataArr[3]["stitle"])

    } catch {
        print("解析 json 失敗")
    }

}

上述程式使用NSJSONSerialization.JSONObjectWithData()來解析 JSON 檔案,因為設計為一個拋出函式,所以使用do-catch語句來定義錯誤的捕獲

以上即為這小節範例的內容。

範例

本節範例程式碼放在 apps/taipeitravel

results matching ""

    No results matching ""