资讯动态

彻底搞懂位、字节、字符:编码原理与乱码解决实战

发布时间:2026/10/19 3:21:01 来源:尧图企业网站定制
位、字节、字符……这三个词几乎出现在每一本编程入门书的前几十页但也是第一批会在项目里坑你的老朋友。你以为自己懂了位就是二进制的一位字节就是 8 个位字符就是像a、中这样的符号。可真到干活的时候同一个中字一会儿占 2 个字节一会儿占 3 个字节文件传到一半变成乱码数据库存进去再拿出来完全不是当初那个样子。我在这行干了十几年接手的乱码问题、存储异常问题往回追根基本都是这三个概念没对齐。这篇文章就从干活的角度把位、字节、字符的底细彻底捋清楚顺便把字符编码、BOM、大小端这些绕不开的坑一次性讲明白。1. 先把名词对齐位、字节、字符到底是谁1.1 位bit只有一个“有/没有”的开关计算机底层没有那么多玄学说到底就是一群开关。一个开关两种状态——通电和断电我们就记成 1 和 0。一个这样的开关就是一个位英文叫 bit中文也叫比特简写是b。位是最小的信息单位它本身存不了什么“有意义”的内容只能表达两种状态。两个开关组合就是 4 种状态00、01、10、11三个开关组合是 8 种状态n 个开关组合就是 2 的 n 次方种状态。这就是为什么计算机世界里到处是 2、8、16、32、64、256、1024 这种数字——它们都是 2 的幂。这里顺带提一个特别容易搞混的点平时说“32 位系统”“64 位系统”这里的“位”不是指内存只有 32 个 bit而是指 CPU 的寄存器宽度、一次能处理的整数位数。一个 64 位系统里一块内存照样有几十 GB 的容量。所以“位”在不同语境下含义要跟着上下文走别看到一个“位”字就以为是二进制的一位。1.2 字节byte机器眼中的最小“一个字”8 个位排成一队就是一个字节英文叫 byte简写是B。为什么要凑 8 个不凑 6 个、10 个这里面有历史原因早期 ASCII 字符集用了 7 位但 7 这个数字在计算机里非常别扭内存寻址、存储规划都喜欢按 2 的幂来8 正好是 2 的 3 次方还能留一位做扩展或校验于是 8 位一组的模式成了事实标准。一个字节能表示 0 到 255一共 256 种组合。这个容量在大家眼里好像不大但它是计算机内存、文件存储、网络传输里真正的最小寻址单位。你可以理解成内存是一整面墙的抽屉每个抽屉正好放一个字节8 个开关CPU 要读写数据最少也是按一个抽屉来操作不可能只打开半个抽屉。单位换算也是个经典大坑。教科书上写 1KB 1024B、1MB 1024KB这是二进制习惯。而网络带宽和硬盘厂商经常按十进制算1KB 1000B。所以 100M 宽带的理论下载速度是 100Mbps ÷ 8 ≈ 12.5MB/s这里的“Mb/s”是兆位每秒不是兆字节每秒大写小写差 8 倍。遇到这种换算问题先搞清楚说的是 bit 还是 byte能避免很多看起来像“网速被坑了”的误会。1.3 字符character给人看的符号跟存多长没有必然关系字符是语义层面的东西。a是一个字符中是一个字符是一个字符也是一个字符。字符是给人看的位和字节是给机器用的。问题来了一个字符到底占几个字节答案是不一定完全取决于你用的编码方案。同一个“中”字在 GBK 编码里占 2 个字节在 UTF-8 编码里占 3 个字节在 UTF-16 编码里占 2 个或者 4 个字节。你没法只凭“这是个字符”就断定它占几个字节必须知道它用什么编码。这就像同样一个地名中文写“北京”英文写“Beijing”日文写“北京”形式不同但指代同一个地方。字符和字节之间需要一个翻译规则这就是编码。2. 编码是把字符翻译成字节的那张表2.1 编码的本质是两层东西字符集 编码方案很多人把“字符集”和“编码”混着说实际上它们是两个维度。字符集character set负责给字符发号把每个字符映射成一个数字这个数字叫码点code point。比如英文大写A在 Unicode 里的码点是 U0041汉字中的码点是 U4E2D。码点只是“编号”不代表怎么存。编码方案encoding负责把码点变成字节序列。同一个码点在 UTF-8 里是一种字节排法在 UTF-16 里是另一种在 GBK 里可能根本没有对应关系。用一个生活类比字符集是城市的门牌号系统每个地址有唯一编号编码方案是寄件的打包方式——用纸箱还是塑料袋、贴几张面单、怎么塞填充物。地址是同一个打包方式不一样运出去的样子就完全不一样。2.2 ASCII 为什么只用到 127 个位置聊编码绕不开 ASCII。上世纪 60 年代美国搞出了一套字符编码把英文字母、数字、标点和一些控制字符统一编号一共 128 个字符用 7 位二进制就可以表示。这套东西叫 ASCII美国信息交换标准代码。为什么是 127 而不是 255因为它只服务英语。26 个大写字母、26 个小写字母、10 个数字、若干标点和控制符30 多个控制字符加在一起128 个位置绰绰有余。控制字符是那些不显示但能“做事”的字符比如换行LF、回车CR、退格BS、响铃BEL。这类字符虽然看不到但在文件解析、网络协议里很重要比如\n的 ASCII 码就是十进制的 10。ASCII 的编号表要熟记几个常用值干活时候真用得上字符ASCII 十进制ASCII 十六进制0480x30A650x41a970x61换行 LF100x0A回车 CR130x0D空格 Space320x20这几个数记得住你在排查协议数据、看十六进制日志的时候会轻松非常多。我曾经调试一个二进制协议对方文档只写了“消息头长度字段是 0x61”我一看就对上了这是小写字母a说明长度是 97省了好几个小时。2.3 第 8 位被各国拿走之后乱码的种子埋下了ASCII 用 7 位计算机存储流行 8 位多出来的第 8 位留给了“扩展”。有人觉得 0-127 之外还有 128 个位置闲着也是闲着于是各国开始往这 128 个位置里塞自己的字符。问题在于没有人统一塞法。欧洲国家把 128-255 填成带重音的字母希腊人填成希腊字母俄罗斯人填成西里尔字母我们中国人呢汉字成千上万128 个位置根本不够塞。更麻烦的是同一个字节在不同国家的扩展表里代表完全不同的字符。比如字节 0xE9在某个西欧编码里是é在另一个编码里可能是别的字。这就是乱码产生的根源之一同一串字节用不同编码表去解读得到完全不同的文本。你看到的“乱码”往往不是你数据坏了而是你的程序用错了编码表去翻译那串字节。3. 中文的入场GB2312、GBK、GB18030 是怎么迭代的3.1 一个字节不够那就两个字节先说 GB2312 的区位码中文要进计算机必须解决一个核心矛盾汉字不是几十几百个而是几万个。1 个字节最多 256 种组合连常用汉字都装不下2 个字节最多 65536 种组合装常用汉字绰绰有余。于是 1980 年国家发布了 GB2312 标准。GB2312 的设计很有特色它把汉字放在一个 94×94 的矩阵里每个汉字对应一个区位码第一字节叫“区”第二字节叫“位”范围都是 1 到 94。存储的时候区号和位号各自加上 0xA0也就是十进制的 160变成两个字节。举个例子“中”字的区位码是 54-48区号 54 加 160 等于 214也就是十六进制 0xD6位号 48 加 160 等于 208也就是 0xD0。所以“中”在 GB2312 里的字节就是 D6 D0。这套“偏移 0xA0”的设计保证了一个细节所有 GB2312 双字节的高位都大于 0xA0跟 ASCII 的 0x00-0x7F 天然区分开程序读到字节后能判断这是英文字符还是中文字符的一部分。GB2312 收录了 6763 个常用汉字和 682 个其他符号。当年这覆盖了绝大多数日常用字所以迅速铺开。3.2 GBK 把双字节空间彻底撑开GB2312 有个问题只覆盖常用字很多繁体字、生僻字、人名地名用字没有。于是 1995 年前后出现了 GBK 编码。GBK 的 K 是“扩展”的意思它在 GB2312 的基础上大幅扩展能表示 2 万多个字符完全兼容 GB2312——这意味着只要是 GB2312 能编的字GBK 的字节编码完全一样老文件在新编码体系下不会变样。GBK 的做法很聪明它扩宽了第一字节和第二字节的取值区间。第一字节用 0x81-0xFE第二字节用 0x40-0xFE跳过单字节值 0x7F两个字节组合出两万多个码位既兼容 ASCII 又兼容 GB2312。要注意的是GBK 并不是国家标准而是一个事实上广泛使用的编码规范后来国家在 GB18030 标准里把 GBK 吸收进去了。在很长一段时间里Windows 简体中文系统的默认 ANSI 编码就是 GBK所以很多老一辈的中文软件、旧网站、老数据库里存的都是 GBK 文本。你现在打开某些老的 txt 文件或者老系统的导出文件看到的中文乱码很多就是 GBK 和 UTF-8 之间的“翻译事故”。3.3 GB18030 用四字节兜住整个 UnicodeGBK 虽然能装两万多字符但还不是所有汉字都收得进去更别说其他语言文字。于是国家又推出 GB18030它采用 1、2、4 字节变长编码单字节完全兼容 ASCII双字节兼容 GBK四字节用来表示 GBK 双字节空间之外的 Unicode 码点。也就是说从覆盖范围上看GB18030 可以表示所有 Unicode 字符只是存储形态是中文体系自己的。某些政务系统、公共平台强制要求支持 GB18030所以做相关业务的人要认识这个词。实际开发中如果你不确定目标环境用哪种中文编码统一走 UTF-8 通常是最稳妥的除非对接的老系统明文要求 GBK 或 GB18030。3.4 双字节编码的特有问题解析时不能跳着看GB2312、GBK 这类双字节编码有个天然坑一个汉字是两个字节但你无法从单个字节知道它是不是汉字的一部分只能线性扫描判断。如果内容里混着半角字符和全角字符解析时一旦在错误的位置截断后半段全部错位。举个例子如果一个字符串本应是“中文abc”按 GBK 存成字节D6 D0 61 62 63。如果你从第 1 个字节开始读能正确解析成“中”“文”“a”“b”“c”如果你从第 2 个字节开始读D0 和 61 会被当作一个汉字的两个字节直接解出一个怪字。这就是为什么二进制协议里处理中文字符串一定要在协议层写明“字符串长度按字节计”而不是按字符计否则解析错位你连原因都找不到。4. Unicode 和 UTF-8全世界字符的统一门牌号4.1 Unicode 只编号不存储各国各搞一套编码最直接的恶果就是国际协作时鸡同鸭讲。为了解决这个问题Unicode 标准出现了它给世界上几乎所有字符都分配一个唯一码点覆盖各大语言文字、数学符号、emoji、特殊符号目标是“一个字符一个号天下大同”。在 Unicode 体系里A的码点是 U0041中的码点是 U4E2D好的码点是 U597Demoji的码点是 U1F602。码点是一个整数范围从 0 到约 110 万理论上足够覆盖人类已知符号。但 Unicode 不规定怎么存储。同一个 U4E2D你可以把它存成 2 字节的 4E 2D也可以存成 3 字节的 E4 B8 AD也可以存成 4 字节的 00 00 4E 2D——具体看你选哪种编码方案。Unicode 只是门牌号运输方式由 UTF-8、UTF-16、UTF-32 这些方案决定。4.2 UTF-8 的变长规则手把手算一遍UTF-8 是目前最主流的编码方案几乎是互联网事实标准。它的核心设计是变长 1 到 4 字节而且刻意做到了完全兼容 ASCII码点 0 到 127UTF-8 编码后就是单字节本身。这意味着你任何一个纯英文 UTF-8 文件同时也是一个合法的纯 ASCII 文件。UTF-8 的编码规则就几条按二进制位拆开看码点范围UTF-8 字节模板0x00 - 0x7F0xxxxxxx1 字节兼容 ASCII0x80 - 0x7FF110xxxxx 10xxxxxx2 字节0x800 - 0xFFFF1110xxxx 10xxxxxx 10xxxxxx3 字节0x10000 - 0x10FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx4 字节规则的精髓在于每个多字节序列都用“10”打头的字节当续体所以程序从一个字节就能判断如果开头是 0那就是单字节 ASCII如果是 110、1110、11110说明后面还有 1、2、3 个续体字节如果开头是 10说明你从中间插进来了数据可能被截断了。拿“好”字练一遍它的码点是 U597D转成二进制是0101 1001 0111 1101一共 16 位。16 位落在三字节区间按1110xxxx 10xxxxxx 10xxxxxx的模板把 16 位拆成 466 三段0101、100101、111101。填进去第一字节1110 0101 0xE5第二字节10 100101 0xA5第三字节10 111101 0xBD所以“好”在 UTF-8 里就是E5 A5 BD三个字节。你可以用hexdump随便看一个存着“好”的文件一定是这个结果。算过一遍之后你会突然明白为什么网上常有人说 UTF-8 中文是 3 字节——因为常用汉字的码点基本都在 U0800 到 UFFFF 之间正好落进三字节区间。4.3 UTF-16、BOM 和大小端UTF-16 是另一套方案它默认把码点存成 2 字节遇到超出 65535 的码点比如 emoji用一对 2 字节的代理对来表示总共 4 字节。JS 内部、很多 Windows 原生 API 用的是 UTF-16。UTF-16 有个绕不开的问题多字节数据存到文件或传进网线时到底是高位在前还是低位在前如果先写低字节叫小端little-endian先写高字节叫大端big-endian。为了告诉读者自己用的是哪种人们发明了 BOM字节序标记。常见 BOM 是这样的编码BOM 字节含义UTF-8EF BB BF通常是多余的但可用来标记“这是 UTF-8”UTF-16 LEFF FE小端UTF-16 BEFE FF大端有意思的是BOM 本身也是为了解决“我的字节序是什么”的问题但 BOM 也带来了新的麻烦有些程序不认 BOM会把 EF BB BF 当成三个普通字符显示成“锘”之类的怪字有些老接口会把带 BOM 的文件内容拼在字符串前面导致解析第一个字段失败。所以实践中纯文本和程序内部基本都用 UTF-8 无 BOM只有需要给特定软件比如某些版本的 Excel识别时才加 BOM。这里我要明确说结论新项目、新协议、新数据库能选 UTF-8 就选 UTF-8。理由很实际兼容 ASCII没有大小端问题没有 BOM 歧义网页和 JSON 的默认编码就是它网络传输中也不会因为一个字节序搞出灵异现象。4.4 特殊字符为什么 emoji 和组合字符最容易出事这个部分要专门说一说“特殊字符”。现代文本里真正让程序员头疼的往往不是汉字而是以下几类emoji 和生僻字码点在 U10000 以上UTF-8 占 4 字节JS 的 UTF-16 里还占 2 个逻辑单元。你用函数量长度、截断字符串、统计字数时非常容易算错。组合字符比如é可以是预合成的单码点 U00E9也可以是eU0065紧跟一个组合音调符 U0301。显示出来差不多但前者长度是 1后者长度是 2。做字数统计、数据库字段长度校验时这类字符很容易引发“DB 说长度超了但我明明没写几个字”的诡异错误。零宽字符和方向控制符零宽空格、零宽连接符、阿拉伯文方向的强制标记等。这类字符肉眼看不见但真实存在于字符串里能导致字符串比较失败、搜索引擎关键词不匹配、甚至文件名“看着一样但就是打不开”。全角符号 全角和半角A看起来接近码点完全不同直接做字符串比较会返回不相等。处理这类特殊字符的正确习惯是不要用“看得见几个字符”去判断文本长度而要用你所在运行环境的明确规则去计算涉及存储和传输时先把字符串编码成字节再看字节长度做校验时统一用 code point 或者 grapheme cluster 的标准而不是凭直觉。5. 实操现场用命令和代码把编码看个明明白白5.1 hexdump 看真实字节排查编码问题最忌讳的就是用“肉眼”在编辑器里看乱码猜原因。正确做法是先看原始字节。假设你有一个 UTF-8 文件内容是“中ab”$ printf 中ab demo.txt $ hexdump -C demo.txt 00000000 e4 b8 ad 61 62 |...ab|e4 b8 ad正好是“中”的 UTF-8 编码后面61 62是a和b一共 5 个字节。你不需要认识这些字节只需要知道“中”在这里是 3 字节。如果把它转成 GBK 再看$ iconv -f UTF-8 -t GBK demo.txt | hexdump -C 00000000 d6 d0 61 62 |..ab|“中”变成d6 d0两个字节整个文件 4 个字节。这一步操作很有价值它让你直观看到同一个字符在不同编码下的字节形态差异。不同系统上file命令也能帮你判断文件大致编码比如$ file -i demo.txt demo.txt: text/plain; charsetutf-8但file只是猜测不一定准尤其是短文本。真正靠谱的做法永远是看字节、对照表不要靠猜。5.2 Python 里 str 与 bytes 的关键区别Python 3 把字符串和字节分得很清楚这恰恰是理解编码概念的最佳教材。字符串str是字符集合字节串bytes是原始字节集合两者通过encode和decode互相转换s 中文abc print(len(s)) # 5字符数 b s.encode(utf-8) print(len(b)) # 9UTF-8 字节数两个汉字 3*2 三个英文 print(b) # b\xe4\xb8\xad\xe6\x96\x87abc print(b.decode(utf-8)) # 中文abc如果你用 GBK 去编一个 emojitext ok text.encode(gbk) # UnicodeEncodeErrorGBK 里没有 emoji 的映射这就是特殊字符和旧编码之间的经典冲突。反过来如果拿错解码表去解一串字节也可能得到一堆“看起来像中文但其实毫无意义”的怪字bad b\xe4\xb8\xad\xe6\x96\x87.decode(gbk) print(bad) # 一串奇怪的汉字 restored bad.encode(gbk) print(restored.decode(utf-8)) # 中文这个“先按错误编码解出怪字再按错误编码编回去最后用正确编码解”的过程就是乱码恢复的常用手法。它能救回来一部分误用错编码读取的文件但不是万能的——如果中间某步把字符替换成了?或者原始信息就永久丢失了。5.3 乱码排查标准流程遇到乱码按下面的顺序走能省掉大量碰运气的时间搞清楚原始文本的真实编码来源文件是用什么工具生成的HTTP 响应头里charset写了什么数据库连接串和表字段的字符集是什么老系统导出的文件八成是 GBK网上爬下来的页面按 HTML 的 meta 声明判断。先用 hexdump 或 xxd 看一眼原始字节确认“字节层到底是什么”这一步别跳。在字节层面做转换不要对着一堆乱码字符反复另存为、复制粘贴那样只会让编码错误叠加。每次转换只做一步从已知的原始编码一次性转换到目标编码。如果文本里出现UFFFD说明已经有不可逆的替换发生了这时候别继续转换先回源找原始数据。6. 经典乱码速查表与多年避坑心得6.1 几个典型乱码“指纹”你看到的乱码最常见成因处理思路锟斤拷锟斤拷无效 UTF-8 字节被替换成 UFFFDEF BF BD又被当成 GBK 解码回源找原始字节别在这层继续转ä¸-æ–‡、é之类UTF-8 字节被按 Latin-1/Windows-1252 解读把文本按原错误编码反向编码回字节再按 UTF-8 解码涓栫晫这类“中文但不像话”UTF-8 中文被按 GBK 解读同样手段先按 GBK 编码还原字节再按 UTF-8 解码满屏?????数据库连接或写入时做了有损替换源字符不存在于目标字符集源头已经丢信息只能修改连接参数和字段字符集后重新导数据烫烫烫或屯屯屯调试模式下未初始化内存被填充了固定字节这些字节被当作 GBK 汉字显示不是编码问题是访问了未初始化内存文件开头有锘或?带 UTF-8 BOM 的文件被按 GBK 读取去掉 BOM或改用 UTF-8 读取这里最有意思的是锟斤拷。它的字节来源是 EF BF BD 重复出现——也就是一堆被替换成了 UFFFD 的无效字符——再被 GBK 解读后就变成“锟斤拷锟斤拷”。你只要看到这几个字基本可以断定数据已经经历过两次编码伤害一次丢失信息一次错误翻译。这种乱码几乎不能恢复最好的办法是找原始数据源。6.2 让编码少出问题的工作习惯最后说几个我从多次踩坑里总结出来的习惯不一定写在哪本教科书里但非常实用。第一新项目默认 UTF-8没有例外。数据库表字符集、连接字符串、文件读写、HTTP 响应头、HTML meta 标签全部显式声明 UTF-8不依赖环境默认值。环境默认值是最不可控的变量同一个程序在服务器上跑的编码和你本机可能完全不一样。第二文本跨系统传递前先约定编码再传数据。哪怕是内部系统也要在接口文档里写清“所有字符串按 UTF-8 编码”。如果对接老系统必须用 GBK那也要在中间层显式转换不要指望着双方恰好一样。第三读写文件时永远显式指定编码。很多语言的文件 API 都有默认编码而这个默认值跟操作系统 locale 有关。你以为写的是 UTF-8实际可能按本地 ANSI 写了文件换个环境就炸。Python 里读写文件建议这样写with open(out.txt, w, encodingutf-8, newline) as f: f.write(中文) with open(out.txt, r, encodingutf-8) as f: text f.read()第四测试用例里故意放上中文、日文、emoji、组合字符。开发的时候全是 ASCII 的代码测不出编码问题。我第一次意识到这个是因为一个“看起来完美”的工具在客户的中文路径上直接崩了原因就是某层代码用了默认编码去拼路径。从那以后凡是涉及字符串长度、截断、存储字段的测试我必放几个特殊字符进去。第五做代码审查和运维排查时看到乱码不要急着“换一个编码打开”。先从字节判断再决定转换链永远不要在 GUI 里反复瞎试。一个人如果连“字节和字符不是一回事”都没想清楚那修乱码就是在赌运气想清楚了乱码问题十有八九能定位到具体某一层代码。我在实际项目里见过太多因为编码问题导致的“事故调查”——从日志乱码一路查到数据库最后发现只是某个接口在返回时少写了一行charsetutf-8。这种问题不难但特别浪费时间。希望你看完这篇之后以后再遇到乱码第一反应不是“这文件坏了”而是“这串字节是用哪张表翻译出来的”。想明白这个问题你离真相就只剩一条命令的距离。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑