C# 解析 URL URI 中的参数
完整代码
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
反向解码
- 使用