#40 搭配 AI 建立 JSON API, 並用程式抓取API內容

透過 Github 在網路上放置一個自己的 JSON API 網址並用程式抓取內容(#hw14–1)

配AI 建立 JSON API 的生成步驟:

  1. 請 AI 創作或讀取網頁內容生成 JSON
  2. 到 Github 新增檔案,將剛剛 AI 生成的 JSON 貼上
  3. 取得 JSON API 的網址
  4. URLSession.shared.dataTask 抓取資料
  5. 定義 JSON 對應的 struct
  6. 透過 JSONDecoder().decode 把 JSON 資料轉成 struct

以下我們請 AI 產生 5 種常見的 JSON 例子。

  1. 內容是字串的 array。

2. 第一層是 object( { } )。

3. 第一層是 array( [ ] ),array 的成員是物件( { } )。

4. 讀取網頁內容產生 JSON。

5. 多層的 JSON,比方至少 4 層的 JSON。

1. 內容是字串的 array。

prompt:

用 JSON 寫一個台灣小吃名字的 array ,第一層是 [ ] ,寫台灣譯名,至少有二十個名字。

answer:

[
"珍珠奶茶",
"滷肉飯",
"肉圓",
"擔仔麵",
"蚵仔煎",
"米苔目",
"臭豆腐",
"豬血糕",
"蔥油餅",
"台南擂茶",
"蘭州牛肉麵",
"割包",
"魷魚羹",
"貢丸湯",
"潤餅",
"豆花",
"烤玉米",
"花生糖",
"擂茶",
"鳳梨酥"
]

在 GitHub 新增檔案,貼上 AI 生成的 JSON。

連到 GitHub 網站,點選左上的 New 建立 Repositories。

輸入 Repository 的名字,點選 Create Repository。
ps: 優秀的儲存庫名稱簡短且易於記憶。

點選 create a new file。

輸入檔名和貼上 AI 產生的 JSON。
點選右上方的 Commit changes。

會再出現確認窗,可以加上描述,點選右下方的 Commit changes。

之後若想再新增 JSON,可點選 Add file 的 create new file。

取得 JSON API 的網址。

進入剛剛建立的 JSON 檔頁面,點選右上方的 Raw。

此時顯示的才是 JSON 內容的真正頁面,瀏覽器框框裡的網址即為我們可抓取 JSON 的 API 網址。

https://raw.githubusercontent.com/JasonHungApp/JSON_API/main/TaiwanSnackNames.json

2. 第一層是 object( { } )

範例 1

  • prompt:

用 JSON 定義珍珠奶茶的相關資訊,第一層是 { }, key 的部份是用英文,但 value 的部份是繁體中文

answer:

{
"name": "珍珠奶茶",
"ingredients": ["紅茶", "鮮奶", "珍珠"],
"sweetness_levels": ["全糖", "半糖", "無糖"],
"ice_levels": ["正常冰", "少冰", "去冰"],
"features": "將口感Q彈的珍珠搭配香濃的紅茶和鮮奶,形成經典的台灣飲料。",
"preparation": [
"先沖泡紅茶,加入糖調製成奶茶。",
"煮熟珍珠,加入黑糖調味。",
"將珍珠放入杯中,注入調製好的奶茶。",
"根據客戶口味添加甜度和冰塊。"
]
}

範例 2

我們也可以指定資料的類型,比方以下例子。

prompt:

用 JSON 定義珍珠奶茶的相關資訊,第一層是 { },資料以台灣中文呈現,key 用英文,資料的類型包含字串,整數,浮點數和 array。

answer:

{
"name": "珍珠奶茶",
"category": "奶茶",
"ingredients": [
{
"name": "紅茶",
"amount": "適量"
},
{
"name": "鮮奶",
"amount": "適量"
},
{
"name": "珍珠",
"amount": "適量"
}
],
"sweetness_levels": [0, 50, 100],
"ice_levels": ["正常冰", "少冰", "去冰"],
"price": 60.5,
"description": "將口感Q彈的珍珠搭配香濃的紅茶和鮮奶,形成經典的台灣飲料。",
"preparation_steps": [
"先沖泡紅茶,加入糖調製成奶茶。",
"煮熟珍珠,加入黑糖調味。",
"將珍珠放入杯中,注入調製好的奶茶。",
"根據客戶口味添加甜度和冰塊。"
]
}

3. 第一層是 array( [ ] ),array 的成員是物件( { } )

prompt:

用 JSON 定義台灣小吃的相關資訊,第一層是 [ ],array 的成員是物件( { } ),資料以台灣中文呈現,key 用英文

answer:

[
{
"name": "臭豆腐",
"category": "傳統小吃",
"ingredients": ["豆腐", "特製醬料"],
"flavor": "香酥臭味",
"features": "以特殊發酵製程製成,香脆外皮,帶有獨特的臭味。",
"price": 50
},
{
"name": "肉圓",
"category": "傳統小吃",
"ingredients": ["糯米", "五花肉", "香菇", "冬菇", "紅蔥頭"],
"flavor": "香Q美味",
"features": "外皮糯而不膩,內餡豐富,是台灣常見的傳統小吃之一。",
"price": 30
},
{
"name": "擔仔麵",
"category": "麵食",
"ingredients": ["細麵條", "蝦仁", "濃郁湯頭"],
"flavor": "鮮甜美味",
"features": "特色在於湯頭濃郁,配上嫩滑的蝦仁,是台灣的地方美食之一。",
"price": 40
}
]

4. 讀取網頁內容產生 JSON。

我只有 ChatGPT 3.5 他不能連網,殘念,

換 Bing Chat 來幫忙

prompt:

參考以下網頁,用 JSON 寫出劉德華的相關資訊,key 用英文,JSON 字數 1000 字以內。只需要參考以下網頁的內容,不需要再連到其它網頁。 https://zh.wikipedia.org/zh-tw/%E7%8E%8B%E5%BF%83%E5%87%8C

answer:


{
"name": "王心凌",
"birth_date": "1982年1月15日",
"birth_place": "台灣 台北市",
"occupation": ["歌手", "演員", "作家"],
"works": {
"albums": ["Begin...", "愛你", "蜜糖", "美麗的新世界", "Honey", "Cyndi Loves You", "Cyndi With U", "Sticky", "Fly! Cyndi", "天使心", "愛的抱抱", "幸福", "印象·心凌"],
"films": ["西遊記之大鬧天宮", "大明星小跟班", "超級經理人", "超級經理人2", "超級經理人3"],
"tv_series": ["西街少年", "天國的嫁衣", "煙火", "天下無雙", "美味關係", "天外飛仙", "美樂。加油", "微笑Pasta", "愛就宅一起", "爱情睡醒了", "幸福光年", "幸福選擇題", "幸福三顆星", "幸福不二家", "幸福的面条", "幸福的起点", "幸福的方程式", "幸福的指南", "幸福的密码", "幸福的便利店"]
}
}

Claude 也不能連網,殘念

5. 多層的 JSON,比方至少 4 層的 JSON

範例 1

prompt:

用 JSON 定義台灣小吃的相關資訊,JSON 有 4 層,資料以台灣中文呈現,key 用英文

answer:

[
{
"name": "小籠包",
"type": "點心",
"ingredients": [
{"name": "豬肉", "quantity": "200克"},
{"name": "湯汁", "quantity": "100毫升"},
{"name": "餃子皮", "quantity": "適量"}
],
"instructions": [
"將豬肉剁成細末。",
"將湯汁和剁碎的豬肉混合均勻。",
"將適量的餃子皮包入混合物,形成小籠包的形狀。",
"蒸煮約15分鐘,直至小籠包熟透即可。"
],
"details": {
"description": "以豬肉餡料和特製湯汁為特色的小吃,包裹在薄薄的餃子皮中。",
"origin": "上海",
"popular_places": ["臺北市", "新北市"]
}
},
{
"name": "珍珠奶茶",
"type": "飲料",
"ingredients": [
{"name": "茶葉", "quantity": "2茶匙"},
{"name": "牛奶", "quantity": "200毫升"},
{"name": "珍珠", "quantity": "適量"}
],
"instructions": [
"將茶葉沖泡。",
"加入牛奶,攪拌均勻。",
"加入適量的珍珠,即可享用。"
],
"details": {
"description": "由茶葉、牛奶和Q彈珍珠構成的經典台灣飲料。",
"origin": "臺灣",
"popular_places": ["台中市", "高雄市"]
}
},
{
"name": "蚵仔煎",
"type": "小吃",
"ingredients": [
{"name": "蚵仔", "quantity": "300克"},
{"name": "蛋", "quantity": "2顆"},
{"name": "太白粉", "quantity": "適量"}
],
"instructions": [
"將蚵仔和蛋混合。",
"在混合物中加入適量的太白粉,攪拌均勻。",
"平底鍋中加熱,將混合物倒入,煎至金黃色即可。"
],
"details": {
"description": "將新鮮的蚵仔和鹹蛋煎熟,再加上太白粉製成的酥皮,是經典的台灣街頭小吃。",
"origin": "台灣",
"popular_places": ["台南市", "嘉義市"]
}
}
]

請 AI 寫出 JSON 對應的 Swift Codable 型別

把剛剛的 JSON 放到 Github 去 https://raw.githubusercontent.com/JasonHungApp/JSON_API/main/TaiwanSnackPreparation.json

ViewController.swift

import UIKit

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
testJsonAPIinGithub()
}

func testJsonAPIinGithub(){
// Call the function to fetch JSON data
fetchJSONData { snacks in
if let snacks = snacks {
// Handle the decoded JSON data
for snack in snacks {
print("Name: \(snack.name)")
print("Type: \(snack.type)")
print("Ingredients:")
for ingredient in snack.ingredients {
print("- \(ingredient.name): \(ingredient.quantity)")
}
print("Instructions:")
for instruction in snack.instructions {
print("- \(instruction)")
}
print("Details:")
print("- Description: \(snack.details.description)")
print("- Origin: \(snack.details.origin)")
print("- Popular Places: \(snack.details.popular_places)")
print("\n")
}
}
}
}
}

fetchJSONData.swift

使用Thread方式去抓資料 URLSession.shared.dataTask

import Foundation

struct TaiwaneseSnack: Codable {
let name: String
let type: String
let ingredients: [Ingredient]
let instructions: [String]
let details: Details
}
struct Ingredient: Codable {
let name: String
let quantity: String
}
struct Details: Codable {
let description: String
let origin: String
let popular_places: [String]
}
let jsonURL = URL(string: "https://raw.githubusercontent.com/JasonHungApp/JSON_API/main/TaiwanSnackPreparation.json")! // Replace with the actual URL or local file URL
// Define a function to fetch and decode JSON
func fetchJSONData(completion: @escaping ([TaiwaneseSnack]?) -> Void) {
URLSession.shared.dataTask(with: jsonURL) { data, response, error in
guard let data = data, error == nil else {
print("Error fetching JSON:", error?.localizedDescription ?? "Unknown error")
completion(nil)
return
}

do {
let snacks = try JSONDecoder().decode([TaiwaneseSnack].self, from: data)
completion(snacks)
} catch {
print("Error decoding JSON:", error.localizedDescription)
completion(nil)
}
}.resume()
}

執行結果:

程式研究

Thread

URLSession.shared.dataTask 會被分配到 Thread 去抓資料

URLSession.shared.dataTask(with:completionHandler:)

func dataTask(
with url: URL,
completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void
) -> URLSessionDataTask

Closure (閉包)

URLSession.shared.dataTask(with:completionHandler:) 方法中的 completionHandler 是一個 Closure(閉包)。Closure 是一種可以在其他函數中傳遞和使用的匿名函數,它可以被視為一段代碼塊,可以在需要的時候被調用。在這個情況下,completionHandler 用於處理當數據任務完成時的回調(callback)操作,例如處理獲取的數據或處理錯誤。

執行可以看到 URLSession 目前被分配到 Thread 5 去執行

抓取資料完成,開始執行 completionHandler 被分配到 Thread 6

解碼

用 JSONDecoder().decode 來解碼抓取回來的資料

let snacks = try JSONDecoder().decode([TaiwaneseSnack].self, from: data)

分析一下 TaiwaneseSnack 的 JSON結構

Codable protocol = Decodable & Encodable

因為遵從 Codable 協議

當你使用 Swift 中的 Codable 時,你可以方便地進行對象(Object)和 JSON 數據之間的轉換。Codable 是 Swift 4 引入的一個協議,結合了兩個協議:Encodable(編碼)和 Decodable(解碼)。

public protocol Encodable {
func encode(to encoder: Encoder) throws
}

public protocol Decodable {
init(from decoder: Decoder) throws
}
public typealias Codable = Decodable & Encodable

JSONDecoder().decode

@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *)
open class JSONDecoder {

/// Decodes a top-level value of the given type from the given JSON representation.
///
/// - parameter type: The type of the value to decode.
/// - parameter data: The data to decode from.
/// - returns: A value of the requested type.
/// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid JSON.
/// - throws: An error if any value throws an error during decoding.
open func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable

Q: 為什麼不用去實作協議的 Codable protocol 裡 function

AI說明:

在你提供的程式碼中,你定義了三個結構(TaiwaneseSnackIngredientDetails),它們都遵從 Codable 協議。由於你的結構中的屬性都是基本數據類型或標準庫類型(例如 String[String]),它們已經默認實現了 Codable 相關的協議方法,因此你不需要手動實作這些方法。

當你的結構中包含的所有屬性都是 Codable 的時候,Swift 編譯器會自動為你生成默認的編碼(encode(to:))和解碼(init(from:))實現。這就是所謂的「合成實現」。

換句話說,由於你的結構中的所有屬性都是 Codable,Swift 會自動為你處理將這些屬性轉換為 JSON 格式(編碼)和將 JSON 轉換為結構實例(解碼)的操作,而不需要你手動實現相關的協議方法。

簡單來說,如果你的結構中的屬性都是 Swift 的內建類型或標準庫類型,而且它們已經實現了 Codable 相關的協議,你可以不必手動實作這些方法,Swift 會自動幫你處理。

追一下 String 程式看看。String 裡有實作 Codable 裡面的二個 function

public protocol Encodable {
func encode(to encoder: Encoder) throws
}

public protocol Decodable {
init(from decoder: Decoder) throws
}
public typealias Codable = Decodable & Encodable
extension String : Codable {
public init(from decoder: Decoder) throws
public func encode(to encoder: Encoder) throws
}
/// A type that can decode values from a native format into in-memory
/// representations.
public protocol Decoder {

/// The path of coding keys taken to get to this point in decoding.
var codingPath: [CodingKey] { get }
/// Any contextual information set by the user for decoding.
var userInfo: [CodingUserInfoKey : Any] { get }
/// Returns the data stored in this decoder as represented in a container
/// keyed by the given key type.
///
/// - parameter type: The key type to use for the container.
/// - returns: A keyed decoding container view into this decoder.
/// - throws: `DecodingError.typeMismatch` if the encountered stored value is
/// not a keyed container.
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey
/// Returns the data stored in this decoder as represented in a container
/// appropriate for holding values with no keys.
///
/// - returns: An unkeyed container view into this decoder.
/// - throws: `DecodingError.typeMismatch` if the encountered stored value is
/// not an unkeyed container.
func unkeyedContainer() throws -> UnkeyedDecodingContainer
/// Returns the data stored in this decoder as represented in a container
/// appropriate for holding a single primitive value.
///
/// - returns: A single value container view into this decoder.
/// - throws: `DecodingError.typeMismatch` if the encountered stored value is
/// not a single value container.
func singleValueContainer() throws -> SingleValueDecodingContainer
}
/// A type that can encode values into a native format for external
/// representation.
public protocol Encoder {

/// The path of coding keys taken to get to this point in encoding.
var codingPath: [CodingKey] { get }
/// Any contextual information set by the user for encoding.
var userInfo: [CodingUserInfoKey : Any] { get }
/// Returns an encoding container appropriate for holding multiple values
/// keyed by the given key type.
///
/// You must use only one kind of top-level encoding container. This method
/// must not be called after a call to `unkeyedContainer()` or after
/// encoding a value through a call to `singleValueContainer()`
///
/// - parameter type: The key type to use for the container.
/// - returns: A new keyed encoding container.
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey
/// Returns an encoding container appropriate for holding multiple unkeyed
/// values.
///
/// You must use only one kind of top-level encoding container. This method
/// must not be called after a call to `container(keyedBy:)` or after
/// encoding a value through a call to `singleValueContainer()`
///
/// - returns: A new empty unkeyed container.
func unkeyedContainer() -> UnkeyedEncodingContainer
/// Returns an encoding container appropriate for holding a single primitive
/// value.
///
/// You must use only one kind of top-level encoding container. This method
/// must not be called after a call to `unkeyedContainer()` or
/// `container(keyedBy:)`, or after encoding a value through a call to
/// `singleValueContainer()`
///
/// - returns: A new empty single value container.
func singleValueContainer() -> SingleValueEncodingContainer
}

待研究,總之

上面程式碼是 Swift 中 Encoder 協定的定義,該協定是 Swift 標準庫中的 Codable 框架的一部分,用於將 Swift 值編碼成外部表示,例如 JSON 或 Property List。

Encoder 協定提供了一組方法來創建編碼容器,每個容器在編碼過程中有特定的用途。這個協定通常由類型如 JSONEncoderPropertyListEncoder 實現,以實現將 Swift 值轉換為它們相應的外部表示形式。

--

--