可是這小問題有些細節,若不整理一下,恐怕沒多久就會忘掉大半。
寫 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"),在查詢參數的部分則加了 空白字元、加號「+」小數點「.」、冒號「:」、問號「?」、驚嘆號「!」、單引號「'」等等。
.NET Framework 提供了以下幾個方法供我們選擇(有漏掉嗎?):
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 組件而已。
另外,我的 UrlEncode() 裡面呼叫了另一個函式 SplitToKeyValuePairs() 把查詢字串分解成一個 key-values 串列,這個函式的原始碼沒有列出來(後面有完整原始碼的下載網址)。
為了避免這兩個函式出什麼大亂子,我寫了點單元測試。程式碼如下:
[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 取得完整原始碼。
混亂的 URLENCODE
Don't use .NET System.Uri.UnescapeDataString in URL Decoding(2006 年的文章,仍然很有參考價值)
Encoding/Decoding URIs and HTML in the .NET 4 Client Profile
从此不再惧怕URI编码:JavaScript及C# URI编码详解
RFC3986