供應商的報價頁只差一個禮貌的請求,可上一個實習生因為猛刷被封了。請求需要名字、超時和理由。解析之前,你該先檢查什麼?
爬蟲從請求開始,而不是從正規開始。請求說明你要存取哪個 URL、以什麼身份存取,以及客戶端願意等待多久。本課先構造請求物件,再解釋解析之前必須處理的失敗情況。 你還會編碼查詢參數、檢查 HTTPError,並限制重試。
請求不只有 URL
urllib.request.Request 儲存 URL 和可選請求頭。描述清楚的 User-Agent 告訴伺服器是誰在存取。構造請求不會真的發送;在普通 Python 環境裡,urlopen 才執行網路呼叫。
from urllib.request import Request
request = Request(
"https://example.com/docs",
headers={"User-Agent": "PyPracticeBot/1.0"},
)
print(request.full_url)
print(request.get_header("User-agent"))先構造,再發送
為慢伺服器和故障做準備
網路會失敗,頁面會遷移。超時限制等待時間,狀態碼告訴你回應是否可用。2xx 視為成功,3xx 需要處理重定向,4xx 或 5xx 需要判斷,而不是盲目重試。
status = 404
if 200 <= status < 300:
result = "ok"
elif 300 <= status < 400:
result = "redirect"
else:
result = "error"
print(result)按狀態碼分支
把抓取和解析分開
把解析寫成接收 HTML 文字的函式。這樣可以用儲存的網頁樣本測試,不需要聯網。這個邊界也能清楚劃分限速和快取應放在哪裡。
from html.parser import HTMLParser
class Links(HTMLParser):
def __init__(self):
super().__init__()
self.links = []
def handle_starttag(self, tag, attrs):
if tag == "a":
href = dict(attrs).get("href")
if href:
self.links.append(href)
def parse_links(html):
parser = Links()
parser.feed(html)
return parser.links
saved = '<a href="/docs">Docs</a><a href="/about">About</a>'
print(parse_links(saved))可以離線測試的解析器
先編碼查詢參數,再構造請求
urlencode 會跳脫空格、& 和非 ASCII 文字。用編碼後的片段拼 URL,不要直接拼接原始使用者輸入。清楚的 User-Agent 和 Accept 頭也能讓請求更容易識別和偵錯。
from urllib.parse import urlencode
from urllib.request import Request
query = urlencode({"q": "green tea", "page": 2})
url = "https://example.com/search?" + query
request = Request(url, headers={"User-Agent": "PyPracticeGift/1.0"})
print(request.full_url)
print(request.get_header("User-agent"))由編碼片段組成的 URL
把 HTTPError 當成回應來讀
urllib 遇到 4xx/5xx 會拋出 HTTPError,但例外裡仍有狀態碼、回應頭和可讀回應體。錯誤頁裡有有用資訊時可以捕獲並讀取;不要捕獲所有網路例外然後假裝請求成功。
from io import BytesIO
from urllib.error import HTTPError
error = HTTPError(
"https://example.com/missing",
404,
"Not Found",
{"Content-Type": "text/plain"},
BytesIO(b"no such page"),
)
print(error.code, error.reason)
print(error.read().decode("utf-8"))失敗回應體也是資料
限制重試並尊重 Retry-After
只重試可能暫時的錯誤,例如 429 和 503。Retry-After 可能是秒數,也可能是 HTTP 日期;要給它設上限,別讓伺服器讓你的任務睡幾個小時。重試次數也必須封頂。
def retry_delay(status, retry_after, cap=300):
if status not in {429, 503}:
return None
try:
seconds = int(retry_after)
except (TypeError, ValueError):
return 60
return max(1, min(seconds, cap))
for status, header in [(200, None), (429, "120"), (503, "soon")]:
print(status, retry_delay(status, header))有上限的等待,不是無限睡眠
本課要點
- Request 物件攜帶 URL 和請求頭,包括描述清楚的 User-Agent。
- 設定超時,避免卡住的連接讓程式一直等待。
- 認真處理狀態碼,不要假設每個回應都是 HTML。
- 用儲存的 HTML 樣本測試解析器,而不是直接打真實網站。
- 通過快取和限速減少不必要的請求。
- 查詢參數要編碼,HTTPError 要主動讀,Retry-After 和重試次數都要封頂。
常見錯誤
- 在迴圈裡發請求,卻沒有延遲、超時或停止條件。
- 偽裝成瀏覽器來繞過存取控制或站點條款。
- 還沒確認請求成功,就開始解析回應。
- 連永久性的 404 也重試,給已經吃力的伺服器繼續加負載。
為一個文件頁構造帶機器人 User-Agent 的 Request,在不發送的情況下列印 URL 和請求頭。
動手試試
改改變數、先預測輸出,再執行程式碼。第一次點擊「執行程式碼」時會下載 Python 執行時,可能需要幾秒鐘。
練習本課
12 道題,作答後立即看到解釋。