前幾天學會了 API 與 JSON 的基礎概念,並完成了第一次呼叫 API。
今天要進一步學習,如何解析 JSON:把 API 回傳的資料轉成 Python 物件,並安全地取出特定欄位。
當用 Python 的 requests 套件去向 API 拿資料時,伺服器回傳的資料通常是「JSON 格式」的文字。
不能直接用字串去方便地拿取裡面的資料,所以要用 .json() 把它轉成 Python 的資料結構,方便使用。
把伺服器回來的位元組(bytes),依照編碼(像是 utf-8)轉成文字。
再用 json 解析器把 JSON 格式的文字轉換成 Python 的資料類型:
-JSON 的物件會變成 Python 字典(dict)
-JSON 的陣列會變成 Python 清單(list)
-JSON 裡的數字、布林、null會變成 Python 的 int/float、True/False、None。
.json()
vs r.text
:差在哪?r.text
:是伺服器回來的「純文字」,還是純純的 JSON 字串。
.json()
:已經把 JSON 字串轉成 Python 物件,可以直接用 data.get(...)
取欄位。
什麼時候用哪個?
確定回應是 JSON ,想方便處理資料,用 .json()
最直接。
不確定回傳內容,或想看原始字串內容,就先用 r.text
看一看。
通常會看 HTTP 回應的 Content-Type
標頭,如果裡面有 application/json
或類似包含 json
,代表它是 JSON。
也可以用程式檢查:
content_type = r.headers.get("Content-Type", "")
is_json_like = "json" in content_type.lower()
(1) JSONDecodeError
:表示伺服器回的內容不是正確的 JSON,可能是錯誤頁面或空字串。
解決方法:先看 Content-Type
,印 r.text
頭幾百字判斷,並加例外處理。
(2) 204 No Content:伺服器回成功但沒資料,這時 .json()
會出錯。可以先檢查 r.status_code
或 r.content
是否為空。
(3) 非標準 JSON:JSON 裡不允許 NaN、Infinity 等,若 API 錯誤傳回有這些資料會解析錯。這時需要先把資料清理或自訂解析行為。
(4) XSSI 防護字首:部分 API 為防止跨站攻擊可能在 JSON 前加多餘字元,需先去除再解析。
(5) 錯誤狀態碼(4xx/5xx):有時錯誤回應不是 JSON,呼叫 .json()
會錯誤,要先檢查狀態碼或用 r.raise_for_status()
。
以範例 API: https://jsonplaceholder.typicode.com/users/1
為例,我們要從裡面取出name
、email
,還有巢狀的 address.city
、company.name
。
API 回傳的 JSON 格式資料,透過 Python 的 requests 套件轉成 Python 可以操作的資料型態(通常是 dict 字典 和 list 清單)。
例如:data = r.json()
把 API 回傳的 JSON 字串,轉成 Python dict。
# 連到網站拿資料
import requests # 載入會「發送網路請求」的套件
url = "https://jsonplaceholder.typicode.com/users/1" # 要去拿的資料位置(第 1 號使用者)
r = requests.get(url, timeout=10) # 用 GET 把資料抓回來,最久等 10 秒
r.raise_for_status() # 如果伺服器回應不是成功(非 200 系列),立刻丟錯誤,避免用到壞資料
# 把回應轉成 Python 能用的型態
user = r.json() # 把回傳的 JSON 轉成 Python 的字典,之後就能像取字典一樣拿資料
# 直接取簡單欄位
name = user.get("name") # 抓 "name"
email = user.get("email") # 抓 "email"
address = user.get("address", {}) # 先拿 address,若沒這欄位就用空字典預設
city = address.get("city") # 再從 address 拿 city
company = user.get("company", {}) # 同理,拿 company
company_name = company.get("name") # 從 company 拿 name
print(name, email, city, company_name)
執行結果會是:
Leanne Graham Sincere@april.biz Gwenborough Romaguera-Crona
.get()
比 []
安全?user["name"]
如果 key 不存在會報錯 (KeyError),程式停止。
user.get("name")
如果 key 不存在會回傳 None 或預設值,不會錯誤。
對巢狀資料更重要,像user["company"]["name"]
若 company 不存在就會直接錯誤崩潰。
安全寫法是 (user.get("company") or {}).get("name")
,當 company 是 None 時會用空字典代替,避免報錯。
有些 API 會回傳多筆資料,格式是「陣列(list)裡裝著多個字典(dict)」。像這個範例 API:https://jsonplaceholder.typicode.com/posts
我們拿到的資料是多篇貼文,每篇資料是字典,裡面有 userId、id、title、body 等欄位。
import requests
r = requests.get("https://jsonplaceholder.typicode.com/posts", timeout=10) # 去網站把文章資料抓回來,最多等 10 秒
r.raise_for_status() # 如果不是成功回應(200 系列),馬上丟錯,避免用到壞資料
posts = r.json() # 把回傳的 JSON 轉成 Python 的 list
# 用 for 迴圈遍歷,印出前 5 筆的 id 和 title
for p in posts[:5]:
print(p.get("id"), p.get("title"))
# 篩選 userId 是1的貼文
user1_posts = [p for p in posts if p.get("userId") == 1]
# 從 posts 裡一筆一筆拿出文章 p,如果 p 的 userId 等於 1,就把它放進新的列表 user1_posts。
用 .get("userId") 比 p["userId"] 安全:缺欄位時不會拋錯,只會回 None
# 只看前 5 筆並輸出想看的欄位
for p in user1_posts[:5]: # 把篩好的結果取前 5 筆,逐筆處理
print(p.get("id"), p.get("title")) # 對每筆文章,安全地取出 id 跟 title 並印出來
if not isinstance(posts, list): #檢查 posts 是不是 list。
# isinstance(變數, 類型) 會回傳 True/False;這裡加上 not,代表「如果不是 list」
raise TypeError("預期是 list,但拿到不是") # 如果型態不符合,就主動拋出 TypeError,並附上容易看懂的錯誤訊息
[:5]
比用 for i in range(5)
更安全,避免索引超出範圍導致錯誤當想從字典拿某個欄位,但怕這個欄位沒資料,可以用 .get()
搭配預設值。
email = user.get("email", "(無 email)") # 如果 user 沒 email,會回傳括號內字串,不會報錯
JSON 常有巢狀結構(字典裡有字典),取值時要避免中間某層是 None 或沒資料而錯誤。
可以這樣寫:
city = (user.get("address") or {}).get("city") # 若 address 不存在,先用空字典代替,避免錯誤
company_name = (user.get("company") or {}).get("name", "(無公司)") # 若 company 無 name,回預設字串
當有一串多筆資料,可以用簡潔語法一次取出所有欄位:
titles = [p.get("title") for p in posts] # 取得所有貼文的標題
pairs = [(p.get("id"), p.get("title")) for p in posts] # 同時取得 id 和標題,成為 tuple 清單
篩選符合條件的資料,例如只取標題裡有 "qui" 的資料:
important = [p for p in posts if "qui" in (p.get("title") or "")]
for p in posts if ...
:列表生成式。從 posts 逐筆拿出 p,符合條件就收進新列表。p.get("title") or ""
:先拿 title;如果沒有,就用空字串 "" 來代替,避免對 None 做字串查找而報錯。"qui" in (...)
:檢查子字串是否存在;有就為 True 。結合以上的技巧,先篩選,再取欄位,缺欄位用預設值填補:
rows = [
{"id": p.get("id"), "title": p.get("title") or "(untitled)"}
# 把每一篇文章變成一個小字典,只留下兩個欄位 # 安全取值 p.get("id")
#先拿 title;如果是 None 或空字串,就用 "(untitled)" 當預設
for p in posts
if p.get("userId") == 1 # 從所有 posts 裡挑出 userId 等於 1 的文章
]
Python 有內建的 json 模組,可以把資料轉成 JSON 格式存到檔案中。
import json
data = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "王小明"}]
# data = [...]:這是要輸出的資料,兩筆物件放在一個列表裡。
with open("data_out.json", "w", encoding="utf-8") as f:
# 開啟(或新建)一個叫 data_out.json 的檔案,用「寫入模式」w,編碼是 UTF-8。
# with ... as f::用 with 來管理檔案,區塊結束會自動關檔。
json.dump(data, f, indent=2, ensure_ascii=False) # 把 data 寫進檔案 f
這段程式是在「把一份 Python 資料(list of dict)存成漂亮可讀的 JSON 檔案」。
利用這個方法,就能抓到網路上的資料,挑想用的欄位,存成好看的 JSON 檔案。
這個部分 Day03 有實作過了,這邊就先跳過。