嗯,這看起來只是小問題,可是....
可是這小問題有些細節,若不整理一下,恐怕沒多久就會忘掉大半。
寫 JavaScript 時曾用過 encodeURIComponent,寫 .NET 程式時,印象中都是用 HttpUtility.UrlEncode()。最近寫 web API 時突然想到,如果需要一個 URL 編碼函式,它接受一個 URL 字串(裡面包含查詢參數),並且傳回適當編碼過的、伺服器端可正常處理的 URL 字串,已知的現成函式可以做到嗎?我的意思是,就像我們在瀏覽器的網址列輸入一整串完整 URL,按 Enter 之後,瀏覽器會把我們輸入的文字經過適當編碼之後再傳送給伺服器。這應該很簡單吧?
比如底下這個 URL:
Chrome 和 IE 實際送出的 HTTP 請求都會轉成:
http://whotest.com/a%20b/c?phone=+886&name=M.%20Tsai&msg=say:'hello?!'
為了盡量涵蓋各種狀況和夠多的特殊字元(如需要完整資訊請看 RFC 3986),我在測試網址中的路徑部分加了空白字元("a b"),在查詢參數的部分則加了 空白字元、加號「+」小數點「.」、冒號「:」、問號「?」、驚嘆號「!」、單引號「'」等等。
.NET Framework 提供了以下幾個方法供我們選擇(有漏掉嗎?):
編號 2 號的 HttpUtility.UrlPathEncode() 在官方文件裡面的說明文字就只一行:
Do not use; intended only for browser compatibility. Use UrlEncode.
直接告訴我們別用它了,所以可用選項剩下三個。其中 HttpUtility 類別屬於 System.Web.dll 組件,Uri 類別則是隸屬於基礎的 System.dll 組件。
有人老早寫了測試程式,還整理了很詳細的測試結果和對照表格。但我還是自己實驗了一下,再根據實際的觀察來兜出那個我想要的 URL 編碼函式。
先看實驗的部分:
解碼暫且忽略,目前我只關心編碼的結果。如下圖:
圖中三處黃色標示的文字就是三種方法編碼出來的結果。結果沒有一個是我要的,因為...
(迷之音:有沒有這麼複雜啊 Orz)
這些細微差異,如果有一份對照表會更清楚。先前提過的那篇文章裡面就有整理對照表:Don't use .NET System.Uri.UnescapeDataString in URL Decoding。
這是常見問題,我想應該很多人早就有自己的解法了。底下是我為自己寫的工具函式,包含編碼和解碼。
程式碼裡面已經有註解(菜英文,請包涵),就不細說了。僅提一下重點:
單元測試
為了避免這兩個函式出什麼大亂子,我寫了點單元測試。程式碼如下:
只是簡單地測試一下編碼和解碼的基本功能,以及空白字元轉 '+' 號(前面提過)和重複編碼(double encoding)的狀況,以確保兩次編碼之後再做兩次解碼,仍能夠還原成初始的 URL。當然啦,實際寫程式時,能夠避免二次編碼/解碼是最好。
我把這兩個函式以及單元測試都放到最近建立的 Yalib 專案裡,類別全名是 Yalib.StrHelper。若有興趣試試,可下載 NuGet 套件,亦可至 GitHub 取得完整原始碼。
參考資料
可是這小問題有些細節,若不整理一下,恐怕沒多久就會忘掉大半。
寫 JavaScript 時曾用過 encodeURIComponent,寫 .NET 程式時,印象中都是用 HttpUtility.UrlEncode()。最近寫 web API 時突然想到,如果需要一個 URL 編碼函式,它接受一個 URL 字串(裡面包含查詢參數),並且傳回適當編碼過的、伺服器端可正常處理的 URL 字串,已知的現成函式可以做到嗎?我的意思是,就像我們在瀏覽器的網址列輸入一整串完整 URL,按 Enter 之後,瀏覽器會把我們輸入的文字經過適當編碼之後再傳送給伺服器。這應該很簡單吧?
比如底下這個 URL:
http://whotest.com/a b/c?phone=+886&name=M. Tsai&msg=say:'hello?!'
Chrome 和 IE 實際送出的 HTTP 請求都會轉成:
http://whotest.com/a%20b/c?phone=+886&name=M.%20Tsai&msg=say:'hello?!'
為了盡量涵蓋各種狀況和夠多的特殊字元(如需要完整資訊請看 RFC 3986),我在測試網址中的路徑部分加了空白字元("a b"),在查詢參數的部分則加了 空白字元、加號「+」小數點「.」、冒號「:」、問號「?」、驚嘆號「!」、單引號「'」等等。
- HttpUtility.UrlEncode()
- HttpUtility.UrlPathEncode()
- Uri.EscapeUriString()
- Uri.EscapeDataString()
編號 2 號的 HttpUtility.UrlPathEncode() 在官方文件裡面的說明文字就只一行:
Do not use; intended only for browser compatibility. Use UrlEncode.
直接告訴我們別用它了,所以可用選項剩下三個。其中 HttpUtility 類別屬於 System.Web.dll 組件,Uri 類別則是隸屬於基礎的 System.dll 組件。
有人老早寫了測試程式,還整理了很詳細的測試結果和對照表格。但我還是自己實驗了一下,再根據實際的觀察來兜出那個我想要的 URL 編碼函式。
先看實驗的部分:
static void Main(string[] args) { string input = "http://whotest.com/a b/c?phone=+886&name=M. Tsai&msg=say:'hello?!'"; Console.WriteLine("input: \n{0}", input); string encoded = System.Web.HttpUtility.UrlEncode(input); string decoded = System.Web.HttpUtility.UrlDecode(encoded); Console.WriteLine(new string('=', 70)); Console.WriteLine("HttpUtility.UrlEncode/UrlDecode: \n{0}\n{1}", encoded, decoded); encoded = Uri.EscapeUriString(input); decoded = Uri.UnescapeDataString(encoded); Console.WriteLine(new string ('=', 70)); Console.WriteLine("Uri.EscapeUriString/UnescapeDataString: \n{0}\n{1}", encoded, decoded); encoded = Uri.EscapeDataString(input); decoded = Uri.UnescapeDataString(encoded); Console.WriteLine(new string('=', 70)); Console.WriteLine("Uri.EscapeDataString/UnescapeDataString: \n{0}\n{1}", encoded, decoded); }
解碼暫且忽略,目前我只關心編碼的結果。如下圖:
圖中三處黃色標示的文字就是三種方法編碼出來的結果。結果沒有一個是我要的,因為...
- HttpUtility.UrlEncode() 連網址的路徑部分都編碼了,例如 http:// 變成 http%3a%2f%2f,是無效的網址。
- Uri.EscapeUriString() 不會弄壞網址的路徑部分,對於空白字元的處理也沒問題(編碼成 "%20",可是查詢字串的部分就漏掉很多符號,例如 '+' 號,這個不處理是不行的。
- Uri.EscapeDataString() 的結果跟 HttpUtility.UrlEncode() 幾乎一樣,只有兩個差別:
(1) Uri.EscapeDataString() 在編碼時採用大寫 16 進制字元,例如 %3A。HttpUtility.UrlEncode() 則是小寫。根據 RFC 文件,使用 % 十六進位編碼時,大小寫視為相同,但為求一致性,建議採用大寫。
(2) Uri.EscapeDataString() 碰到空白字元時會轉成 %20,HttpUtility.UrlEncode() 則會轉成 '+' 號。
至於 HttpUtility.UrlEncode() 碰到空白字元會轉成 '+' 號,這是 OK 的。在 application/x-www-form-encoded 類型的文件裡,空白字元可以編碼成 '+' 號(據說定義在 RFC 2396 裡面,我沒去查 :p)。換言之,URL 查詢字串中的空白字元既可以編碼成 "%20",亦可編碼成 "+" 號。
(迷之音:有沒有這麼複雜啊 Orz)
這些細微差異,如果有一份對照表會更清楚。先前提過的那篇文章裡面就有整理對照表:Don't use .NET System.Uri.UnescapeDataString in URL Decoding。
自己寫編解碼函式
/// <summary> /// Encoding a URL. Basically the Path Component is encoded with Uri.EscapeUriString, and the Query Component is encoded with Uri.EscapeDataString. /// </summary> /// <param name="input"></param> /// <returns></returns> public static string UrlEncode(string input) { var aUri = new Uri(input, true); // 'true' means don't encode it, or else the space characters will be double encoded! // Parse query string to a name-value collection. The first '?' is removed and remained '?' characters will be encoded. var queryParams = SplitToKeyValuePairs(aUri.Query.TrimStart('?'), '&', '='); // Do NOT use HttpUtility.ParseQueryString(aUri.Query) because it does encode. // Rebuilding and encoding query string. var sb = new StringBuilder(); foreach (var item in queryParams) { sb.AppendFormat("{0}={1}&", Uri.EscapeDataString(item.Key), Uri.EscapeDataString(item.Value)); } sb.Remove(sb.Length - 1, 1); // Remove last '&' string result = String.Format("{0}?{1}", Uri.EscapeUriString(aUri.GetLeftPart(UriPartial.Path)), sb.ToString()); return result; } /// <summary> /// Decoding a URL. /// </summary> /// <param name="input"></param> /// <returns></returns> public static string UrlDecode(string input) { if (input == null) { throw new ArgumentNullException("input"); } // Since Uri.UnescapeDataString() does not decode plus sign ('+') to space character, we do it manually. // Yes, System.Web.HttpUtility.UrlDecode() can do this, I just don't want to involve System.Web.dll here. return Uri.UnescapeDataString(input.Replace('+', ' ')); }
程式碼裡面已經有註解(菜英文,請包涵),就不細說了。僅提一下重點:
- UrlEncode() 函式負責編碼,採取的策略是以 Uri.EscapeUriString() 來編碼路徑的部分,並且用 Uri.EscapeDataString() 來編碼查詢字串的部分。
- UrlDecode() 函式負責解碼,其中有針對 "+" 號做額外處理。其實這部分只要用 System.Web.HttpUtility.UrlDecode() 一行就能解決,只是我不想在這裡引用 System.Web 組件而已。
單元測試
為了避免這兩個函式出什麼大亂子,我寫了點單元測試。程式碼如下:
[TestClass] public class StrHelperUnitTest { [TestMethod] public void TestUrlEncode() { string input = "http://xyz.com/test?tel=+1732123456&name=M. Tsai"; string expected = "http://xyz.com/test?tel=%2B1732123456&name=M.%20Tsai"; string encoded = StrHelper.UrlEncode(input); Assert.AreEqual(encoded, expected, true); // decode string decoded = StrHelper.UrlDecode(encoded); Assert.AreEqual(decoded, input, true); // test decoding '+' to space characters. decoded = StrHelper.UrlDecode("How+are+you"); Assert.AreEqual(decoded, "How are you", true); // test double encoding then decoding. encoded = StrHelper.UrlEncode(StrHelper.UrlEncode(input)); decoded = StrHelper.UrlDecode(StrHelper.UrlDecode(encoded)); Assert.AreEqual(decoded, input); } }
只是簡單地測試一下編碼和解碼的基本功能,以及空白字元轉 '+' 號(前面提過)和重複編碼(double encoding)的狀況,以確保兩次編碼之後再做兩次解碼,仍能夠還原成初始的 URL。當然啦,實際寫程式時,能夠避免二次編碼/解碼是最好。
我把這兩個函式以及單元測試都放到最近建立的 Yalib 專案裡,類別全名是 Yalib.StrHelper。若有興趣試試,可下載 NuGet 套件,亦可至 GitHub 取得完整原始碼。
參考資料
沒有留言: