剖析一個 URL 查詢字串,指的是把 URL 中問號後面的那部分文字——也就是那種由一連串鍵與值的配對、以連字符號 & 串接起來的內容——轉換成程式能夠讀取、記錄、驗證,或修改的結構化形式。轉換後的結果,通常是一個純物件或映射表,其中每一個參數名稱,都對應到它解碼後的值,重複出現的名稱會被收集成陣列,保留字元也會被還原成原本的字面形式。解碼規則遵循 WHATWG URL Standard:百分比逸出字元會解析成位元組、加號會變成空格,UTF-8 位元組序列,則會轉換回原本的 Unicode 字元。一個實用的剖析器,還必須決定該如何處理空值、缺少等號的情況,以及重複命名的參數,因為這些選擇,會決定下游程式,收到的是不是伺服器實際預期的資料。Query String Parser 這個工具,會在瀏覽器中使用與瀏覽器原生的 URLSearchParams API 相同的模型,執行這項轉換,而且它也能反過來,從一個扁平的 JSON 物件重新建構出一個查詢字串。

how to parse
how to parse

一個查詢字串實際上包含了什麼

在執行剖析之前,先了解查詢字串是什麼、又不是什麼,會很有幫助。查詢字串,是 URL 中緊接在問號之後、用來把參數帶給伺服器的那一部分。每一個參數,都用一個等號,把名稱與值分隔開來,而參數之間,則用連字符號 & 串接起來。一個典型的例子,可能是像這樣的搜尋網址:/search?q=hello+world&page=2&tag=news&tag=tech,在這個例子中,兩個 tag,是以重複鍵的形式出現,而不是一個以逗號分隔的單一字串。

有三個細節,讓查詢字串很難用手動的方式正確處理。第一,百分比逸出字元,會為任何不屬於純 ASCII 的位元組編碼:一個空格,可能會以 %20 的形式出現,而像 é 這樣的非 ASCII 字元,則會以 %C3%A9 這串位元組序列的形式出現。第二,加號在這種格式中,代表的是空格,因此對「hello world」這個字面搜尋詞來說,實際上會以 hello+world 的形式送出,並且必須被解碼回一個空格字元。第三,同一個參數名稱,可能會出現不只一次,而接收端,必須決定要保留兩者、只保留一個、把它們合併成一個列表,還是依位置挑出特定的某一次出現。

正因為有這些慣例存在,把查詢字串當成一般字串、單純依連字符號 & 與等號來切割,只在最簡單的情況下夠用,一旦某個值裡包含了字面上的連字符號 &、等號,或非 ASCII 字母,就會出問題。一個專門設計的剖析器,會依照一套文件記載的規則,去解析這些位元組,而不是用猜的,這也是為什麼 API 除錯與請求記錄,都仰賴瀏覽器內部所使用的同一套表單解碼模型。

如何把查詢字串剖析成 JSON

  1. 開啟Query String Parser 工具,並保持預設方向「query to JSON」不變。
  2. 把查詢字串部分貼到輸入欄位中。你可以加上開頭的問號,也可以省略,兩種形式都能被接受。
  3. 執行轉換。這個工具會解碼百分比逸出字元、把加號轉換回空格,並把參數列表轉換成一個扁平的 JSON 物件。
  4. 檢查輸出結果中,是否有重複的名稱(會依出現順序,呈現為陣列),以及是否有等號右側刻意留白、沒有內容的空值。
  5. 複製這份 JSON,並對照接收端的 API 進行驗證:送出一個帶有剖析後參數的請求,確認伺服器解讀的結果,與你的預期一致。

同一個方向,在記錄一個請求、從擷取到的網址建立測試夾具,或逐個參數比較兩個請求時,都很實用。因為整個轉換都在本機完成,這一步驟中,查詢文字不會離開瀏覽器,而且輸出結果,是一段不含開頭問號的查詢字串,因此你可以直接把它貼進網址前綴、請求主體,或儲存的記錄中,不需要額外多做一次修剪。

從 JSON 反過來重新建構查詢字串

反方向的轉換,跟剖析同樣實用。給定一個扁平的 JSON 物件,你就能為一段網址、一個請求主體,或一份儲存的測試夾具,重新建構出一段編碼過的查詢字串。這個工具接受字串、有限的 JSON 數字、布林值、null,以及由這些純量值組成的陣列,然後套用與瀏覽器處理表單時相同的編碼規則。單一次出現的鍵,會維持為字串,而陣列,則會依原本順序保留每一個值,讓接收端伺服器,看到的是與來源端相同的重複項目。

Null 會變成一個空值,而 JSON 數字與布林值,則會變成它們的文字形式。空陣列會被捨棄,因為它們不會貢獻任何需要編碼的參數,而巢狀物件,則會被直接拒絕。最後這項限制,反映的是這種傳輸格式的一個真實限制:查詢字串,並沒有一套所有人都同意的方式,來序列化巢狀資料,因此括號、以點分隔的名稱、帶索引的名稱,以及內嵌的 JSON,都是彼此互相競爭的慣例。如果悄悄選定其中一種,輸出結果,對另一個剖析器來說,可能會代表完全不同的意思,因此這個工具,選擇不去用猜的。

輸出結果,基於與剖析方向相同的理由,同樣省略了開頭的問號。呼叫端,可能需要把參數放進網址前綴、放進請求主體、獨立記錄它們,或是拿去跟一個基準值做比較,而在這些情境中的任何一種,一個多出來的問號,都會打斷下一步的處理。

重複值、空值,以及型別是如何被處理的

情境來源形式在相反格式中的結果
單一次出現?tag=news{"tag":"news"}
重複的鍵?tag=news&tag;=tech{"tag":["news","tech"]}
加號作為空格?q=hello+world{"q":"hello world"}
百分比逸出字元?name=%C3%A9{"name":"é"}
空值?debug={"debug":""}
缺少等號?flag{"flag":""}
值中含有保留字元?note=a%26b{"note":"a&b;"}

型別推論,是刻意不執行的。位元組序列 2 在傳輸過程中是以文字形式送達的,因此它會變成 JSON 字串 "2",而不是數字 2。如果你的消費端預期的是一個有型別的值,請在剖析之後自行轉型,而不要仰賴剖析器去猜測。想了解這個工具遵循的完整解碼規則,請參閱MDN URLSearchParams referenceWHATWG URL Standard

限制、已簽章的網址,以及不該放進查詢字串裡的內容

這個工具,把每次轉換的輸入上限設定為 200,000 個字元,這遠遠超過一般請求所需要的規模,但又足夠小,能讓頁面保持順暢反應。每一項操作,都在目前的分頁中執行,因此查詢文字、JSON、權杖,以及網址,都不會被上傳到伺服器。這種只在本機執行的保證雖然有用,但並不能抹去一個更大的現實:查詢字串,經常會出現在瀏覽器歷史紀錄、伺服器日誌、代理伺服器、referrer 標頭,以及分析工具中。工作階段識別碼、電子郵件地址、搜尋關鍵字,以及追蹤標籤,經常都會透過這條路徑外洩,無論你用的是哪一種剖析器。在分享剖析結果之前,請先遮蔽敏感資料,或者,乾脆完全不要把機密內容放進網址裡。

如果你需要檢視的是整個網址,而不只是它的查詢部分,請先把那個網址,送進一個專門的網址剖析器。把一個完整的網址,貼進一個只處理查詢字串的剖析器,可能會把路徑跟參數名稱搞混,因為這種剖析,並不會驗證一個完整網址、主機名稱、路徑、片段、簽章,或授權政策。重新排序、去除重複,或重新編碼查詢參數,也可能會讓已簽章的網址失效,因為簽章可能涵蓋了參數順序、重複出現的次數、百分比逸出字元的寫法,以及加號與 %20 之間的選擇。當某項協定,定義了官方的正規化方式時——舉例來說,某種 OAuth 簽章演算法,或某種付款請求格式——請改用該協定自己的編碼器,而不是一個通用的轉換工具。Query String Parser 的設計目的,是用於一般的請求檢視、日誌測試夾具,以及測試資料,而不是用來重建已簽章的酬載內容。

因為這個工具,遵循的是 WHATWG 的 application/x-www-form-urlencoded 規則,你在瀏覽器中看到的行為,會跟這個轉換工具產生的結果一致,而鎖定了單一配對、重複名稱、加號轉空格的解碼、UTF-8 文字、缺少等號、空值、保留字元,以及可省略的開頭問號的測試夾具,涵蓋了大多數團隊會遇到的情況。當接收端伺服器,使用的是不同的慣例時——例如只保留第一個值、只保留最後一個值、永遠建立陣列,或依逗號切割——請先確認它的規範,再把剖析後的資料,當成標準答案來信任。

延伸閱讀:How to Convert SVG Image to Base64 Data URLs

延伸閱讀:How to Convert a Unix Timestamp in SQL Queries