想象你经营着一家全球邮政公司。每天客户递来一张张写着字的纸条——每个字符,都是一封“信”。计算机只认识数字和字节,不认识人间的文字,所以你的工作就是:把这些信编排成数字编号,再贴上标签,装进网络、硬盘。这套“把字符变成数字、把数字变成字节”的规则,就是字符编码。最早的邮局只是英国小镇上的一个代办点,127 个信箱装下英文、数字和标点绰绰有余,那是 ASCII;后来联合国设立了“全球字符登记处”,给全人类所有文字里的每个字符发一张独一无二的护照号,这是 Unicode;而真正厉害的,是快递分拣员设计的那套“小号短贴、大号长贴”的标签方案——UTF-8。从这三封信讲起,你会彻底看懂:乱码从哪来、为什么程序员总说“先 decode 再 encode”、以及 Python 里那些编码 API 到底在干什么。
掌握字符编码三件套,等于点亮了数据链路的第一盏灯:文本处理、日志清洗、模型服务部署中的乱码与编码事故,对你将不再是玄学,而是可定位、可修复的工程问题。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
编码基本概念
你开了一家全球邮政公司,客户递来一张写着字的纸条,你的任务是把这些字变成能在计算机网络里流转的“信”——一串字节。这一过程叫编码:计算机不认字,只认数字;而解码,就是邮差到了收件人手里,把编号还原成字的逆操作。每一次文件保存、每一次网络请求,本质都是这两种操作的交替。
在编码世界里,有三个核心角色:字符、码点、码元。字符是“信的内容”,就是你想表达的符号;码点是“全球护照号”,即 Unicode 为每个字符分配的唯一数字;码元则像信封上的“标签格”——UTF-8 的标签格是 8 比特一格(1 字节),UTF-16 是 16 比特一格(2 字节)。一个字符要上网络,先查它的护照号,再把护照号翻译成标签序列,最后写成字节。
以“中”字为例:它的护照号是 U+4E2D,UTF-8 编码器查表后把它写成 E4 B8 AD 三个字节,这三字节才能在硬盘或网线上真正“寄出去”。
所以,字符、码点、字节,是邮局里的三张单据:内容、护照、标签。理解了这三层,下面所有编码知识都在同一张桌上展开。
- 字符识别字符是人写下的最小语义符号,比如字母 A、汉字“中”。
- 码点映射查“全球字符登记处”,拿到该字符的护照号。
- 码元切分把护照号的二进制按标签格的宽度切分,UTF-8 每格 8 位。
- 字节存储把每个码元写成十六进制字节,落盘或上网。
ASCII码详解
最早的邮局只是英国小镇上的一个代办点,镇上只有英文客户,一百来个格子就装下了所有人。这就是 1960 年代的 ASCII(American Standard Code for Information Interchange):给英文字母、数字和常用符号各发一个 0~127 的编号。它诞生于电传打字机时代,目标只有一个——让美国本土的英文文本能在不同厂商的机器之间交换。
ASCII 用 7 个二进制位表示一个字符,编号范围从 0x00 到 0x7F。排在前面的 33 个是控制字符(回车、换行、响铃等“内部指令”,其中还包含文件结束符 DEL),后面 95 个是可打印字符(大写字母、小写字母、数字、标点)。存储时每个字符占 1 字节,最高位固定为 0。例如大写 A 的编号是 65(0x41),小写 a 是 97(0x61),数字 0 是 48(0x30)。
这套结构在英文世界完美运行了几十年,直到计算机走出英语国家,问题才暴露:法语的 é、中文的“中”、日文的假名,都没有编号。小镇邮局的 127 个格子,注定装不下全世界的信。
小镇邮局的格子编号清晰、规则简单,但它只是世界编码史的第一页。下一节,全球字符登记处登场。
- 7 位编号用 7 个二进制位做编号,容量自然就是 128 个。
- 控制字符区编号 0~31 加上最后的 127,共 33 个,负责换行、回车、响铃等设备指令。
- 可打印字符区编号 32~126,共 95 个,包含字母、数字、标点,空格是 32。
- 字节存储机器按字节处理,7 位前面补一个 0,凑成 8 位。
Unicode体系
小镇邮局不够用了,联合国成立了“全球字符登记处”。这个机构不问国家、不分语言,把人类历史上出现过的每个字符都登记在册,发一个唯一的“护照号”——这个号就是码点(code point)。护照号只解决“身份”问题:每个字符有一个全球统一编号,与存储无关。
Unicode 的码点空间从 U+0000 到 U+10FFFF,总数约 111 万。它被划分成 17 个平面,每个平面有 65,536 个码位。第 0 号平面叫基本多文种平面(BMP),日常使用的中文、英文、日文都在里面;第 1 号平面以上是补充平面,存放 Emoji、古文字、数学符号等冷门字符。
16 比特的 UTF-16 编码只能直接覆盖 BMP 的 0~65535。超出 BMP 的字符,需要一对 16 比特码元组合表示,这叫代理对(surrogate pair):一个高代理(U+D800~U+DBFF)配一个低代理(U+DC00~U+DFFF)。代理区本身的 2048 个码位被保留,不能独立表示字符。
护照号是“身份”,怎么装进信封是“格式”。Unicode 只负责发护照,不负责寄信——真正设计标签方案的是下一节的 UTF-8。
- 平面数量Unicode 共规划 17 个平面,编号 0~16。
- 每平面码位每个平面用 2 字节的地址空间,65536 个码位。
- 代理区排除D800~DFFF 共 2048 个码位保留给 UTF-16 代理对。
- 实际可用总量全部码位减去代理保留区,才是可分配给字符的数量。
UTF-8原理
全球字符登记处有了,快递分拣师开始头疼:如果每个护照号都按 4 字节的大信封寄,那 90% 的英文信都在浪费邮费。聪明的分拣师设计了一套“看号定箱”的规则:小号用短信封,大号用长信封。这就是 UTF-8 的变长策略——它是 Unicode 的一种存储格式,用 1~4 个字节按需包装每个字符。
规则只有四条:码点在 0~127 之间用 1 字节;128~2047 用 2 字节;2048~65535 用 3 字节;再往上用 4 字节。关键在于前缀位:首字节开头的前缀 0 / 110 / 1110 / 11110 直接标明“这是几字节字符的起始”;而所有续字节统一以 10 开头,标明“我是后续标签,不是新字符”。
操作时,把码点换算成二进制,再从左到右把数据位填入规则表中的 x 位置。例如“中”(U+4E2D = 0100 1110 0010 1101)落到 3 字节区间,填入 1110xxxx 10xxxxxx 10xxxxxx,得到 1110 0100 1011 1000 1010 1101,即 E4 B8 AD。
这套规则看似简单,却同时解决了兼容性、自同步和空间效率三个问题——正是下一节的主角。
\begin{array}{c|c|c}\text{码点范围} & \text{字节数} & \text{字节模式} \\\hline0\sim 0\times7F & 1 & 0xxxxxxx \\0\times80\sim 0\times7FF & 2 & 110xxxxx\ 10xxxxxx \\0\times800\sim 0\timesFFFF & 3 & 1110xxxx\ 10xxxxxx\ 10xxxxxx \\0\times10000\sim 0\times10FFFF & 4 & 11110xxx\ 10xxxxxx\ 10xxxxxx\ 10xxxxxx\end{array}- 区间定位根据码点大小,查出应该用几个字节。
- 首字节前缀首字节写 n 个 1 再接一个 0,作为信封规格标识。
- 续字节格式后续 n-1 个字节全部以 10 开头,标识“续体”。
- 数据位填入把码点二进制从右往左的位,依次填入所有 x 位置,空位补 0。
UTF-8特性
这套标签方案能在互联网上称霸几十年,靠的是四个贴心设计,让全球邮局都愿意收编。第一个是 ASCII 兼容:单字节标签 0xxxxxxx 与 ASCII 完全一致,1960 年代的老系统一个字都不用改,就能和 UTF-8 系统交换信件。这是 UTF-8 推广成本几乎为零的根本原因。
第二个是自同步:在字节流中间任意位置断开并重新开始读,读到任意字节都能通过开头判断这是新字符还是续字节。损坏不会连环传染,一个字符的垃圾数据不会吞掉后面整个句子。第三个是字节序无关:UTF-8 信封是一个标签一个标签按顺序贴的,没有“大头小头”(大小端)的歧义,而 UTF-16 在不同 CPU 上可能字节序相反。
空间效率上,英文 1 字节/字符,拉丁、希腊等文字 2 字节,中文 3 字节,Emoji 4 字节。对比 UTF-16:纯英文内容 UTF-8 只有一半大小;纯中文 UTF-16 每个字符 2 字节反而更省。因此实践中互联网 95% 以上的网页选择 UTF-8,因为全球网页英文占绝大多数。
正是这四条特性,让 UTF-8 在网络传输和流式处理中几乎无懈可击。但工程里没有银弹——BOM 和乱码依然是每个开发者都会撞上的墙。
- 自同步判定读到任意字节,看开头前缀即可判断它是不是字符起始。
- ASCII 兼容前缀为 0 的单字节,内容与 ASCII 编号完全重合,老文件无需转换。
- 字节序无关标签按序排列,不存在多字节整数的高低顺序问题。
- 流式恢复损坏后扫描到非 10 开头的字节,立即重新对齐。
工程实践
信件寄多了,会遇到各种问题:有的信头上粘了一截奇怪的“隐形胶带”(BOM),有的信到了对方手里变成了满纸乱码。BOM(Byte Order Mark) 是 UTF-8 文件开头可选的 EF BB BF 三个字节,用来声明“我是 UTF-8”。但不少程序不识别它,把 BOM 当作一个隐藏字符显示成 ,也有人叫它“零宽不换行空格”;Excel 打开 UTF-8 的 CSV 时若不带 BOM 就会乱码,反过来又有些编译器见到 BOM 直接报错。
乱码的本质是“编码用 A,解码用 B”。比如“中文”用 UTF-8 存成 E4 B8 AD E6 96 87,被 GBK 解码后显示成“涓枃”;如果再把“涓枃”按 GBK 编码回字节,得到 E4 B8 AD E6 96 87,再按 UTF-8 解,就成了传说中的“锟斤拷”。注意:乱码不是数据损坏,字节流原封未动,只是读信封的人用了错误的地址规则。
正确转换永远遵循两步走:先用原编码 decode 成字符串,再用目标编码 encode 成字节。切勿在字节层面做“翻译”,那只会制造新的错配。
理解了 BOM 和乱码的成因,编写健壮的编码工具就水到渠成——Python 正是处理这类问题的利器。
- 解码原编码用文件真实的编码方式把字节流解析成字符串。
- 编码为目标把字符串按目标编码重新打包成字节流。
- BOM 识别文件开头出现 EF BB BF,即可判定是 UTF-8 with BOM。
- 错误示范直接在字节层把“看起来像”某编码的字节替换,产生二次错配。
Python实战
Python 是邮局前台里工具最全的小姐姐:别人只会收信寄信,她还能现场翻译。Python 3 有个重要设计:str 类型存储的是字符序列(码点语义),bytes 存储的是原始字节。两者用 .encode('utf-8') 和 .decode('utf-8') 互转——这就是打包与拆包的动作。
文件读写通过 open 函数的 encoding 参数指定编码,不指定时依赖平台默认值(Windows 上可能是 GBK),这正是很多隐蔽乱码的来源。网络传输同理:requests 拉取网页时用 response.encoding 显式指定解析编码,避免把 UTF-8 页面当 Latin-1 读。
工程中常有三个固定套路:BOM 用 b'\xef\xbb\xbf' 判断;未知编码的字节流用 try/except UnicodeDecodeError 逐个尝试解码;跨平台打印中文前确认终端编码与文本编码一致,否则控制台又成乱码重灾区。
记住:str 是“内容”,bytes 是“信封”。任何文件保存、网络请求、数据库读写,都是在打包与拆包之间切换。而选对信封,就是最后一节要讲的性能哲学。
- str 是字符序列Python 3 的字符串天然按字符(码点)处理,长度即字符数。
- encode 打包逐字符查码点、按 UTF-8 规则转字节,拼接成 bytes。
- decode 拆包按编码规则把字节流还原成字符序列。
- 异常兜底非法字节序列触发 UnicodeDecodeError,可捕获后尝试下一种编码。
性能优化
快递公司做大后开始算成本:信封选得好不好,直接决定邮费和分拣速度。存储优化的第一原则是按内容选编码:内容以英文为主,UTF-8 的 1 字节/字符优势无可替代;纯中文且只在中文环境流通,GBK 的 2 字节确实更省;但涉及多语言、跨平台、对外发布,UTF-8 是底线选择——省下的空间抵不上错配一次的风险。
排序与比较上,UTF-8 有一个隐藏福利:字节序与码点序完全一致。两个字符串逐字节比较得到的大小关系,与它们码点比较的结果相同。这意味着可以直接用快速的字节比较(如 C 的 memcmp)做字典序排序,无需逐字符解码。但要注意:如果开了 UTF-8 的 BOM,排序前必须剥掉,否则 BOM 字节会影响首字符比较。
安全上要警惕两类陷阱。一是 overlong encoding(过长编码):比如把 ASCII 的 A(0x41)用 2 字节 C1 81 表示,非法但某些宽松解析器会接受,可用来绕过输入校验,严格实现应拒绝非最短编码。二是字节截断:按固定字节数截断字符串会切开多字节字符,尾部出现 U+FFFD 替换符(�),MySQL 的 SUBSTRING 等函数按字节截断时尤其常见。
分拣中心最后的秩序来自三条纪律:选对信封规格、按字节序排队、拦住可疑包裹。这同样适用于你维护的任何编码数据。
- 码点有序Unicode 给字符的编号是单调的。
- UTF-8 保序编码后字节序列保持同样的顺序关系。
- 直接比较因此可按字节大小排序,无需逐个解码。
- 安全校验解析时必须拒绝非最短编码,防止校验绕过。
AI 应用场景
场景1:多语言语料清洗管道
阶段:数据预处理
解决的问题:训练大模型的多语言语料来自各地爬虫,原始页面可能是 UTF-8、GBK、Shift-JIS 等编码。若入库前不统一转码,分词器会读到满屏乱码,模型从源头“学坏”。传统方式是人工逐个文件另存为 UTF-8,效率极低且易漏;AI 时代的做法是脚本化批量转码,对每个文本块做鲁棒解码后统一编码。
具体案例:某语料平台每天新增 10 万篇网页文章。管道先用 BOM 检测和候选编码列表(utf-8 → gbk → shift_jis)依次解码,成功后再统一 encode('utf-8'),并统计 U+FFFD 替换符占比作为质量分。转码后语料喂给分词器,OOV(词表外词)率下降 23%。
场景2:RAG 检索服务的中文编码一致性
阶段:模型推理与部署
解决的问题:RAG 应用把用户 query 编码后向量化检索,若 query 在 HTTP 传输层与文档库编码不一致(如前端按 GBK 提交、后端按 UTF-8 解析),检索关键词全部错位,召回率断崖下跌。传统排查靠肉眼盯乱码,难以定位是网关、应用还是数据库哪一层出了问题。
具体案例:某客服问答机器人上线后中文召回率骤降。排查发现浏览器 POST 表单按 GBK 提交,而向量化服务按 UTF-8 解码。将网关统一强制 charset=utf-8,并在服务端先 decode 再 encode 后,召回率恢复,首响延迟降低 15%。
Python 演示
# -*- coding: utf-8 -*-"""字符编码小工具集:演示 ASCII / Unicode / UTF-8 常见操作。""" def encode_to_utf8(text): """把字符串按 UTF-8 编码为字节序列,并逐字符展示结果。""" result = [] for ch in text: raw = ch.encode('utf-8') hex_str = ' '.join('%02X' % b for b in raw) result.append(raw) print('%r U+%04X -> %s (%d 字节)' % (ch, ord(ch), hex_str, len(raw))) return b''.join(result) def strip_bom(data): """去掉 UTF-8 文件头部的 BOM 标记,返回(数据,是否含BOM)。""" if data.startswith(b'\xef\xbb\xbf'): return data[3:], True return data, False def robust_decode(data, candidates=('utf-8', 'gbk', 'shift_jis')): """依次尝试多种编码,返回能成功解读的(内容,编码名)。""" for enc in candidates: try: return data.decode(enc), enc except UnicodeDecodeError: continue return None, None def byte_length_distribution(text): """统计文本中每个字符 UTF-8 编码后的字节数分布。""" hist = {} for ch in text: n = len(ch.encode('utf-8')) hist[n] = hist.get(n, 0) + 1 return hist if __name__ == '__main__': s = 'Hello, 世界 \U0001F30E' print('原始字符串:', s) print('UTF-8 总字节数:', len(s.encode('utf-8'))) print() print('逐字符编码:') encode_to_utf8(s) print() print('字节数分布:', byte_length_distribution(s)) print() data = b'\xef\xbb\xbfHello' clean, has = strip_bom(data) print('BOM 检查: 存在=%s, 清理后=%r' % (has, clean)) print('鲁棒解码:', robust_decode(b'\xc4\xe3\xba\xc3'))
ch.encode('utf-8')给单个字符“打包”:查码头点并按 UTF-8 规则生成字节信封,这是编码的主心骨。ord(ch)在“全球字符登记处”查该字符的护照号(码点)。b'\xef\xbb\xbf'UTF-8 BOM 的三字节标记,用来判断文件头部是否粘了“隐形胶带”。data.decode(enc)按指定编码拆开字节信封,还原字符。except UnicodeDecodeError:信封不合规格时抛出的异常,捕获后换下一种编码尝试。len(ch.encode('utf-8'))统计单个字符打包后的字节数,直观验证变长规律。b''.join(result)把所有字符的小信封串成一条完整的字节流。
常见误区
误区我经常听人说“这是 Unicode 编码”,于是以为 Unicode 就是一个确切的存储格式,UTF-8 只是它的另一个名字。
正解Unicode 是“字符登记处”,只负责给每个字符发码点(护照号),不管怎么存储在磁盘上;UTF-8 是具体把码点打包成字节的“信封规格”。同一个码点 U+4E2D,在 UTF-8 里是 E4 B8 AD,在 UTF-16 里是 4E 2D,在 UTF-32 里是 00 00 4E 2D。护照号是身份,信封是格式,不能混为一谈。
为何常见:因为日常使用中大家只接触 UTF-8,误把最常用的一种编码实现当成了标准本身。
误区从 ASCII 开始学,觉得所有字符都对应一个字节,写代码时想当然直接用“字数”当“字节数”去申请缓冲区。
正解UTF-8 变长 1~4 字节:英文字符 1 字节,拉丁/希腊字母 2 字节,汉字 3 字节,Emoji 4 字节。文件大小按字节算,字符串长度按字符算,两者不一定相等。“中”占 3 字节,"A"占 1 字节,截断、存储、传输时都必须分清。
为何常见:中文环境里很多老教程从 ASCII 讲起,缺乏 Unicode 变长概念,导致字节与字符在潜意识里被画了等号。
误区看到屏幕上“涓枃”、“锟斤拷”这些乱码,以为数据已经损坏,只能删掉重来。
正解乱码不是数据损坏,而是“信封读错”。原始字节流完好无损,只要找到正确的解码方式即可恢复。比如 UTF-8 字节被 GBK 解读才出现“涓枃”,换回 UTF-8 解码原样复原。真正该做的,是保留原始字节,用候选编码列表逐个尝试,而不是急着删除。
为何常见:大多数工具自动按默认编码解码,用户看不到底层字节,误认为乱码即损坏。
误区UTF-16 每个字符固定 2 字节,UTF-8 一个汉字要 3 字节,所以 UTF-16 肯定更省。
正解要分语言。中文 UTF-16 2 字节 < UTF-8 3 字节;但英文 UTF-16 2 字节 > UTF-8 1 字节,而且英文在互联网内容中占比极高。“哪个更省”取决于文本的语言分布,不存在绝对赢家。纯中文内部系统 GBK 甚至可以低到 2 字节/字。
为何常见:只拿自己熟悉的中文场景比较,忽略了 ASCII 占比极大的英文场景,忽视了 UTF-8 的 1 字节区间。
面试题
Q1. ASCII 编码中字符 'A' 的二进制位长是多少?
- A.8位
- B.7位← 正确答案
- C.16位
- D.32位
答案:B。ASCII 用 7 位二进制编号 0~127,存储时补到 1 字节(8 位)。就像小镇邮局只有 127 个信箱,7 位数字足够;多余的第 8 位只是空位,不是编号的一部分。
Q2. UTF-8 编码下,英文字母 'A' 占多少字节?
- A.1← 正确答案
- B.2
- C.3
- D.4
答案:A。UTF-8 的 1 字节规则 0xxxxxxx 与 ASCII 完全重叠,英文字符全部按 1 字节打包。相当于快递分拣师给最常用的地址用最短的 1 联标签,省纸又省力。
Q3. Unicode 码点空间的最大编号是多少?
- A.0xFFFF
- B.0x7F
- C.0x10FFFF← 正确答案
- D.0xFFFFFFFF
答案:C。Unicode 码点范围是 U+0000~U+10FFFF,共 17 个平面约 111 万码位。0x10FFFF 是“全球字符登记处”护照号的上限,超出此范围就不是合法 Unicode 字符。
Q4. 汉字“中”在 UTF-8 编码下占几个字节?
- A.1
- B.2
- C.3← 正确答案
- D.4
答案:C。“中”的码点 U+4E2D 落在 U+0800~U+FFFF 区间,对应 3 字节模板 1110xxxx 10xxxxxx 10xxxxxx,编码结果为 E4 B8 AD。3 字节是 CJK 统一表意文字在 UTF-8 中的标准长度。
Q5. UTF-8 编码中,3 字节字符的首字节前缀是?
- A.0
- B.10
- C.110
- D.1110← 正确答案
答案:D。1/2/3/4 字节的首字节前缀分别是 0、110、1110、11110,后续字节一律以 10 开头。3 字节字符首字节前缀 1110,相当于 3 联信封的规格标识,一读便知。
Q6. 以下哪些属于 UTF-8 的特点?
- A.兼容 ASCII← 正确答案
- B.无大小端歧义← 正确答案
- C.定长编码
- D.自同步← 正确答案
答案:ABD。UTF-8 是变长编码(1~4 字节),不是定长,C 错在把它当成固定字节数方案。A、B、D 分别对应兼容性、字节序无关、断点恢复三大特性,正是它在互联网胜出的原因。
Q7. 文件开头出现 EF BB BF 三个字节,说明?
- A.文件损坏
- B.带有 UTF-8 BOM← 正确答案
- C.UTF-16 LE 标记
- D.加密头
答案:B。EF BB BF 是 UTF-8 的字节序标记(BOM),用来声明“我是 UTF-8”。但它像一截多余的隐形胶带,部分程序会把它当字符显示成 ,也有的程序没有它就乱码。
Q8. 关于 UTF-16 代理对,下列说法正确的有?
- A.用于表示 BMP 之外(补充平面)的字符← 正确答案
- B.由高代理与低代理各 16 位组成← 正确答案
- C.ASCII 字符也需要代理对表示
- D.代理区码位可单独当作合法字符使用
答案:AB。代理对由高代理 U+D800~U+DBFF 与低代理 U+DC00~U+DFFF 各 16 位拼成,仅用于 UTF-16 表示超出 BMP 的字符。ASCII 在 BMP 内,2 字节直接表示,不需要代理;代理区是保留区,单独出现即为非法码位。
Q9. 经典乱码“锟斤拷”的产生原因是?
- A.UTF-8 字节流被 GBK 解码后再按 GBK 编码,再被 UTF-8 解码← 正确答案
- B.文件加密
- C.网络丢包
- D.内存不足
答案:A。“锟斤拷”是双重错配的产物:UTF-8 字节先用 GBK 解码得到乱码字符“涓枃”,再把“涓枃”按 GBK 编码回字节,最后被 UTF-8 解码成“锟斤拷”。字节流自始至终没变,变的是每次读信用的地址规则。
Q10. 关于 UTF-8 的性能与安全,下列说法正确的有?
- A.UTF-8 字节序与码点序一致,可直接按字节比较排序← 正确答案
- B.overlong encoding(过长编码)应被严格校验拒绝← 正确答案
- C.按字节截断字符串不会出错
- D.英文为主时 UTF-8 比 UTF-16 更省空间← 正确答案
答案:ABD。A 正确,UTF-8 保序,memcmp 即可做字典序比较;B 正确,过长编码(如把 ASCII 用 2 字节表示)可绕过安全校验,必须拒绝;C 错误,截断可能切开多字节字符产生 U+FFFD 替换符;D 正确,英文 UTF-8 占 1 字节,UTF-16 固定 2 字节。