取得遠端 API 資料並儲存

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

API 簡介

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

進入臺北市臺北旅遊網-景點資料(中文)資料集後,往下滑可以看到資料項目中有一個 API ,接著點擊進入,如下圖:

taipeitravel03

API 存取頁面便可以看到代表這個 API 的 RID,如下圖:

taipeitravel04

取得資料的方式則是根據開發指南所示,以https://data.taipei/opendata/datalist/apiAccess?scope=resourceAquire&rid=[RID]來取得資料,其中 [RID]要替換成 API 的 RID ,如下圖:

taipeitravel04

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

taipeitravel05

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

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

開始實作

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

一開始先為ViewController建立三個屬性。taipeiDataURL用來儲存 API 網址,hotelURLtouringSiteTargetURL則是取得 API 資訊解析後的 JSON 儲存的本機檔案路徑:

class ViewController: UIViewController {
    let taipeiDataURL = 
    "https://data.taipei/opendata/datalist/apiAccess?scope=resourceAquire&rid="

    var hotelURL: URL = {
        do {
            return try FileManager.default.url(
                for: .documentDirectory, 
                in: .userDomainMask, 
                appropriateFor: nil, 
                create: true)
                .appendingPathComponent("hotel.json")
        } catch {
            fatalError("Error getting hotel URL from document directory.")
        }
    }()

    var touringSiteTargetURL: URL = {
        do {
            return try FileManager.default.url(
                for: .documentDirectory, 
                in: .userDomainMask, 
                appropriateFor: nil, 
                create: true)
                .appendingPathComponent("touringSite.json")
        } catch {
            fatalError("Error getting touringSite URL from document directory.")
        }
    }()

    // 省略
}

獲取遠端 API 資料

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

基本獲取遠端資訊方式

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

func simpleGet(_ myUrl :String, targetPath :URL) {
    URLSession.shared.dataTask(
        with: URL(string: myUrl)!, 
        completionHandler: {data, response, error in
            // 將取得的資訊轉成字串印出
            print(String(data: data!, encoding: .utf8))        
    }).resume()
}

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

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

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

普通獲取遠端資訊方式

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

class ViewController: UIViewController,
  URLSessionDelegate, URLSessionDownloadDelegate {

  // 省略
}

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

// 下載完成
func urlSession(
    _ session: URLSession, 
    downloadTask: URLSessionDownloadTask, 
    didFinishDownloadingTo location: URL) {
    print("下載完成")
}

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

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

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

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

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

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

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

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

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

執行獲取資訊

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

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

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

儲存為本地檔案

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

首先是稍前建立的方法simpleGet(),獲得的資料data為 Data? 型別,將其儲存到本機檔案位置targetURL,以下為儲存方式:

// 建立檔案
do {
    try data?.write(to: targetURL, options: .atomic)
    print("基本獲取遠端資訊的方式:儲存資訊成功")
    self.jsonParse(targetURL)
} catch {
    print("基本獲取遠端資訊的方式:儲存資訊失敗")
}

接著是方法normalGet(),會在下載完成的委任方法中獲得資料location (型別為 URL ),這是一個本地的暫存檔案路徑,先取得此資料後,再將其儲存至本機檔案位置,以下為儲存方式:

do {
    let data = try? Data(contentsOf: location)
    try data?.write(to: touringSiteTargetURL, options: .atomic)
    print("普通獲取遠端資訊的方式:儲存資訊成功")
    self.jsonParse(touringSiteTargetURL)
} catch {
    print("普通獲取遠端資訊的方式:儲存資訊失敗")
}

以上如果都順利儲存成功,會在本地的 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 :URL) {
    do {
        let dict = try JSONSerialization.jsonObject(
                with: Data(contentsOf: url), 
                options: 
                JSONSerialization.ReadingOptions.allowFragments) 
        as! [String:[String:AnyObject]]

        print(dict.count)
        let dataArr = dict["result"]!["results"] as! [AnyObject]
        print(dataArr.count)
        print(dataArr[3]["stitle"] as Any)
    } catch {
        print("解析 json 失敗")
    }

}

將 JSON 資料解析出來後便可以繼續執行你想要作的後續動作了,像是列表出來或是分頁顯示。

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

範例

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

results matching ""

    No results matching ""