C# 解析 URL URI 中的参数
C# 解析 URL URI 中的参数
文章目录
- 完整代码
- 概述
- 核心功能概述
- 查询参数解析方法对比
- 核心方法实现解析
- 1. 查询参数解析逻辑
- 2. URL构建逻辑
- 特殊场景处理说明
- 测试用例验证
- 使用注意事项
完整代码
namespace System
{using System.Collections.Generic;using System.Collections.Specialized;using System.Text;/// <summary>/// 函数<see cref="GetQueryDictionary(string, bool)"/> 和 函数<see cref="GetQueryCollection(string, bool)"/><br />/// 支持解析示例:<br />/// <![CDATA[ https://www.google.com/index?page=1&lang=eng ]]> <br />/// <![CDATA[ https://www.google.com/ ]]> <br />/// <![CDATA[ https://www.google.com/index? ]]> <br />/// <![CDATA[ https://www.google.com/index?page=title=index=1&lang=&chang&char=?&id=123 ]]> <br />/// </summary>public static class UrlHelper{#region Test 测试示例。/// <summary>/// 使用示例。/// </summary>[System.Diagnostics.Conditional("DEBUG")]public static void Test(){TestGetQueryNormal();TestGetQuerySpecial();TestGetUrlStringNormal();}/// <summary>/// 常见的使用示例。解析URL中的参数。/// </summary>[System.Diagnostics.Conditional("DEBUG")]public static void TestGetQueryNormal(){string urlTest1 = "https://www.google.com/index?page=1&lang=eng";string dictionaryBaseUrlTest1;var dictionaryTest1 = GetQueryDictionary(urlTest1, out dictionaryBaseUrlTest1);System.Diagnostics.Debug.Assert(dictionaryBaseUrlTest1 == "https://www.google.com/index" &&dictionaryTest1["page"] == "1" && dictionaryTest1["lang"] == "eng","GetQueryDictionary Test1");System.Diagnostics.Debug.Assert(GetUrlString(dictionaryBaseUrlTest1, dictionaryTest1) == urlTest1,"GetUrlString by Dictionary Test1");string collectionBaseUrlTest1;var collectionTest1 = GetQueryCollection(urlTest1, out collectionBaseUrlTest1);System.Diagnostics.Debug.Assert(collectionBaseUrlTest1 == "https://www.google.com/index" &&collectionTest1["page"] == "1" && collectionTest1["lang"] == "eng","GetQueryCollection Test1");System.Diagnostics.Debug.Assert(GetUrlString(collectionBaseUrlTest1, collectionTest1) == urlTest1,"GetUrlString by Collection Test1");string urlTest2 = "https://www.google.com/";string dictionaryBaseUrlTest2;GetQueryDictionary(urlTest2, out dictionaryBaseUrlTest2);System.Diagnostics.Debug.Assert(dictionaryBaseUrlTest2 == "https://www.google.com/","GetQueryDictionary Test2");string collectionBaseUrlTest2;GetQueryCollection(urlTest2, out collectionBaseUrlTest2);System.Diagnostics.Debug.Assert(collectionBaseUrlTest2 == "https://www.google.com/","GetQueryCollection Test2");string urlTest3 = "https://www.google.com/index?";string dictionaryBaseUrlTest3;GetQueryDictionary(urlTest3, out dictionaryBaseUrlTest3);System.Diagnostics.Debug.Assert(dictionaryBaseUrlTest3 == "https://www.google.com/index","GetQueryDictionary Test3");string collectionBaseUrlTest3;GetQueryCollection(urlTest3, out collectionBaseUrlTest3);System.Diagnostics.Debug.Assert(collectionBaseUrlTest3 == "https://www.google.com/index","GetQueryCollection Test4");}/// <summary>/// 不常见的使用示例。解析URL中的参数。/// </summary>[System.Diagnostics.Conditional("DEBUG")]public static void TestGetQuerySpecial(){string urlSpecial4 = "https://www.google.com/index?page=title=index=1&lang=&chang&char=?&id=123";string dictionaryBaseUrlSpecial4;var dictionarySpecial4 = GetQueryDictionary(urlSpecial4, out dictionaryBaseUrlSpecial4);System.Diagnostics.Debug.Assert(dictionaryBaseUrlSpecial4 == "https://www.google.com/index" &&dictionarySpecial4["page"] == "title=index=1" && dictionarySpecial4["lang"] == "" &&dictionarySpecial4["chang"] == null && dictionarySpecial4["char"] == "?" && dictionarySpecial4["id"] == "123","GetQueryDictionary UrlSpecial4");string collectionBaseUrlSpecial4;var collectionSpecial4 = GetQueryCollection(urlSpecial4, out collectionBaseUrlSpecial4);System.Diagnostics.Debug.Assert(collectionBaseUrlSpecial4 == "https://www.google.com/index" &&collectionSpecial4["page"] == "title=index=1" && collectionSpecial4["lang"] == "" &&collectionSpecial4["chang"] == null && collectionSpecial4["char"] == "?" && collectionSpecial4["id"] == "123","GetQueryCollection UrlSpecial4");string urlSpecial5 = "https://www.google.com/index?page=1&&lang=eng";string dictionaryBaseUrlSpecial5;var dictionarySpecial5 = GetQueryDictionary(urlSpecial5, out dictionaryBaseUrlSpecial5);System.Diagnostics.Debug.Assert(dictionaryBaseUrlSpecial5 == "https://www.google.com/index" &&dictionarySpecial5["page"] == "1" && dictionarySpecial5["lang"] == "eng","GetQueryDictionary UrlSpecial5");string collectionBaseUrlSpecial5;var collectionSpecial5 = GetQueryCollection(urlSpecial5, out collectionBaseUrlSpecial5);System.Diagnostics.Debug.Assert(collectionBaseUrlSpecial5 == "https://www.google.com/index" &&collectionSpecial5["page"] == "1" && collectionSpecial5["lang"] == "eng","GetQueryCollection UrlSpecial5");}/// <summary>/// 常见的使用示例。拼接URL的<paramref name="baseUrl"/>和URL中的参数。/// </summary>[System.Diagnostics.Conditional("DEBUG")]public static void TestGetUrlStringNormal(){string baseUrl1 = "https://www.google.com/index?page=1&lang=eng";System.Diagnostics.Debug.Assert(GetUrlString(baseUrl1, new Dictionary<string, string>(){["TestKey1"] = "TestValue1",["TestKey2"] = "TestValue2",}) == "https://www.google.com/index?page=1&lang=eng&TestKey1=TestValue1&TestKey2=TestValue2","GetUrlString by Dictionary BaseUrl1");System.Diagnostics.Debug.Assert(GetUrlString(baseUrl1, new NameValueCollection(){["TestKey1"] = "TestValue1",["TestKey2"] = "TestValue2",}) == "https://www.google.com/index?page=1&lang=eng&TestKey1=TestValue1&TestKey2=TestValue2","GetUrlString by Collection BaseUrl1");string baseUrl2 = "https://www.google.com/index";System.Diagnostics.Debug.Assert(GetUrlString(baseUrl2, new Dictionary<string, string>(){["TestKey1"] = "TestValue1",["TestKey2"] = "TestValue2",}) == "https://www.google.com/index?TestKey1=TestValue1&TestKey2=TestValue2","GetUrlString by Dictionary BaseUrl2");System.Diagnostics.Debug.Assert(GetUrlString(baseUrl2, new NameValueCollection(){["TestKey1"] = "TestValue1",["TestKey2"] = "TestValue2",}) == "https://www.google.com/index?TestKey1=TestValue1&TestKey2=TestValue2","GetUrlString by Collection BaseUrl2");}#endregion Test 测试示例。/// <summary>/// 解析URL中的参数。会覆盖重复键的值。<br />/// 注意:在<see cref="Dictionary{TKey, TValue}"/>中,/// 通过<see cref="Dictionary{TKey, TValue}"/>的<![CDATA[ this[TKey key] ]]>,/// 直接读取不存在的键值对时,会抛出异常。<br />/// </summary>/// <param name="url"></param>/// <param name="ignoreCase"></param>/// <returns></returns>public static Dictionary<string, string> GetQueryDictionary(string url, bool ignoreCase = false){string baseUrl;return GetQueryDictionary(url, out baseUrl, ignoreCase);}/// <summary>/// 解析URL中的参数。会覆盖重复键的值。<br />/// 注意:在<see cref="Dictionary{TKey, TValue}"/>中,/// 通过<see cref="Dictionary{TKey, TValue}"/>的<![CDATA[ this[TKey key] ]]>,/// 直接读取不存在的键值对时,会抛出异常。<br />/// </summary>/// <param name="url"></param>/// <param name="baseUrl"> URL中符号“?”的前面部分。</param>/// <param name="ignoreCase"></param>/// <returns></returns>public static Dictionary<string, string> GetQueryDictionary(string url, out string baseUrl, bool ignoreCase = false){StringComparer comparer;if (ignoreCase){comparer = StringComparer.InvariantCultureIgnoreCase;}else{comparer = StringComparer.InvariantCulture;}return GetQueryDictionary(url, out baseUrl, comparer);}/// <summary>/// 解析URL中的参数。会覆盖重复键的值。<br />/// 注意:在<see cref="Dictionary{TKey, TValue}"/>中,/// 通过<see cref="Dictionary{TKey, TValue}"/>的<![CDATA[ this[TKey key] ]]>,/// 直接读取不存在的键值对时,会抛出异常。<br />/// </summary>/// <param name="url"></param>/// <param name="baseUrl"> URL中符号“?”的前面部分。</param>/// <param name="comparer"></param>/// <returns></returns>public static Dictionary<string, string> GetQueryDictionary(string url, out string baseUrl, StringComparer comparer){baseUrl = null;// 第一个“?”符号的下标。// 用于支持,参数中包括“?”符号的URL。int indexQuestionMark = url.IndexOf('?');int countQuery = url.Length - indexQuestionMark - 1;Dictionary<string, string> info = null;// 如果URL中包括有效的参数。if (indexQuestionMark > -1 && countQuery > 0){string queryString = url.Substring(indexQuestionMark + 1, countQuery);// 为空的键值对没有意义,所以,舍弃为空的键值对。string[] keyAndValuePairs = queryString.Split(new char[] { '&' }/*, StringSplitOptions.RemoveEmptyEntries*/);info = new Dictionary<string, string>(keyAndValuePairs.Length + 1, comparer);foreach (var pair in keyAndValuePairs){// 第一个“=”符号的下标。// 用于支持,value中包括“=”符号的参数。int indexEquals = pair.IndexOf('=');// 如果包含“=”符号。if (indexEquals > -1){string key = pair.Substring(0, indexEquals);key = Uri.UnescapeDataString(key);int countValue = pair.Length - indexEquals - 1;// 避免“=”符号后面没有内容时,indexEquals + 1超出数组的有效索引。string value = (countValue == 0) ? string.Empty : pair.Substring(indexEquals + 1, countValue);value = Uri.UnescapeDataString(value);info[key] = value;}// 用关键字保存特殊参数。else{info[pair] = null;}}}info = info ?? new Dictionary<string, string>(1, comparer);if (indexQuestionMark < 0 || indexQuestionMark > url.Length){indexQuestionMark = url.Length;}// URL中符号“?”的前面部分。baseUrl = url.Substring(0, indexQuestionMark);return info;}/// <summary>/// 解析URL中的参数。支持重复键。<br />/// 注意:在<see cref="NameValueCollection"/>中,直接读取不存在的键值对时,返回 null ,不会抛出异常。<br />/// </summary>/// <param name="url"></param>/// <param name="ignoreCase"></param>/// <returns></returns>public static NameValueCollection GetQueryCollection(string url, bool ignoreCase = false){string baseUrl;return GetQueryCollection(url, out baseUrl, ignoreCase);}/// <summary>/// 解析URL中的参数。支持重复键。<br />/// 注意:在<see cref="NameValueCollection"/>中,直接读取不存在的键值对时,返回 null ,不会抛出异常。<br />/// </summary>/// <param name="url"></param>/// <param name="baseUrl"> URL中符号“?”的前面部分。</param>/// <param name="ignoreCase"></param>/// <returns></returns>public static NameValueCollection GetQueryCollection(string url, out string baseUrl, bool ignoreCase = false){StringComparer comparer;if (ignoreCase){comparer = StringComparer.InvariantCultureIgnoreCase;}else{comparer = StringComparer.InvariantCulture;}return GetQueryCollection(url, out baseUrl, comparer);}/// <summary>/// 解析URL中的参数。支持重复键。<br />/// 注意:在<see cref="NameValueCollection"/>中,直接读取不存在的键值对时,返回 null ,不会抛出异常。<br />/// </summary>/// <param name="url"></param>/// <param name="baseUrl"> URL中符号“?”的前面部分。</param>/// <param name="comparer"></param>/// <returns></returns>public static NameValueCollection GetQueryCollection(string url, out string baseUrl, StringComparer comparer){// 第一个“?”符号的下标。// 用于支持,参数中包括“?”符号的URL。int indexQuestionMark = url.IndexOf('?');int countQuery = url.Length - indexQuestionMark - 1;NameValueCollection info = null;// 如果URL中包括有效的参数。if (indexQuestionMark > -1 && countQuery > 0){string queryString = url.Substring(indexQuestionMark + 1, countQuery);// 为空的键值对没有意义,所以,舍弃为空的键值对。string[] keyAndValuePairs = queryString.Split(new char[] { '&' }/*, StringSplitOptions.RemoveEmptyEntries*/);info = new NameValueCollection(keyAndValuePairs.Length + 1, comparer);foreach (var pair in keyAndValuePairs){// 第一个“=”符号的下标。// 用于支持,value中包括“=”符号的参数。int indexEquals = pair.IndexOf('=');// 如果包含“=”符号。if (indexEquals > -1){string key = pair.Substring(0, indexEquals);key = Uri.UnescapeDataString(key);int countValue = pair.Length - indexEquals - 1;// 避免“=”符号后面没有内容时,indexEquals + 1超出数组的有效索引。string value = (countValue == 0) ? string.Empty : pair.Substring(indexEquals + 1, countValue);value = Uri.UnescapeDataString(value);info.Add(key, value);}// 用关键字保存特殊参数。else{info.Add(pair, null);}}}info = info ?? new NameValueCollection(1, comparer);if (indexQuestionMark < 0 || indexQuestionMark > url.Length){indexQuestionMark = url.Length;}// URL中符号“?”的前面部分。baseUrl = url.Substring(0, indexQuestionMark);return info;}/// <summary>/// 拼接URL的<paramref name="baseUrl"/>(可以是URL中符号“?”的前面部分,也可以是已经包含参数的URL)和URL中的参数。/// </summary>/// <param name="baseUrl"></param>/// <param name="parameters"></param>/// <returns></returns>public static string GetUrlString(string baseUrl, ICollection<KeyValuePair<string, string>> parameters){if (parameters != null && parameters.Count > 0){var indexEndSplit = baseUrl.LastIndexOf('/');if (indexEndSplit < 0){indexEndSplit = 0;}StringBuilder builder = new StringBuilder(baseUrl.Length + 128 + 1);var indexQuestionMark = baseUrl.IndexOf('?', indexEndSplit);if (indexQuestionMark < 0){builder.Append(baseUrl);bool hasQueryItem = false;bool addQuestionMark = true;GetUrlQueryStringCore(parameters, builder, hasQueryItem, addQuestionMark);}else{builder.Append(baseUrl);bool hasQueryItem = indexQuestionMark < baseUrl.Length - 1;bool addQuestionMark = false;GetUrlQueryStringCore(parameters, builder, hasQueryItem, addQuestionMark);}return builder.ToString();}return baseUrl;}/// <summary>/// 拼接URL的<paramref name="baseUrl"/>(可以是URL中符号“?”的前面部分,也可以是已经包含参数的URL)和URL中的参数。/// </summary>/// <param name="baseUrl"></param>/// <param name="parameters"></param>/// <returns></returns>public static string GetUrlString(string baseUrl, NameValueCollection parameters){if (parameters != null && parameters.Count > 0){var indexEndSplit = baseUrl.LastIndexOf('/');if (indexEndSplit < 0){indexEndSplit = 0;}StringBuilder builder = new StringBuilder(baseUrl.Length + 128 + 1);var indexQuestionMark = baseUrl.IndexOf('?', indexEndSplit);if (indexQuestionMark < 0){builder.Append(baseUrl);bool hasQueryItem = false;bool addQuestionMark = true;GetUrlQueryStringCore(parameters, builder, hasQueryItem, addQuestionMark);}else{builder.Append(baseUrl);bool hasQueryItem = indexQuestionMark < baseUrl.Length - 1;bool addQuestionMark = false;GetUrlQueryStringCore(parameters, builder, hasQueryItem, addQuestionMark);}return builder.ToString();}return baseUrl;}public static string GetUrlQueryString(ICollection<KeyValuePair<string, string>> parameters, bool addQuestionMark){StringBuilder builder = new StringBuilder(128);bool hasQueryItem = false;GetUrlQueryStringCore(parameters, builder, hasQueryItem, addQuestionMark);return builder.ToString();}public static string GetUrlQueryString(NameValueCollection parameters, bool addQuestionMark){StringBuilder builder = new StringBuilder(128);bool hasQueryItem = false;GetUrlQueryStringCore(parameters, builder, hasQueryItem, addQuestionMark);return builder.ToString();}private static void GetUrlQueryStringCore(ICollection<KeyValuePair<string, string>> parameters, StringBuilder builder, bool hasQueryItem, bool addQuestionMark){foreach (KeyValuePair<string, string> item in parameters){GetUrlQueryStringForAddParameterBefore(builder, ref hasQueryItem, addQuestionMark);builder.Append(Uri.EscapeDataString(item.Key));if (item.Value != null){builder.Append("=");builder.Append(Uri.EscapeDataString(item.Value));}}}private static void GetUrlQueryStringCore(NameValueCollection parameters, StringBuilder builder, bool hasQueryItem, bool addQuestionMark){foreach (object itemKeyObj in parameters.Keys){string itemKey = itemKeyObj?.ToString();if (itemKey != null){var itemValues = parameters.GetValues(itemKey);if (itemValues == null){GetUrlQueryStringForAddParameterBefore(builder, ref hasQueryItem, addQuestionMark);builder.Append(Uri.EscapeDataString(itemKey));}else{foreach (var itemValue in itemValues){GetUrlQueryStringForAddParameterBefore(builder, ref hasQueryItem, addQuestionMark);builder.Append(Uri.EscapeDataString(itemKey));if (itemValue != null){builder.Append("=");builder.Append(Uri.EscapeDataString(itemValue));}}}}}}private static void GetUrlQueryStringForAddParameterBefore(StringBuilder builder, ref bool hasQueryItem, bool addQuestionMark){if (hasQueryItem){builder.Append("&");}else{hasQueryItem = true;if (addQuestionMark){builder.Append('?');}}}}
}
概述
核心功能概述
-
查询参数解析:
GetQueryDictionary()
:将URL查询参数解析为字典(键值唯一,后值覆盖前值)GetQueryCollection()
:将URL查询参数解析为集合(支持多值键)
-
URL构建:
GetUrlString()
:将基础URL与参数集合/字典拼接成完整URL
-
特殊支持:
- 处理含特殊字符的键值(如
?
、=
) - 支持无值参数(如
&key
) - 处理参数中出现的等号和问号
- 处理含特殊字符的键值(如
查询参数解析方法对比
特性 | GetQueryDictionary | GetQueryCollection |
---|---|---|
返回值类型 | Dictionary<string, string> | NameValueCollection |
键唯一性 | ✔️(后值覆盖前值) | ✖️(支持多值键) |
读取不存在键 | 抛出异常 | 返回null |
参数格式 | 键=值(值中可含=) | 键=值(值中可含=) |
无值参数处理 | 键→null | 键→null |
核心方法实现解析
1. 查询参数解析逻辑
// 解析字典示例
int indexQuestionMark = url.IndexOf('?');
if (indexQuestionMark > -1)
{string queryString = url.Substring(indexQuestionMark + 1);string[] pairs = queryString.Split('&');foreach (var pair in pairs){int indexEquals = pair.IndexOf('=');if (indexEquals > -1){string key = Uri.UnescapeDataString(pair.Substring(0, indexEquals));string value = Uri.UnescapeDataString(pair.Substring(indexEquals + 1));dict[key] = value; // 字典直接赋值(覆盖)}else{dict[pair] = null; // 无值参数处理}}
}
baseUrl = url.Substring(0, indexQuestionMark);
2. URL构建逻辑
// URL拼接核心逻辑
private static void GetUrlQueryStringCore(NameValueCollection parameters, StringBuilder builder)
{foreach (string key in parameters.Keys){string[] values = parameters.GetValues(key);foreach (string value in values){// 添加分隔符(?或&)if (builder.Length > baseUrlLength) builder.Append('&');else if (needQuestionMark) builder.Append('?');builder.Append(Uri.EscapeDataString(key));if (value != null){builder.Append('=');builder.Append(Uri.EscapeDataString(value));}}}
}
特殊场景处理说明
-
值中含等号
使用首次出现的=
分割键值:// 示例参数:title=index=1 // 解析结果:key="title", value="index=1" int indexEquals = pair.IndexOf('=');
-
无值参数
被解析为key → null
:// 示例URL:https://site.com?key1&key2=val // 解析结果:key1=null, key2="val"
-
问号处理
仅识别第一个?
作为查询起始:// 示例:https://site.com?param=? // 正确解析:param="?" int indexQuestionMark = url.IndexOf('?');
测试用例验证
测试场景:
1. 标准URL解析✓ https://site.com?k1=v1&k2=v2 → k1="v1", k2="v2"2. 特殊字符处理✓ https://site.com?key=? → key="?"✓ https://site.com?k=1=2=3 → k="1=2=3"3. 边界情况✓ https://site.com? (空参数) → 空集合✓ https://site.com (无参数) → 空集合4. URL重建验证✓ 解析后重建URL应与原始URL一致
使用注意事项
-
字典读取风险
// 安全读取方式 if (dict.TryGetValue("key", out var value)) { ... }
-
编码规范
- 使用
Uri.EscapeDataString
处理特殊字符 - 使用
Uri.UnescapeDataString
反向解码
- 使用