Python 會使用 urllib.parse 模組來解析絕對 URL,其中的 urlparse 函式會回傳一個包含六個欄位的具名 tuple,分別為 scheme、netloc、path、params、query 與 fragment。對於 query 部分,parse_qs 與 parse_qsl 會進行第二輪處理,將查詢字串拆成 Python dict 或一組有序的鍵值對。大多數的 Python 教學在介紹完這兩步驟模式後就不再深入,對於符合 RFC 3986 的 URL 進行往返處理固然沒問題,但當輸入包含非 ASCII 主機名稱、明確寫出的預設連接埠,或查詢參數的值使用加號來編碼空格時,卻經常會產生令人意外的結果。瀏覽器端的對應工具——WHATWG URL 解析器——對這些情況有不同的標準化處理方式,而大多數跨語言的 bug 都可以追溯到這兩種語法之間的落差。URL Parser 會直接在你的瀏覽器分頁中執行 WHATWG 演算法,並公開 JavaScript URL 與 URLSearchParams 物件所暴露的每個欄位,因此你可以貼上可疑的 URL、準確查看瀏覽器所見的內容,並複製標準化後的 JSON 結果來與 urlparse(...)._asdict() 進行比對。

how to parse url in python
在 Python 中解析 URL 並與瀏覽器進行驗證

Python 的 urllib.parse 會回傳六個頂層欄位

這是 Python 標準函式庫的路徑,也是大多數程式碼審查預期會看到的做法。匯入 urllib.parse 後即可使用 urlparse,它接受 URL 字串並回傳 SplitResult 或 ParseResult 具名 tuple。這六個屬性在所有 Python 版本中保持穩定,並且對 ASCII 輸入的行為一致,因此對於路由、記錄或從 referrer 取出主機名稱等工作來說仍然是合理的預設選擇。該具名 tuple 為唯讀,但你可以使用私有的 _asdict 輔助方法或將其傳入 dict() 來轉換成 dict。split 變體新增了 separator 關鍵字,可讓你解析查詢分隔符不是問號的類 URL 字串,這是一個 urllib.parse 有文件說明但大多數教學略過的選項。

urlparse 會在剛好兩種情況下默默地給出錯誤的答案:當主機包含 WHATWG 解析器會進行 Punycode 編碼的國際化字元時,以及當輸入在 netloc 中包含使用者名稱或密碼時。urlparse 會原封不動地保留 netloc 中的 userinfo 部分,且沒有提供任何屬性來去除它,因此任何 https://user:[email protected]/ 字串都會帶著嵌入的憑證通過 netloc,並在毫無警示的情況下進入你的記錄中。

urllib.parse 與瀏覽器解析器分歧之處

這種分歧並非任一函式庫的 bug,而是由不同團體維護的兩套語法。下表左欄對應 urllib.parse 屬性,右欄對應 URL Parser 輸出中最接近的對應欄位,實務上的差異記錄在第三欄。

urllib.parse (Python) URL Parser (WHATWG) 實務差異
scheme protocol urllib 回傳小寫的 scheme 名稱;WHATWG 會附加結尾冒號,回傳 "https:"。
netloc host、hostname、port urllib 回傳一個合併的字串;WHATWG 將其拆分為 host(含 port)、獨立的 hostname,以及獨立的 port 值。
path pathname urllib 回傳原始的 path 部分;WHATWG 會標準化前導斜線,並逐位元組保留百分比編碼。
params (無對應欄位) urllib 已棄用的分號 params 欄位在 WHATWG 中沒有對應項,且很少有用。
query search urllib 會去除前導的 "?";WHATWG 在 search 欄位中保留前導的 "?"。
fragment hash urllib 會去除前導的 "#";WHATWG 在 hash 欄位中保留前導的 "#"。
(無對應欄位) origin WHATWG 新增 origin 為 scheme + hostname + 有效的非預設連接埠;urllib 需要手動組合。
(無對應欄位) filename WHATWG 新增 filename 為 pathname 的最後一段;urllib 需要手動以 "/" 進行切割。

這兩個解析器在另外三種行為上也存在差異,這些差異會影響任何進行條件式路由、IDN 處理或查詢解碼的程式碼。首先,預設連接埠去除:輸入主機 example.com:443 為 15 個字元,標準化後的 URL Parser host 變為 example.com,即 11 個字元,減少的 4 個字元完全來自被去除的冒號與數字。其次,國際化主機名稱:包含 例え.jp 的 URL 在 URL Parser 的 hostname 欄位中會產生 example.xn--r8jz45g.xn--zckzah 的 ASCII 形式,而 urllib.parse 則會在 netloc 中保留原始的 Unicode 字元。第三,加號解碼:parse_qs 會將加號保留為解碼後值中的字面字元,但 URL Parser 會將其解碼為空格,因為 URLSearchParams 遵循 WHATWG 的 form-urlencoded 規則。這種差異最常出現在搜尋與 OAuth 回呼中,因為這些場景使用加號作為空格的替代字元。

解析絕對 URL

每當你想檢查瀏覽器實際上會如何處理在你的 Python 程式碼中行為異常的 URL 字串時,請使用 URL Parser。該工具完全在你的當前瀏覽器分頁中執行,絕不會將 URL 傳送到任何地方,因此對於包含內部主機名稱或不希望外洩的已簽署查詢 token 的正式環境 URL 來說是安全的。

  1. 將一個絕對的 HTTP 或 HTTPS URL 貼到輸入區域中,且不要包含使用者名稱或密碼,並在解析前檢視輸入字元數。輸入上限剛好為 8,192 個 UTF-16 字碼單位,任何周圍的空白、原始反斜線或 ASCII 控制字元都會使 URL 被拒絕,而不是被靜悄悄地修剪。
  2. 選擇 Parse URL 並回傳標準化後的元件,包括 protocol、origin、host、hostname、port、pathname、search、hash 與 filename。序列化後的 filename 為最後一個斜線之後的最後一段;以斜線結尾的 pathname 會產生空的 filename,而不是目錄標記。
  3. 讀取有序且已解碼的查詢參數列表,檢視任何可能敏感的 query 或 hash 資料,並視情況將完整的已解析 JSON 複製到剪貼簿。編輯輸入會清除舊的結果,因此失敗的 URL 絕不會讓先前成功的解析結果與新的錯誤同時顯示。

如果你還需要只專注於查詢部分而無需重新貼上整個 URL,同樣的 URLSearchParams 解碼規則也適用於 Parse Query Strings in JavaScript the Browser-Native Way 中所述的專屬工作流程。

讀取已解析的 JSON 輸出

輸出的 JSON 由瀏覽器 URL 與 URLSearchParams 物件所暴露的相同欄位構成。每個條目都採用兩個空格的格式,且完整文件的上限為 50,000 個 UTF-16 字碼單位;如果你的 URL 會產生超過該上限的內容,則整個結果會被拒絕,而不是被截斷。對 https://example.com:443/path/to/page?tag=python&tag;=web&q;=url+parsing#section 進行的典型解析會產生 protocol "https:"、origin "https://example.com"、host "example.com"、hostname "example.com"、port ""、pathname "/path/to/page"、search "?tag=python&tag;=web&q;=url+parsing"、hash "#section"、filename "page",以及一個由三個有序的 name-and-value 物件組成的 query 陣列:tag 等於 python、tag 等於 web,以及 q 等於 "url parsing",其中加號會被解碼為空格。query 陣列保留順序並允許重複,而不會將重複的鍵折合成 dict,這與 URLSearchParams 內部遵循的反重複折合規則相同。pathname 與 filename 中的百分比編碼會保留為百分比編碼,因此輸出與序列化後的 URL 在位元組層級保持一致;只有 query 名稱與值會被解碼。

URL Parser 在產生輸出前強制執行的規則

該工具刻意僅作為檢視器使用,拒絕充當其他任何角色。以非大小寫不敏感的 http:// 或 https:// 前綴開頭的輸入會在任何 URL 物件建構之前被拒絕,這代表像 /docs/page 這樣的路徑、像 //example.com/path 這樣的 scheme 相對引用,以及像 example.com 這樣的裸網域,都會產生明確的錯誤,而不是相對於當前頁面進行解析。經過前綴檢查後,只有 http: 與 https: 會被接受;javascript:、data:、file:、ftp:、blob:、mailto: 以及瀏覽器 URL API 原本會解析的其他 scheme 都會被拒絕,因為將它們呈現為似乎共享同一個 HTTP origin、host、port 與 query 模型,會誤導任何複製輸出的人。

包含非空的使用者名稱或密碼的 URL 會直接被拒絕:該工具不會遮罩、遮蔽或部分顯示 userinfo,因此像 https://user:[email protected] 這樣的 URL 會回傳憑證錯誤,並且不會產生任何已解析的欄位。query 參數陣列的上限剛好為 200 個條目;第 201 個 name-value 對會被拒絕,且先前的條目不會被靜悄悄地抽樣或略過。ASCII 控制字元會在建構 URL 之前被拒絕,因此瀏覽器無法靜悄悄地從貼上的內容中去掉換行或 tab。該工具連同不會對所提供的 URL 進行導航、不會執行連線檢查、不會測試 DNS,也絕不會聲稱已驗證網站是否安全或連結是否具有惡意。

如果你正在權衡選項,Convert XML to JSON in Python and in the Browser 對此有詳細說明。