處理含中文字符的 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 傳輸。
- 不管選哪條路,關鍵在于「入口唯一化」:只讓一個地方負責轉義/解碼,就不會踩坑。