第 38 課 · 實戰 · 18 分鐘

Python URL 解析

連結進入佇列前,先把相對位址補全。

urlparseurljoinparse_qsurlencode

供應商頁面上的商品連結全是相對路徑,一半還帶著追蹤參數。當成普通字串處理,它們就變成了重複項。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 執行時,可能需要幾秒鐘。

瀏覽器內 Python本機執行 · 無需帳號
首次執行會從 CDN 下載 Pyodide。修改後點擊「執行程式碼」。

練習本課

12 道題,作答後立即看到解釋。

查看 12 道題答案開始練習
← HTML 解析分頁與去重 →