处理含中文字符的 URL
1 为什么会出现“乱码”或崩溃?
- URL 标准(RFC 3986)规定:除少数保留字符外,URL 只能包含 ASCII。中文属于 Unicode,因此必须先转换。
- 如果直接把
https://example.com/路径/
这样的字符串传给URL(string:)
,Swift 会把它视为 非法,初始化直接返回nil
,后续网络请求也会失败。 - 主机名部分(如
网址.中国
)可以使用 IDN(Punycode)隐式转换;路径 / 查询 / 片段 不会自动转义,必须开发者处理。
2 Swift /Foundation 的行为细节
位置 | 直接支持中文? | 需要开发者操作 | 典型失败表现 |
---|---|---|---|
scheme / host | ✅(自动转 Punycode) | 无 | – |
path / query / fragment | ❌ | 必须百分号编码 | URL(string:) == nil |
URLComponents / URLQueryItem | ✅(自动做正确编码) | 建议使用 | – |
绝对语句:未编码的中文字符出现在路径、查询或片段里时,
URL(string:)
一定 返回nil
,而不是“通常”。
3 安全构造 URL 的 3 个方法
// ✅ 方法 1:让系统帮你转义
let raw = "https://www.example.com/搜索?q=中文"
let encoded = raw.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let url = URL(string: encoded)! // 一定成功
// ✅ 方法 2:URLComponents 适合拼接 Query
var comps = URLComponents(string: "https://www.example.com/搜索")!
comps.queryItems = [URLQueryItem(name: "q", value: "中文")
]
let url = comps.url! // 已正确编码
// ✅ 方法 3:只编码 path
let path = "路径/子路径".addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
let url = URL(string: "https://example.com/\(path)")!
场景 | 推荐字符集 |
---|---|
path | .urlPathAllowed |
query key/value | .urlQueryAllowed |
fragment | .urlFragmentAllowed |
4 常见坑
-
双重编码
- 调用两次
addingPercentEncoding
会把%
再转义成%25
。 - 解决:只在“最终拼接”前调用一次,或依赖
URLComponents
。
- 调用两次
-
手工拼接 Query
"q=\(value)"
如果value
含&
会截断。- 用
URLQueryItem
永不过错。
-
服务器端未解码
- 有些后端接口期望收到 UTF-8 原文:确认后端是否已自动
percentDecode
。
- 有些后端接口期望收到 UTF-8 原文:确认后端是否已自动
-
UIWebView/WKWebView 打开中文 URL
- 同样先编码,或使用
URLRequest(url:)
传入已编码的URL
。
- 同样先编码,或使用
5 测试与调试
import XCTestfunc testChineseURL() {let comps = URLComponents(string: "https://域名.中国/搜索")!XCTAssertNotNil(comps.url) // host 已转 PunycodeXCTAssertEqual(comps.url!.absoluteString,"https://xn--ihq79i.xn--fiqs8s/%E6%90%9C%E7%B4%A2")
}
- Xcode 网络抓包:在 Console 或 Charles 中确认最终请求的 URL。
- Unit Test:固化编码逻辑,避免后期回归。
6 总结
- 中文主机名:Foundation 会自动 Punycode,无需关心。
- 路径 / 查询 / 片段:必须 百分号编码,否则 Swift 一定失败。
- 最佳实践:
URLComponents
+URLQueryItem
,让系统帮你一次性处理所有细节。 - 按照上面的套路,任何含中文字符的 URL 在 Swift 中都能被安全、稳定地使用。
by 侯仕奇 | sqi.io
所以最简单直接的方法就是严格规定:URL 中不允许使用中文 ?
可以,但你要知道这是「团队约定」,不是网络协议要求
做法 | 优点 | 明确缺点 |
---|---|---|
硬性规定:所有接口只接受 ASCII(即先手动转义) | - 团队心智负担最低 - 服务器端解析简单,不用担心不同语言框架的容错 | - 可读性差,日志里全是 %E6%B5%8B%E8%AF%95 - SEO / 分享链接体验变差 - 第三方跳转(微信、微博等)容易出现两次转义或未转义问题 |
允许中文,但入口统一做一次 percent‐encode (推荐) | - 代码可读(/搜索?q=中文 )- 外部 copy-paste 时肉眼友好 - 仍然保证传输符合 RFC 3986 | - 需要在 SDK 或 API Gateway 层加一段轻量工具方法 - 需要写单元测试防止误双重转义 |
为什么“完全禁用中文”并不总是最佳
- 现代浏览器与搜索引擎本来就支持 Unicode URL;直接禁用会牺牲人类可读性。
- 移动端分享体验:用户复制的往往是裸中文 URL,如果后台 400,用户体验会受损。
- 多语言产品:电商、文档类站点需要保留自然语言 slug(
/产品/苹果手机
)提升可维护性。
推荐落地方案
extension String {/// 将任何路径或查询中的非 ASCII 部分一次性安全转义func urlEncodedPath() -> String {addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!}
}// ⚙️ 统一在网络层 (e.g. APIClient) 使用
func makeRequest(path: String, query: [String: String] = [:]) -> URLRequest {var comps = URLComponents()comps.scheme = "https"comps.host = "example.com"comps.percentEncodedPath = path.urlEncodedPath()comps.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) }return URLRequest(url: comps.url!)
}
- 任何进入网络层的字符串都可含中文;
- 网络层保证 encode 一次且仅一次;
- 服务器端:若使用现代框架(Node/Go/Java/Spring)通常会自动解码
%xx
,无需额外处理。
结论:
- 若团队小、接口固定,强行禁止中文确实最省事,但长期会降低可维护性。
- 更稳健的做法是允许中文输入 → 统一转义 → 所有链路都用合法 ASCII URL 传输。
- 不管选哪条路,关键在于「入口唯一化」:只让一个地方负责转义/解码,就不会踩坑。