取得遠端 API 資料並儲存
這一小節會介紹如何取得遠端 API 的資料,並將資料儲存成本地檔案,以供後續使用。
API 簡介
我們使用 Data.Taipei 臺北市政府資料開放平台 的 API 資料作為示範,這個平台開放的資料有很多項,這邊以取得臺北市臺北旅遊網-住宿資料(中文)及臺北市臺北旅遊網-景點資料(中文)作為示範。
進入臺北市臺北旅遊網-景點資料(中文)資料集後,往下滑可以看到資料項目中有一個 API
,接著點擊進入,如下圖:
在 API 存取頁面便可以看到代表這個 API 的 RID,如下圖:
取得資料的方式則是根據開發指南所示,以https://data.taipei/opendata/datalist/apiAccess?scope=resourceAquire&rid=[RID]
來取得資料,其中 [RID]
要替換成 API 的 RID ,如下圖:
將上圖的網址以瀏覽器開啟後,會發現取得的資料是 JSON 格式,如果你的瀏覽器尚無內建將 JSON 結構化的功能,可以使用桌機的 Chrome 瀏覽器的擴充套件 JSONView 來將這個 JSON 資料結構化呈現在畫面中,比較方便檢視內容,如下圖:
以上為取得遠端 API 資料前置作業的介紹,藉由 API 網址即可取得政府提供的開放資料。接著會開始介紹如何在應用程式中獲取遠端 API 資料,並將這資料儲存為本地的一個 JSON 檔案,最後會說明如何解析這個 JSON 檔案並讀取其內資訊。
Hint
- JSON 是一種輕量級的資料交換格式,以純文字為基礎來儲存與傳送結構資料,可以經由特定的格式儲存任何文字資料(像是字串、數字、陣列或物件), JSON 可以讓你很簡單的與其他程式交換資料。
- 有些 API 會需要事先向擁有者申請 ID ,以便在接收資料前辨別獲取資料者的身分。
開始實作
首先在 Xcode 裡,新建一個 Single View Application 類型的專案,取名為 ExFetchDataAndStorage 。
一開始先為ViewController
建立三個屬性。taipeiDataURL
用來儲存 API 網址,hotelURL
跟touringSiteTargetURL
則是取得 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 內容:
可以發現格式如下:
{
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