Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第二 字符与编码

二进制能够表示数字,那能不能表示数字之外的各种文字,如英文、中文、俄语、法语等世界各个国家的文字?答案是肯定的,不仅可以表示各种文字,还能表示各种符号、图形(包括表情包)等。其中就使用到一种古老而又充满活力的技术——编码(coding)技术

计算机中的编码技术,简单而言就是建立数字与字符之间一一对应的关系。比如用数字0到9分别表示其本身,10表示A,11表示B,12表示C,以此类推,就能解决所有字符的编码问题。很显然实际上存在无数种编码的方式。正所谓“无规矩不成方圆“,没有统一的编码规则是不行的。本章将系统介绍计算机中主流的字符编码方案,从经典的ASCII到现代的Unicode,以及Rust中的字符串处理。


2.1 ASCII编码

美国作为计算机的发明地,最先提出了一套编码方案——ASCII(American Standard Code for Information Interchange,美国信息交换标准代码),即ASCII码

2.1.1 ASCII编码原理

ASCII码使用7位二进制数(共128个码位)来表示字符,其编码空间为:

$$ 0 \leq \text{ASCII码} \leq 127 \quad (0\text{x}00 \sim 0\text{x}7F) $$

ASCII表

ASCII码是计算机历史上最著名的编码方案之一,它最初是美国国家标准,供不同计算机在相互通信时用作共同遵守的西文字符编码标准,后来它被国际标准化组织(International Organization for Standardization, ISO)定为国际标准,称为ISO 646标准。

2.1.2 ASCII控制字符

ASCII码中,0~31和127为控制字符,用于控制设备的操作:

十进制十六进制缩写含义
00x00NUL空字符
70x07BEL响铃
80x08BS退格
90x09HT水平制表符
100x0ALF换行
130x0DCR回车
270x1BESC转义
1270x7FDEL删除

2.1.3 ASCII可打印字符

32~126为可打印字符,包括数字、英文字母、标点符号等:

范围内容
48~57 (0x30~0x39)数字 0~9
65~90 (0x41~0x5A)大写英文字母 A~Z
97~122 (0x61~0x7A)小写英文字母 a~z
32~47, 58~64, 91~96, 123~126空格、标点及特殊符号

ASCII码的局限性也是显而易见的,它仅能表示128个字符,对于数以万计的中国汉字就显得十分无力了。


2.2 ANSI编码——各国方言

ANSI编码并非一个具体的编码标准,而是对不同国家和地区各自制定的多字节字符编码标准的统称。不同的国家和地区制定了不同的标准,由此产生了 GB2312、GBK、GB18030、Big5、Shift_JIS 等各自的编码标准。这些使用多个字节来代表一个字符的各种汉字延伸编码方式,统称为 ANSI 编码

2.2.1 各国主要编码标准

编码标准国家/地区字节数收录字符数说明
GB2312中国大陆1~2字节~7,000最早的中文编码标准
GBK中国大陆1~2字节~21,000GB2312的扩展
GB18030中国大陆1~4字节~160万国家标准,兼容Unicode
Big5中国台湾1~2字节~13,000繁体中文编码
Shift_JIS日本1~2字节~12,000日文编码
EUC-KR韩国1~2字节~11,000韩文编码
ISO-8859-1西欧1字节256Latin-1,西欧语言

2.2.2 ANSI编码的局限

ANSI编码的核心问题是:相同的字节序列在不同编码下可能表示不同的字符。例如,在简体中文Windows系统中,字节0xD6 0xD0表示“中“字(GB2312编码);而在日语系统中,相同的字节序列可能表示完全不同的字符。这种“方言“式的编码体系是产生乱码的根本原因。


2.3 Unicode编码——计算机世界中的“书同文“

秦始皇统一六国后,为了便于管理庞大的帝国,保证政令通畅,于是推行统一的文字和度量衡——“书同文,车同轨“不仅是秦始皇一项重要的历史贡献,也深刻影响和促成中国之后两千多年大一统的政治格局的形成。如今世界那么大,各国的文字、符号的数量是相当惊人的,并且随着社会的发展,新的字符不断涌现。字符的编码也需要与时俱进。Unicode(统一码,也叫万国码、单一码)应运而生,彻底解决全世界所有国家的文字、符号的编码问题。

2.3.1 Unicode编码空间

Unicode编码系统可分为编码方式和实现方式两个层次。Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。Unicode用数字0~0x10FFFF来映射这些字符,最多可以容纳1114112个字符,或者说有1114112个码位。码位(Code Point)就是可以分配给字符的数字,通常表示为 U+XXXX 的形式。

Unicode编码空间划分为若干平面(Plane):

平面范围名称说明
第0平面U+0000 ~ U+FFFFBMP(基本多文种平面)最常用的字符,包括基本拉丁字母、CJK统一表意文字等
第1平面U+10000 ~ U+1FFFFSMP(多文种补充平面)古文字、音乐符号等
第2平面U+20000 ~ U+2FFFFSIP(表意文字补充平面)罕用汉字
第3~13平面U+30000 ~ U+DFFFF保留未分配
第14平面U+E0000 ~ U+EFFFFSSP(特殊用途补充平面)格式控制字符
第15~16平面U+F0000 ~ U+10FFFFPUA(私人使用区)用户自定义字符

2.3.2 UTF-8编码原理

UTF-8(Unicode Transformation Format - 8-bit)是一种变长编码方案,使用1~4个字节表示一个Unicode字符:

Unicode码位范围UTF-8字节序列字节数
U+0000 ~ U+007F0xxxxxxx1
U+0080 ~ U+07FF110xxxxx 10xxxxxx2
U+0800 ~ U+FFFF1110xxxx 10xxxxxx 10xxxxxx3
U+10000 ~ U+10FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx4

UTF-8的编码规则可以用以下方式理解:

$$ \text{UTF-8字节数} = \begin{cases} 1 & \text{if } 0 \leq U \leq 0\text{x}7F \ 2 & \text{if } 0\text{x}80 \leq U \leq 0\text{x}7FF \ 3 & \text{if } 0\text{x}800 \leq U \leq 0\text{x}FFFF \ 4 & \text{if } 0\text{x}10000 \leq U \leq 0\text{x}10FFFF \end{cases} $$

编码示例

字符Unicode码位UTF-8字节序列(十六进制)
‘A’U+004141
‘中’U+4E2DE4 B8 AD
‘𠮷’(吉的异体字)U+20BB7F0 A0 AE B7
‘💖’U+1F496F0 9F 92 96

UTF-8的优势在于:

  • 向后兼容ASCII:纯ASCII文本在UTF-8下完全不变
  • 无字节序问题:不需要BOM(虽然可以添加)
  • 自同步:可以从任意字节位置开始解析

2.3.3 UTF-16编码原理

UTF-16使用2或4个字节表示一个Unicode字符:

  • BMP内字符(U+0000 ~ U+FFFF):直接使用2字节表示
  • 辅助平面字符(U+10000 ~ U+10FFFF):使用4字节(代理对)表示

代理对编码方式:

$$ \text{辅助平面字符编码} = \begin{cases} \text{高位代理} = \frac{U - 0\text{x}10000}{0\text{x}400} + 0\text{x}D800 \ \text{低位代理} = (U - 0\text{x}10000) \bmod 0\text{x}400 + 0\text{x}DC00 \end{cases} $$

其中,高位代理范围:0xD800~0xDBFF,低位代理范围:0xDC00~0xDFFF。

2.3.4 UTF-32编码原理

UTF-32使用固定4个字节表示所有Unicode字符,是最简单的编码方式:

$$ \text{UTF-32编码} = \text{Unicode码位值} $$

虽然UTF-32在字符定位上效率最高(O(1)随机访问),但由于每个字符固定占用4字节,对于以ASCII为主的文本,空间效率较低。

2.3.5 三种UTF编码对比

特性UTF-8UTF-16UTF-32
编码长度变长(1~4字节)变长(2或4字节)定长(4字节)
空间效率(ASCII文本)最优较差(2倍)最差(4倍)
空间效率(CJK文本)一般(3字节/字)较好(2字节/字)较差
随机访问O(n)O(n)O(1)
字节序无关需要BOM需要BOM
自同步
主要使用场景网络传输、Linux/macOSWindows、Java、JavaScript内部处理

2.3.6 BOM(字节顺序标记)

BOM(Byte Order Mark)是放在文本文件开头的特殊字符(U+FEFF),用于标识字节序:

编码BOM字节序列说明
UTF-8EF BB BF可选,通常不推荐
UTF-16 BEFE FF大端序
UTF-16 LEFF FE小端序(Windows默认)
UTF-32 BE00 00 FE FF大端序
UTF-32 LEFF FE 00 00小端序

访问如下网站查看所有Unicode符号:


2.4 中文编码

中国程序员在职业生涯中,一定会遇到的一个问题就是中文乱码问题。操作系统、数据库、网络传输都有可能出现中文乱码问题,其本质就是使用了不恰当的编码对中文数据进行编码、解码。因此非常有必要了解各种场景下系统正在使用的编码方式。

2.4.1 中文编码标准演进

为了解决汉字的编码问题,中国制定了GB2312、GBK、GB18030等编码标准。

编码标准发布时间字节数收录汉字数特点
GB23121980年1~2字节6,763覆盖常用简体字,兼容ASCII
GBK1995年1~2字节21,003扩展GB2312,含繁体字
GB180302000年1~4字节160万+国家标准,与Unicode完全映射

2.4.2 编码转换 iconv

生僻字 GB2312、GBK两种编码基本解决中文常用字符的编码,但中文中还有大量生僻字,需要GB18030才能编码。因此需要视具体情况采用恰当的编码。

# 查看文件编码
file -bi example.txt

# UTF-8转为GBK
iconv -f UTF-8 -t GBK 沁园春·雪.txt > snow.txt

# 忽略无法转换的字符
iconv -c UTF-8 -t GBK 沁园春·雪.txt > snow.txt

# GBK转为UTF-8
iconv -f GBK -t UTF-8 abc.txt > abc_1.txt

# UTF-8转为GB18030
iconv -f UTF-8 -t GB18030 生僻字.txt > gb18030.txt

2.4.3 操作系统的编码

Windows下中文常用支持中文编码有:简体中文(GB2312)、繁体中文(Big5)、UTF-8。

# Windows PowerShell
PS C:\Users\huang> chcp
活动代码页: 936
代码页国家(地区)或语言
437美国
932日文(Shift-JIS)
936中国 - 简体中文(GB2312)
949韩文
950繁体中文(Big5)
1200Unicode
1252西欧(Windows)
65001Unicode (UTF-8)

ISO-8859-1(Latin-1) 是MySQL数据库默认编码,也是网络传输默认编码之一。


2.5 Rust中的字符串处理

Rust中默认使用的是UTF-8编码。这一设计决策与Rust的内存安全和零成本抽象理念高度一致。

2.5.1 Rust中的String、&str与Vec<u8>

Rust中的字符串类型设计体现了所有权和借用的核心概念:

类型内存布局所有权说明
StringVec<u8> 的包装拥有堆分配的UTF-8字符串,可增长
&str指向UTF-8字节序列的引用借用字符串切片,不可变引用
Vec<u8>字节数组拥有原始字节序列,不保证UTF-8合法性
char4字节(Unicode标量值)值类型单个Unicode字符
fn main() {
    // String:拥有所有权的UTF-8字符串
    let mut s = String::from("hello");
    s.push_str(" world");
    println!("String: {}", s);

    // &str:字符串切片(借用)
    let slice: &str = &s;
    println!("&str: {}", slice);

    // String 与 &str 的关系
    let owned: String = slice.to_string();  // &str -> String(拷贝)
    let borrowed: &str = &owned;             // String -> &str(借用)

    // `Vec<u8>`:原始字节
    let bytes: Vec<u8> = owned.into_bytes();
    println!("`Vec<u8>`: {:?}", bytes);

    // `Vec<u8>` -> String(需要验证UTF-8)
    let s2 = String::from_utf8(bytes).unwrap();
    println!("String from bytes: {}", s2);
}

2.5.2 Rust中的Unicode操作

fn main() {
    // 从UTF-8字节构造字符串
    let sparkle_heart_vec = vec![240, 159, 146, 150];
    let sparkle_heart = String::from_utf8(sparkle_heart_vec).unwrap();
    assert_eq!("💖", sparkle_heart);

    let bytes = sparkle_heart.into_bytes();
    assert_eq!(bytes, [240, 159, 146, 150]);

    // Unicode转义序列
    println!("\u{65b0}\u{534e}\u{793e}\u{5feb}\u{8baf}\u{ff1a}\u{0033}\u{6708}\u{0032}\u{0036}\u{65e5}\u{ff0c}\u{4e2d}\u{56fd}\u{548c}\u{6d2a}\u{90fd}\u{62c9}\u{65af}\u{5efa}\u{4ea4}\u{3002}");
    println!("\u{5b78}\u{7fd2}\u{5f37}\u{570b} 伟大复兴");

    // 字符转Unicode码位
    println!("{:X} {:X}", '华' as u32, '夏' as u32);

    // Emoji
    println!("Rust常见emoji(表情符号):\u{1F1E8}\u{1F1F3}🦀💖🚀😂\u{1F980}\u{1F602}");
    println!("常见emoji(国旗):\u{1F1E8}\u{1F1F3} 🇨🇳 🇺🇸 🇷🇺");

    // ASCII转义
    println!("ASCII常见字符:\u{0020}\u{0040}\u{0021}\u{0041}");

    // 数学符号
    println!("数学符号:∂∆∮\u{2211}\u{2200}\u{2208}\u{2209}\u{222b}");
}

2.5.3 String/&str/Vec<u8>转换关系(与第十九章呼应)

use std::str;

fn main() {
    // &str -> String
    let s: &str = "hello";
    let owned: String = String::from(s);
    let owned2: String = s.to_string();
    assert_eq!(owned, owned2);

    // &str -> &[u8]
    let bytes: &[u8] = s.as_bytes();
    println!("bytes: {:?}", bytes); // [104, 101, 108, 108, 111]

    // String -> &str
    let hello = String::from("hello");
    let slice: &str = &hello;
    let slice2: &str = hello.as_str();
    assert_eq!(slice, slice2);

    // String -> `Vec<u8>`
    let hello = String::from("hello");
    let bytes_vec: Vec<u8> = hello.into_bytes();
    println!("into_bytes: {:?}", bytes_vec);

    // &[u8] -> &str(可能失败)
    let valid: &[u8] = b"hello";
    let s: &str = str::from_utf8(valid).unwrap();
    println!("from_utf8: {}", s);

    let invalid: &[u8] = &[0xff, 0xfe]; // 非UTF-8
    let result = str::from_utf8(invalid);
    println!("invalid utf8: {:?}", result); // Err(Utf8Error)

    // `Vec<u8>` -> String(可能失败)
    let bytes = vec![104, 101, 108, 108, 111]; // "hello"的ASCII
    let s = String::from_utf8(bytes).unwrap();
    println!("from_utf8(Vec): {}", s);
}

2.6 数据库编码

数据库编码是中文乱码问题的另一个重灾区。MySQL数据库的历史遗留问题尤其值得关注。

2.6.1 MySQL字符集设置

-- 查看当前数据库字符集设置
show variables like 'character%';

2.6.2 utf8 vs utf8mb4

MySQL中的utf8实际上是utf8mb3的别名,只使用最多3个字节,无法存储4字节的Unicode字符(如Emoji、部分生僻字)。

-- utf8mb4:支持完整的Unicode,包括Emoji
UPDATE bakeries_db.user SET wx_nickname = 'abc🚀🎵✨' WHERE id = 5;

-- utf8mb3(旧版MySQL默认utf8)
-- Error Code: 1366. Incorrect string value: '\xF0\x9F\x9A\x80\xF0\x9F...' for column 'wx_nickname' at row 1
UPDATE uic.user SET wx_nickname = 'abc🚀🎵✨' WHERE id = 5;
字符集最大字节数支持范围说明
utf8mb33字节BMP平面MySQL旧版默认,不支持Emoji
utf8mb44字节全部Unicode推荐使用,完整支持

建议:新项目务必使用 utf8mb4,并设置 character_set_server=utf8mb4


2.7 编码检测与转换

2.7.1 常见编码检测方法

// Rust中使用 encoding_rs 库进行编码检测和转换
use encoding_rs::{GBK, UTF_8};

fn main() {
    // GBK编码的字节
    let gbk_bytes: Vec<u8> = vec![0xD6, 0xD0, 0xCE, 0xC4]; // "中文"的GBK编码

    // GBK -> UTF-8
    let (cow, _, had_errors) = GBK.decode(&gbk_bytes);
    if !had_errors {
        let utf8_string = cow.into_owned();
        println!("转换结果: {}", utf8_string); // 输出: 中文
    }

    // UTF-8 -> GBK
    let utf8_str = "中文";
    let (gbk_result, _, _) = UTF_8.encode(utf8_str);
    println!("GBK字节: {:?}", gbk_result);
}

2.7.2 编码转换注意事项

  1. 有损转换:当目标编码无法表示源编码中的某些字符时,转换可能失败或产生替换字符
  2. BOM处理:UTF-8文件有时包含BOM,读取时需要注意跳过前3个字节
  3. 编码推断:没有绝对可靠的编码自动检测方法,最好明确指定编码

总结

编码方案字节数收录字符数兼容性主要使用场景
ASCII1字节128所有编码兼容英文文本、程序代码
GB23121~2字节~7,000兼容ASCII早期中文系统
GBK1~2字节~21,000兼容GB2312Windows中文系统
GB180301~4字节~160万兼容GBK,映射Unicode中国政府标准
UTF-81~4字节111万+兼容ASCIIWeb、Linux、Rust
UTF-162或4字节111万+不兼容ASCIIWindows、Java
UTF-324字节111万+不兼容ASCII内部处理

练习题

  1. ASCII编码范围:写出字符 ‘A’、‘a’、‘0’ 的ASCII码十进制值,并说明大小写字母ASCII码之间的关系。

  2. UTF-8编码计算:手动计算汉字“中“(Unicode码位 U+4E2D)的UTF-8编码字节序列,并与 "中".as_bytes() 的Rust输出进行对比。

  3. Rust字符串操作:编写Rust代码,创建一个包含Emoji “🦀”(Unicode U+1F980)的 String,然后将其转换为 Vec<u8>,打印每个字节的十六进制值,并验证其符合UTF-8编码规则。

  4. 编码转换陷阱:解释为什么以下Rust代码可能产生非预期结果,并给出修复方案:

    #![allow(unused)]
    fn main() {
    let s = "中文";
    let bytes: Vec<u8> = s.bytes().collect();
    let recovered = String::from_utf8(bytes).unwrap();
    println!("{}", recovered);
    }
  5. BOM处理:编写一个Rust函数 read_file_without_bom(path: &str) -> Result<String, std::io::Error>,读取UTF-8文本文件并自动跳过可能存在的BOM头。

  6. 编码检测:使用 encoding_rs 库编写程序,读取一个未知编码的文本文件,尝试用GBK和UTF-8分别解码,输出两种解码结果供人工判断。

  7. MySQL编码配置:写出创建数据库时指定 utf8mb4 字符集的完整SQL语句,包括数据库、表和连接字符集的设置。

  8. String/&str/Vec<u8>关系图:画出Rust中 String&strVec<u8>&[u8] 之间的转换关系图,标注每个转换方法名称及其可能的失败情况。