供應商頁面上的商品連結全是相對路徑,一半還帶著追蹤參數。當成普通字串處理,它們就變成了重複項。urljoin 知道什麼,是字串拼接不知道的?
抓取到的頁面裡充滿相對連結、追蹤參數和片段。把它們當普通字串處理,會產生重複位址和錯誤請求。urllib.parse 提供一組標準工具,用來拆分、拼接和重建 URL。 你還會為去重規範化 URL,並拒絕不安全的協議或主機。
把 URL 拆成組成部分
urlparse 分離協議、網路位置、路徑、查詢字串和片段。片段是給瀏覽器用的,不會發送給伺服器。分別讀取各部分,比在第一個斜槓處切字串更安全。
from urllib.parse import urlparse
url = urlparse("https://example.com/docs/page.html?q=python#intro")
print(url.scheme)
print(url.netloc)
print(url.path)
print(url.query)一個物件,五個部分
基於目前頁面補全相對連結
urljoin 用頁面 URL 補全相對引用。它能處理 ../、根相對路徑和只有查詢參數的連結。先去重再補全,會把 /docs 和 https://example.com/docs 當成兩個不同位址。
from urllib.parse import urljoin
base = "https://example.com/docs/page.html"
print(urljoin(base, "../guide.html"))
print(urljoin(base, "/about"))
print(urljoin(base, "?page=2"))一個基準,三種連結
讀取查詢參數
parse_qs 把查詢字串轉換成「鍵到列表」的字典,因為同一個鍵可以出現多次。需要保留順序時使用 parse_qsl。值仍然是字串,頁碼在參與計算前要顯式轉換。
from urllib.parse import parse_qs
params = parse_qs("q=python&page=2&tag=a&tag=b")
print(params["page"])
print(params["tag"])重複的鍵會變成列表
安全地構造查詢字串
urlencode 會替你跳脫空格、& 和非 ASCII 文字。可以傳入映射或鍵值對序列。不要把原始使用者輸入直接拼進 URL;一個多餘的 & 就可能改變請求含義。
from urllib.parse import urlencode
query = urlencode({"q": "a b", "page": 2})
print(query)編碼,而不是拼接
先去重前規範化
同一個頁面可能有大小寫不同的協議、預設埠、片段或不同順序的查詢參數。比較前把協議和主機轉小寫、去掉預設埠、排序查詢參數並丟掉片段;路徑本身仍然區分大小寫。
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
def normalize(url):
parts = urlsplit(url)
scheme = parts.scheme.lower()
host = (parts.hostname or "").lower()
default_port = 443 if scheme == "https" else 80
port = parts.port
netloc = host if not port or port == default_port else f"{host}:{port}"
query = urlencode(sorted(parse_qsl(parts.query, keep_blank_values=True)))
return urlunsplit((scheme, netloc, parts.path or "/", query, ""))
for raw in [
"HTTPS://Example.com:443/docs?b=2&a=1#top",
"https://example.com/docs?a=1&b=2",
]:
print(normalize(raw))兩種寫法,一個鍵
拒絕不安全的協議和意外主機
urljoin 會跟隨連結裡的任何協議,包括 javascript: 和 data:。解析結果,只允許 http 或 https,並檢查主機是否在預期範圍內。相對連結拼接後也可能變成外部 URL,所以解析前後都要檢查。
from urllib.parse import urljoin, urlsplit
base = "https://example.com/docs/page"
for link in ["/next", "javascript:alert(1)", "https://evil.example/x", "//example.com/other"]:
target = urljoin(base, link)
parts = urlsplit(target)
safe = parts.scheme in {"http", "https"} and parts.hostname == "example.com"
print(link, safe, target)先解析,再做允許列表
本課要點
- urlparse 分別提供 scheme、netloc、path、query 和 fragment。
- urljoin 用連結所在頁面的位址補全相對連結。
- parse_qs 回傳列表,因為查詢鍵可能重複。
- urlencode 在值進入 URL 前進行跳脫。
- 比較抓取佇列中的位址前,先去掉片段。
- 比較前先規範化;urljoin 之後檢查協議和主機。
常見錯誤
- 用字串拼接來補全相對連結。
- 假設查詢參數只會出現一次。
- 比較兩個頁面位址時保留 # 後面的片段。
- 直接比較原始 URL 字串,結果放過了 javascript: 連結。
基於一個基準 URL 補全三個相對連結,並按首次出現順序列印唯一的絕對位址。
動手試試
改改變數、先預測輸出,再執行程式碼。第一次點擊「執行程式碼」時會下載 Python 執行時,可能需要幾秒鐘。
練習本課
12 道題,作答後立即看到解釋。