抓台股資料被限流或被擋,該怎麼處理?
先看狀態碼分辨情況:429 是被限流,5xx 多半是暫時性,4xx 通常是你的請求本身有問題,重試再多次也不會變好。真正有效的三件事是把重複請求快取起來、把逐檔查詢改成批次區間查詢、以及對可重試的錯誤做指數退避。盲目縮短間隔重打,只會讓你被擋得更久。
先分辨:不是每個失敗都該重試
- 429:被限流。可以重試,但一定要等——而且等的時間要一次比一次長。
- 5xx:伺服器端暫時性問題。可以重試,通常幾秒後就恢復。
- 4xx(429 除外):你的請求本身有問題,例如參數名稱錯了。重試一百次也是同樣結果,先去看錯誤訊息。
- 把這三類混在同一個重試邏輯裡,是最常見的放大器:本來只是參數打錯,結果變成對伺服器猛打。
指數退避,而且要尊重 Retry-After
每次重試把等待時間加倍,並加上一點隨機抖動,避免多個程序同時醒來又一起打。如果回應帶了 Retry-After 標頭就以它為準。注意:我們的回應目前沒有帶速率相關的標頭,所以你的退避不能依賴它存在——要寫成「有就用、沒有就用自己的退避」。
import random, time, requests
RETRYABLE = {429, 500, 502, 503, 504}
def get_with_backoff(url, *, headers, params, attempts=5):
for attempt in range(attempts):
r = requests.get(url, headers=headers, params=params, timeout=30)
if r.status_code == 200:
return r.json()
if r.status_code not in RETRYABLE:
# 參數錯之類的問題,重試不會變好——直接把訊息丟出來
raise RuntimeError(f"{r.status_code}: {r.text[:200]}")
wait = r.headers.get("Retry-After") # 有就聽它的
delay = float(wait) if wait else (2 ** attempt) + random.random()
time.sleep(delay)
raise RuntimeError("重試次數用盡")快取:同一段歷史不需要抓第二次
歷史資料不會變。今天抓過 2330 在 2024 年的日線,明天再跑同一份分析時不需要再抓一次。把回應以「代號+區間」為鍵存到本地,命中就不發請求——這通常比任何重試策略更能降低你的請求量。
import json, pathlib
CACHE = pathlib.Path("cache")
CACHE.mkdir(exist_ok=True)
def cached_prices(symbol, start, end, *, headers):
key = CACHE / f"{symbol}_{start}_{end}.json"
if key.exists():
return json.loads(key.read_text()) # 命中就不打 API
data = get_with_backoff(
"https://api.twmarketdata.com/v2/datasets/twse-daily-price",
headers=headers,
params={"symbol": symbol, "start_date": start, "end_date": end},
)
key.write_text(json.dumps(data, ensure_ascii=False))
return data批次:用區間查詢取代逐日迴圈
最常見的浪費是「一天一個請求」跑一整年,那是兩百多個請求換一份本來一次就能拿完的資料。日期區間是查詢參數,一次把整段拉回來,請求數直接掉兩個數量級。
# 不要這樣:一天一請求# for day in every_trading_day(2024):# fetch(symbol="2330", start_date=day, end_date=day) # 這樣就好:一次一整段data = cached_prices("2330", "2024-01-01", "2024-12-31", headers=HEADERS)print("一個請求拿到", data["count"], "列")把請求量降下來的順序
- 先做快取。重複請求是最容易砍掉的那一塊,而且完全不影響結果。
- 再改批次。逐日改成區間,通常一次就解決問題。
- 最後才調退避。退避是保護機制,不是提高吞吐的手段——它讓你在被擋時優雅地慢下來,不會讓你拿到更多資料。
先驗證再決定要不要註冊
五檔權值股(2330、2317、2454、0050、2603)在免費層資料集上不需要金鑰,可以直接打來確認請求格式、回應結構與你的解析程式碼能不能跑。要注意這是「免費層資料集 + 這五檔」兩個條件同時成立才免金鑰;換成其他代號,即使是同一支免費層端點,一樣會回 401。
# 不需要金鑰,可以直接跑import requestsr = requests.get( "https://api.twmarketdata.com/v2/datasets/twse-daily-price", params={"symbol": "2330", "limit": 1}, timeout=20,)print(r.status_code, r.json()["rows"][0]["date"])我們沒有的東西,先說清楚
- 回應目前不帶速率相關的標頭,所以你無法從回應得知還剩多少額度——退避要寫成不依賴它。
- 沒有即時報價,所以高頻輪詢在這裡沒有意義;歷史資料抓一次快取起來就夠。
- 各方案的實際額度以定價頁為準,這頁不重述數字,避免與定價頁不同步。
常見問題
1收到 429 之後應該等多久?
如果回應帶 Retry-After 就照它等。沒有的話用指數退避:第一次等 1 秒、第二次 2 秒、第四次 8 秒,並加一點隨機抖動避免多個程序同時重打。固定間隔重試是最容易被持續擋住的做法。
2一直收到 4xx,重試就會好嗎?
不會。429 以外的 4xx 代表請求本身有問題,例如參數名稱不對——同樣的請求再送一百次還是同樣的結果。先讀錯誤訊息,它通常會直接告訴你缺哪個欄位。
3怎麼知道我的請求量會不會超?
先把重複請求快取掉、把逐日查詢改成區間查詢,多數情況這樣就不會接近上限。實際額度看定價頁;回應本身不會告訴你還剩多少。
4沒有金鑰可以先測到什麼程度?
免費層資料集加上 2330、2317、2454、0050、2603 這五檔,可以完整驗證請求格式、回應結構與你的解析流程。換成其他代號就會回 401,那時再註冊拿金鑰即可。
想自己查這些資料?免費註冊。