這是一份從基礎(chǔ)到深入的實(shí)戰(zhàn)手冊,目標(biāo)是讓初學(xué)者一步步吃透“字符、碼點(diǎn)、編碼、存儲、傳輸、轉(zhuǎn)換、安全”等全部要點(diǎn)。內(nèi)容覆蓋我們討論到的每個知識點(diǎn),不留空缺。
一、從“字符”到“字節(jié)”:概念坐標(biāo)系
- 字符(character):人類看到/理解的符號(如 A、中、??)。
- 碼點(diǎn)(code point):Unicode 給字符分配的編號,用 U+XXXX 表示。例:A 是 U+0041,“中”是 U+4E2D,“??”是 U+1F600。
- 碼元(code unit):某種 Unicode 變體編碼的最小存儲單元。
- UTF-8 的碼元是 8 位字節(jié)。
- UTF-16 的碼元是 16 位(兩個字節(jié))。
- UTF-32 的碼元是 32 位(四個字節(jié))。
- 字節(jié)序列(byte sequence):真正寫入磁盤/網(wǎng)絡(luò)的數(shù)據(jù),是編碼后的結(jié)果。
- 字形簇(grapheme cluster):用戶感知的“一個字符”。可能由多個碼點(diǎn)組成(如“國”+ 變體選擇符、emoji 組合、含膚色/性別/ZWJ 的序列)。
核心區(qū)分:
- Unicode 是“字符-碼點(diǎn)”的字典與語義標(biāo)準(zhǔn)。
- UTF-8/16/32 是把碼點(diǎn)編碼為字節(jié)的方案。
- GBK/Big5 等是獨(dú)立于 Unicode 的舊時代編碼體系。
二、U+ 表示法與最大碼點(diǎn)
- “U+”是慣用前綴:
- U 代表 Unicode。
- 沒有數(shù)學(xué)含義,僅是固定前綴。
- 示例:U+0041、U+4E2D、U+1F600。
- 合法碼點(diǎn)范圍:U+0000–U+10FFFF。
- U+10FFFF 是最大合法碼點(diǎn)。不會超過此值。
- 范圍內(nèi)存在保留區(qū)與非字符(如 U+FDD0–U+FDEF、每個平面的最后兩個非字符 U+FFFE/U+FFFF 等),以及 UTF-16 的代理項(xiàng)區(qū) U+D800–U+DFFF 不是字符。
三、Unicode 的 17 個平面(Plane)
每個平面 65,536 個碼點(diǎn),共 17 個(0–16),總范圍 U+0000–U+10FFFF。
| 平面號 | 名稱 | 范圍 | 主要內(nèi)容 | 備注 |
|---|---|---|---|---|
| 0 | 基本多文種平面 BMP | U+0000–U+FFFF | 現(xiàn)代語言絕大多數(shù)常用字符、常見符號 | 含代理項(xiàng)區(qū) U+D800–U+DFFF(非字符) |
| 1 | 多文種補(bǔ)充平面 SMP | U+10000–U+1FFFF | 歷史文字、符號、樂譜、部分 emoji | 大量非 BMP 字符 |
| 2 | 表意文字補(bǔ)充平面 SIP | U+20000–U+2FFFF | CJK 擴(kuò)展?jié)h字(擴(kuò)展 B 等) | 漢字?jǐn)U展 |
| 3 | 表意文字第三平面 TIP | U+30000–U+3FFFF | 更多 CJK 擴(kuò)展 | 使用較少 |
| 4–13 | 保留 | U+40000–U+DFFFF | 預(yù)留未來分配 | 當(dāng)前基本未分配 |
| 14 | 特別用途補(bǔ)充平面 SSP | U+E0000–U+EFFFF | 標(biāo)簽、變體選擇符等 | 特殊用途 |
| 15 | 私用區(qū) A PUA-A | U+F0000–U+FFFFF | 應(yīng)用自定義 | 不在標(biāo)準(zhǔn)中定義含義 |
| 16 | 私用區(qū) B PUA-B | U+100000–U+10FFFF | 應(yīng)用自定義 | 不在標(biāo)準(zhǔn)中定義含義 |
要點(diǎn):
- BMP 覆蓋“日常需求”為主,但 emoji、更多漢字、歷史文字常在輔助平面(1–16)。
- 許多平面暫未分配,將來可能逐步填充。
四、UTF-8、UTF-16、UTF-32:變長與定長
UTF-8(網(wǎng)絡(luò)事實(shí)標(biāo)準(zhǔn))
- 變長 1–4 字節(jié);ASCII 兼容(U+0000–U+007F 用 1 字節(jié))。
- 字節(jié)模式:
| 字節(jié)數(shù) | 碼點(diǎn)范圍 | 模式 |
|---|---|---|
| 1 | U+0000–U+007F | 0xxxxxxx |
| 2 | U+0080–U+07FF | 110xxxxx 10xxxxxx |
| 3 | U+0800–U+FFFF(排除 U+D800–U+DFFF) | 1110xxxx 10xxxxxx 10xxxxxx |
| 4 | U+10000–U+10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
- 設(shè)計(jì)優(yōu)點(diǎn):
- 自同步:10xxxxxx 一定是續(xù)字節(jié);首字節(jié) 1 的計(jì)數(shù)標(biāo)識總長度。
- ASCII 原樣不變,兼容歷史系統(tǒng)。
- 無字節(jié)序問題(以字節(jié)為單位)。
- 禁止過長形式和代理碼點(diǎn),唯一性與安全性更好。
UTF-16(廣泛用于 Windows、Java/C#/JS 內(nèi)部表示)
- 變長:1 個或 2 個 16 位碼元(2 或 4 字節(jié))。
- BMP 內(nèi)(除代理區(qū))用 1 個碼元;BMP 外(U+10000–U+10FFFF)用代理對(2 個碼元)。
- 代理對計(jì)算:
- 碼點(diǎn) ? 0x10000 = 20 位值 N。
- 高 10 位放入高位代理:0xD800–0xDBFF。
- 低 10 位放入低位代理:0xDC00–0xDFFF。
- 字節(jié)序:
- UTF-16LE/UTF-16BE;文件/流開頭可用 BOM 指示(U+FEFF 的編碼)。
UTF-32(簡單但占空間)
- 定長:每碼點(diǎn)固定 4 字節(jié)。
- 處理簡單、隨機(jī)索引方便;空間效率低,少用于傳輸/存儲,多用于內(nèi)部中間表示或特定環(huán)境。
五、傳統(tǒng)編碼與 Unicode 的關(guān)系:GBK、Big5 等
- Big5:繁體中文為主,雙字節(jié)為主,非 Unicode。
- GBK:大陸常用,兼容 GB2312,雙字節(jié)為主,非 Unicode。
- 它們與 UTF-8/16 的關(guān)系:平行體系。非“子集/超集”,需“轉(zhuǎn)碼”互通。
- 轉(zhuǎn)碼流程:源字節(jié) → 以源編碼解碼為字符(碼點(diǎn)) → 以目標(biāo) UTF 編碼為字節(jié)。
- 風(fēng)險:
- 字符集不一致導(dǎo)致缺字或有爭議映射,用替代字符(U+FFFD)或私有映射兜底。
- 多語言混排困難、跨平臺易亂碼?,F(xiàn)代系統(tǒng)統(tǒng)一推 UTF-8/UTF-16。
六、標(biāo)準(zhǔn)化、合成與變體:真實(shí)世界“一個字符”有多復(fù)雜
- 規(guī)范化(Normalization):
- NFD:規(guī)范分解(分解為基底 + 組合音標(biāo))。
- NFC:先分解再規(guī)范組合(常用默認(rèn))。
- NFKD/NFKC:兼容分解(會把兼容字符分解,如全角等),NFKC 再組合(用于比對/安全)。
- 變體選擇符:U+FE0E(文本呈現(xiàn))、U+FE0F(emoji 呈現(xiàn))。
- 零寬連接符(ZWJ, U+200D):把多個碼點(diǎn)連接出一個組合字形(如家庭、職業(yè)性別變體)。
- 膚色修改符:U+1F3FB–U+1F3FF。
- 非字符與控制字符:排版、雙向文本控制、不可見字符可能影響渲染/安全。
實(shí)踐建議:
- 用戶交互文本盡量以 NFC 保存與比對。
- 處理 emoji 和復(fù)雜文本時,以“字形簇”為單位,而非碼元或碼點(diǎn)。
七、語言與運(yùn)行時中的字符串差異
| 語言/平臺 | 內(nèi)部存儲 | length 語義 | 索引/遍歷默認(rèn)單位 | 備注 |
|---|---|---|---|---|
| JavaScript | UTF-16 | 碼元數(shù) | 碼元 | for...of 按碼點(diǎn)迭代;包含代理對 pitfalls |
| Java | UTF-16 | 碼元數(shù) | 碼元 | 有 codePoint API |
| C#/.NET | UTF-16 | 碼元數(shù) | 碼元 | Rune/Enumerator 支持 |
| Python 3 | 動態(tài)(UCS-1/2/4) | 碼點(diǎn)數(shù) | 碼點(diǎn) | 大多按碼點(diǎn)運(yùn)算 |
| Go | 字符串為只讀字節(jié)(UTF-8 約定) | 字節(jié)數(shù) | 字節(jié) | 需用 range/utf8 包按 rune 遍歷 |
| Rust | String 為 UTF-8 驗(yàn)證字節(jié) | 字節(jié)數(shù) | 字節(jié) | chars() 按 Unicode 標(biāo)量值迭代 |
要點(diǎn):
- length 往往不是“人眼字符個數(shù)”。JS/Java/C# 的 length 是碼元數(shù);Python 是碼點(diǎn)數(shù);Go/Rust 是字節(jié)數(shù)。
- 用戶界面相關(guān)操作(截?cái)?、?jì)數(shù)、游標(biāo)移動、退格)要按字形簇處理。
八、編碼檢測、標(biāo)識與轉(zhuǎn)換
- 顯式聲明優(yōu)先:
- HTTP 頭/HTML meta:Content-Type/charset=utf-8。
- 文件格式/協(xié)議字段聲明編碼。
- 數(shù)據(jù)庫連接/列字符集設(shè)置(MySQL 用 utf8mb4)。
- BOM(字節(jié)序標(biāo)記):
- UTF-8 BOM:EF BB BF(可有可無,歷史兼容性問題多,謹(jǐn)慎)。
- UTF-16LE:FF FE;UTF-16BE:FE FF。
- 啟發(fā)式檢測(無聲明時):
- 驗(yàn)證 UTF-8 序列合法性(拒絕過長形式、非法代理、孤立續(xù)字節(jié))。
- 統(tǒng)計(jì)字節(jié)分布與特征(GBK/Big5/Shift-JIS 各有模式)。
- 檢測 BOM。
- 容易誤判,僅用于輔助。
- 轉(zhuǎn)換實(shí)踐:
- 解碼為 Unicode 內(nèi)部表示 → 再編碼為目標(biāo)字節(jié)。
- 不可映射字符:替換(U+FFFD)、跳過、或失敗。
- 保持/去除 BOM:依據(jù)目標(biāo)環(huán)境要求。
九、Web、文件、數(shù)據(jù)庫與工具鏈實(shí)務(wù)
- Web/HTML:
- <meta charset="UTF-8"> 與 HTTP 頭一致。
- 服務(wù)器、模板、源代碼文件統(tǒng)一 UTF-8。
- JavaScript:
- 用 TextEncoder/TextDecoder 做顯式轉(zhuǎn)碼。
- 遍歷/計(jì)數(shù)用 [...str] 或 for...of 獲取碼點(diǎn);處理用戶界面文本用 Intl.Segmenter 或第三方庫按字形簇。
- 數(shù)據(jù)庫:
- MySQL/MariaDB:utf8mb4(不要用歷史的 utf8),排序規(guī)則用 utf8mb4_0900_ai_ci 或語言合適的 collations。
- PostgreSQL:UTF8。
- SQL Server:NVARCHAR(UTF-16)。
- 文件處理:
- 打開/保存顯式指定編碼。
- 大量舊資料批量轉(zhuǎn) UTF-8 時,先批量探測,再分批驗(yàn)證,記錄不可映射項(xiàng)。
- 源代碼與編譯鏈:
- 源文件用 UTF-8 無 BOM,避免工具誤判。
- 日志、配置、接口契約統(tǒng)一 UTF-8;邊界處做校驗(yàn)。
十、安全與健壯性
- 嚴(yán)格驗(yàn)證 UTF-8 合法性(拒絕過長序列、孤立續(xù)字節(jié)、代理碼點(diǎn))。
- 輸入統(tǒng)一正規(guī)化(常用 NFC/NFKC),防同形異義/混淆。
- 過濾/匹配前先解碼→正規(guī)化→白名單匹配。
- 小心不可見字符、雙向控制字符(RTL/LTR embedding/override),對源代碼/標(biāo)識符/域名顯示做安全策略(如剔除或可視化標(biāo)記)。
- 避免以“字符長度”等價于“顯示寬度”;等寬終端需 East Asian Width/emoji 寬度處理。
十一、UTF-8 為何不是“最多 3 字節(jié)”?
常見誤解是“Unicode 111 萬個碼點(diǎn),3 字節(jié)(2^24)已經(jīng)夠了,為何 UTF-8 要 4 字節(jié)?”原因:
- UTF-8 的字節(jié)前綴模式要滿足:
- ASCII 原樣保留。
- 自同步、前綴唯一、錯誤定位簡單。
- 保持排序關(guān)系與分段查找效率。
- 設(shè)計(jì)約束下,覆蓋到 U+10FFFF 需 4 字節(jié)。不是純粹“容量最小化”問題。
十二、代理項(xiàng)區(qū)與“非法碼點(diǎn)”
- U+D800–U+DFFF 是 UTF-16 代理項(xiàng)區(qū):保留給代理對使用,單獨(dú)出現(xiàn)不代表字符。
- UTF-8/UTF-32 不應(yīng)該編碼代理項(xiàng)區(qū)值;解碼遇到應(yīng)報錯或替換。
- Unicode 還定義了“非字符”(如 U+FDD0–U+FDEF,每個平面 U+FFFE/U+FFFF 等):不用于交換的內(nèi)部保留,可在內(nèi)部使用但不應(yīng)出現(xiàn)在公開文本交換中。
十三、與舊編碼打交道:策略清單
- 確定源編碼(元數(shù)據(jù)優(yōu)先,不能猜就詢問/業(yè)務(wù)約定)。
- 小心“看起來對但其實(shí)錯”的誤判(尤其 GBK vs UTF-8)。
- 轉(zhuǎn)碼流水線要“字節(jié)→字符→字節(jié)”,不要“字節(jié)→字節(jié)”替換。
- 顯示/搜索時做正規(guī)化;記錄不可映射項(xiàng),必要時保留原始字節(jié)作為審計(jì)字段。
- 漸進(jìn)遷移:接口、存儲、日志先統(tǒng)一到 UTF-8;保留少量邊緣輸入通道的探測與兜底。
十四、工程細(xì)節(jié)與性能
- JS/V8 內(nèi)部字符串形式(了解內(nèi)存/性能特征):
- OneByte/TwoByte、ConsString、SlicedString、ExternalString 等。
- 大量拼接導(dǎo)致 Cons 鏈與扁平化成本;建議用數(shù)組 join 或 builder。
- 切片可零拷貝,但過多切片會牽連大對象存活,注意內(nèi)存。
- UTF-8 與 UTF-16 體積對比:
- 拉丁文本:UTF-8 更省空間。
- 中文日文韓文:UTF-8 常為 3 字節(jié)/字符,UTF-16 常 2 字節(jié)/字符,可能更省。
- Emoji/輔助平面字符:UTF-8 4 字節(jié);UTF-16 4 字節(jié)(代理對),接近。
- 索引成本:
- UTF-8/變長:隨機(jī)按“第 N 個字符”尋址需遍歷或輔助索引。
- UTF-16:按碼元 O(1),按碼點(diǎn)/字形簇仍需遍歷。
- UTF-32:按碼點(diǎn) O(1),但字形簇仍復(fù)雜。
十五、常用“對/錯”用法對照
| 需求 | 錯誤做法 | 正確做法 |
|---|---|---|
| 統(tǒng)計(jì)“字符數(shù)” | 直接用 JS length | [...str].length 或使用按字形簇的分段器 |
| 截?cái)嗫梢曃谋?/td> | 按字節(jié)/碼元裁剪 | 按字形簇邊界裁剪,保留完整 emoji/組合 |
| 存儲文本 | 混用 GBK/UTF-8 | 全部 UTF-8(或明確 UTF-16),統(tǒng)一聲明 |
| 讀取文件 | 不指明編碼“隨緣” | 明確 charset;無法確定就檢測+回退策略 |
| 過濾輸入 | 直接黑名單替換 | 解碼→正規(guī)化→白名單驗(yàn)證 |
| 處理 UTF-8 | 容忍過長序列 | 嚴(yán)格拒絕過長/非法序列 |
十六、速查:關(guān)鍵數(shù)值與區(qū)段
- 合法碼點(diǎn):U+0000–U+10FFFF。
- 代理項(xiàng)區(qū)(非字符):U+D800–U+DFFF。
- 非字符樣例:U+FDD0–U+FDEF;每平面 U+FFFE/U+FFFF。
- UTF-8 BOM:EF BB BF;UTF-16LE:FF FE;UTF-16BE:FE FF。
- Emoji 呈現(xiàn)變體:U+FE0E(文本)、U+FE0F(emoji)。
- ZWJ:U+200D。
十七、自問自答:核心問題回顧
Q: “U+10FFFF 是什么?是不是最大碼點(diǎn)?”
A: U+10FFFF 是十六進(jìn)制碼點(diǎn) 0x10FFFF 的表示,是 Unicode 的最大合法碼點(diǎn)。Unicode 合法范圍是 U+0000–U+10FFFF,不會超過這個上限。
Q: “U+ 里的 U 和 + 各是什么意思?”
A: U 表示 Unicode;+ 沒有數(shù)學(xué)意義,是固定寫法前綴,后接十六進(jìn)制碼點(diǎn)。
Q: 為什么說有 17 個平面?都是什么?
A: 碼點(diǎn)空間按每 65,536 個劃成 17 個平面:Plane 0 是 BMP,Plane 1–16 是輔助平面;SMP、SIP/TIP、SSP、PUA-A/B 等用途明確,4–13 目前大多保留。范圍總體 U+0000–U+10FFFF。
Q: UTF-8 和 UTF-16 都是變長,怎么判定長度?
A: UTF-8 用首字節(jié)前綴位型判定總長度,續(xù)字節(jié)以 10 開頭;UTF-16 用是否命中代理對判定(BMP 內(nèi)一碼元;輔助平面用兩個碼元)。
Q: GBK、Big5 和 UTF-8 是什么關(guān)系?
A: 前兩者是獨(dú)立于 Unicode 的舊編碼,字符集不同;UTF-8 是 Unicode 的一種編碼。相互轉(zhuǎn)換需“先解碼為字符,再編碼為目標(biāo)”。不是子集/超集關(guān)系。
Q: 為什么 UTF-8 要用到 4 字節(jié),三字節(jié)不夠嗎?
A: UTF-8 的前綴與自同步設(shè)計(jì)約束下,覆蓋到 U+10FFFF 需要 4 字節(jié)。設(shè)計(jì)目標(biāo)不僅是容量,還有兼容性、同步、排序與安全。
Q: UTF-16 的代理對到底怎么來的?
A: 碼點(diǎn)減 0x10000 得到 20 位;高 10 位映射到 0xD800–0xDBFF,低 10 位映射到 0xDC00–0xDFFF。解碼時反向合并再加回 0x10000。
Q: BMP 里也有“不能用”的碼點(diǎn)嗎?
A: 有。U+D800–U+DFFF 為代理項(xiàng)區(qū),非字符;還有部分非字符與保留位,不應(yīng)在互操作文本中出現(xiàn)。
Q: 實(shí)際工程中如何避免亂碼?
A: 全鏈路統(tǒng)一編碼(推薦 UTF-8),顯式聲明 charset;讀取時指定編碼;對未知來源進(jìn)行檢測/驗(yàn)證;避免 BOM/無 BOM 混用;數(shù)據(jù)庫/HTTP/源文件一致。
Q: 如何按“人眼字符”遍歷或截?cái)啵?br> A: 使用按字形簇的分段(如 ICU、Intl.Segmenter 或?qū)I(yè)庫)。不要按字節(jié)/碼元/碼點(diǎn)直接截?cái)?,以免破壞組合或 emoji 序列。
Q: 安全上要特別注意什么?
A: 嚴(yán)格驗(yàn)證 UTF-8,拒絕過長序列與代理碼點(diǎn);輸入正規(guī)化(NFC/NFKC);警惕不可見控制字符與雙向控制符;用白名單策略。