
資料抓取與 API 串接的實務
先確認可不可以抓
技術上做得到,不代表可以做。抓取他人網站的資料前,應該先確認三件事:
- 對方的使用條款是否允許
- robots.txt 的規範
- 抓取的內容是否涉及著作權或個資
幾個明確的界線
- 不要抓取個人資料——姓名、聯絡方式、帳號等,即使是公開顯示的
- 不要整批複製他人的內容用於自己的網站——這是著作權問題
- 不要繞過登入或付費機制
- 不要造成對方伺服器的負擔
著作權的相關說明見別人的內容可以用嗎。
合理的使用情境
- 抓取自己網站的資料做檢查或搬遷
- 使用對方正式提供的 API
- 取得公開資料集或有明確授權的資料
- 經對方同意的資料交換
尊重 robots.txt
from urllib.robotparser import RobotFileParser
from urllib.parse import urljoin
def can_fetch(base_url, path, agent='MyBot'):
rp = RobotFileParser()
rp.set_url(urljoin(base_url, '/robots.txt'))
try:
rp.read()
except Exception:
return False # 讀不到就保守處理
return rp.can_fetch(agent, urljoin(base_url, path))
讀不到 robots.txt 時建議保守處理,而不是預設為允許。
基本的請求設定
import requests
session = requests.Session()
session.headers.update({
'User-Agent': 'MyCompanyBot/1.0 (+https://example.com/bot)',
})
resp = session.get(url, timeout=(5, 30))
resp.raise_for_status()
三個必要的設定
- 明確的 User-Agent——包含聯絡方式或說明頁,讓對方知道是誰在抓,有問題時能聯繫
- 逾時設定——
(連線逾時, 讀取逾時)。不設逾時是常見的錯誤,對方無回應時程式會永遠卡住 raise_for_status()——讓錯誤的狀態碼拋出例外,不要靜默繼續
控制頻率
import time, random
for url in urls:
fetch(url)
time.sleep(random.uniform(1.0, 2.5))
不要用固定的極短間隔連續請求。 這既是禮貌,也是自保——過度頻繁的請求可能被封鎖,甚至被視為攻擊行為。
建議的原則:把對方的伺服器當成你自己的網站來對待。
重試機制
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry = Retry(
total=3,
backoff_factor=1.0,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=['GET', 'HEAD'],
)
session.mount('https://', HTTPAdapter(max_retries=retry))
重要的注意事項
- 429 代表請求過於頻繁——收到時應該放慢,而不是立刻重試
- backoff 讓每次重試的間隔逐次拉長,避免持續衝擊
- 只對讀取類的請求自動重試——寫入類的重試可能造成重複
解析 HTML
from bs4 import BeautifulSoup
soup = BeautifulSoup(resp.text, 'html.parser')
title = soup.select_one('h1')
title_text = title.get_text(strip=True) if title else ''
for a in soup.select('article a[href]'):
print(a['href'], a.get_text(strip=True))
兩個實務提醒
- 取值前先確認元素存在——網頁結構會變,直接取值容易拋出例外
- 編碼問題——若出現亂碼,可嘗試指定
resp.encoding或使用resp.apparent_encoding
內容由前端程式產生的情況
如果目標頁面的內容需要執行 JavaScript 才會出現,用一般的請求抓不到。
兩種處理方向
- 找出資料的實際來源——多數情況前端是呼叫某個 API 取得資料,直接取用該來源更穩定也更輕量
- 使用瀏覽器自動化工具——資源消耗大得多,是最後手段
優先找第一種。 用瀏覽器開發者工具的網路分頁,通常能看到實際的資料請求。
串接正式 API 的實務
def call_api(session, url, params=None):
resp = session.get(url, params=params, timeout=(5, 30))
if resp.status_code == 429:
wait = int(resp.headers.get('Retry-After', 60))
logging.warning(f'頻率限制,等待 {wait} 秒')
time.sleep(wait)
return call_api(session, url, params)
resp.raise_for_status()
return resp.json()
要注意的四件事
- 金鑰不要寫在程式碼中——用環境變數
- 注意頻率限制——查閱對方文件的規範
- 處理分頁——大量資料通常需要逐頁取得
- 記錄失敗的項目——之後可以只重試失敗的部分
串接的評估與維護見API 串接前要確認什麼。
自己網站的檢查腳本
這是最沒有爭議、也最實用的應用。
檢查連結是否失效
def check_links(urls, session):
broken = []
for u in urls:
try:
r = session.head(u, timeout=10, allow_redirects=True)
if r.status_code >= 400:
broken.append((u, r.status_code))
except Exception as e:
broken.append((u, str(e)))
time.sleep(0.5)
return broken
用 HEAD 而非 GET——只要狀態碼的話不必下載內容。但部分伺服器不支援 HEAD,失敗時可退回 GET。
其他實用的檢查
- 所有頁面是否都有標題與描述
- 圖片是否都有替代文字
- 是否有混合內容(頁面是 https 但資源是 http)
- 網站地圖中的網址是否都能正常開啟
技術檢查的項目見技術 SEO 的常見問題排查。
資料的儲存與去重
import json
from pathlib import Path
def save_progress(data, path):
tmp = Path(path).with_suffix('.tmp')
with open(tmp, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
tmp.replace(path) # 原子性寫入
長時間執行的抓取要能中斷後續跑——定期存檔進度,並記錄已處理的項目,避免重複。
四個自我約束
- 只抓需要的資料,不要整站複製
- 控制頻率,不造成對方負擔
- 標明身分,讓對方能聯繫你
- 收到停止的要求就停止
優先使用對方提供的 API 或資料集——那才是對方希望你使用的方式,也最穩定。
本文的範例以 Python 3 為例,實際的套件版本與 API 可能隨版本更新而異。在正式環境執行任何批次處理前,請先以少量資料測試並確實備份。