发布时间:2026-05-10
浏览次数:0
这个问题的实质并非代码逻辑有误,而是编码存在不一致的情况。字符编码这类事物,在未出问题之际没人予以关注,一旦出现问题便是一连串的陷阱。数据库连接配置、表字段编码、HTTP响应头、HTML页面的meta标签,任何一个环节的编码倘若不一致均有可能引发问题。
ASCII 的 7 位设计
1960年代的时候,ASCII诞生了,它用7位二进制去表示128个字符,这些字符包含大小写字母、数字、标点以及控制字符。其中,A对应的是65(0x41),a对应的是97(0x61),0对应的是48(0x30)。直至现在,这些编号依旧是所有编码方案的基石所在。所有编码方案设计之际,均保留了ASCII的范围,而这正是向后兼容的关键要点。
刚开始设计 ASCII 时,特意留出一位不使用,那时通信线路仅需 7 位,第 8 位被用于做奇偶校验,或者被各厂商用于扩展自身字符集,是这样一种情况。
扩展ASCII的问题处于此处,那就是,每家厂商的扩展彼此互不兼容。IBM PC采用Code Page 437,另有采用-1252的情况,ISO标准运用ISO-8859-1。相同的字节0x82,于Code Page 437里为é,于-1252中是‚,在ISO-8859-1里则是未定义。早期从事国际化软件的开发者,多数时间都在与这些编码表作斗争。
之前项目邮件模板里的版权符号 © 常常变成乱码,查了许久才发现,邮件发送组件采用 ISO-8859-1 编码,然而模板文件保存的却是 -1252,这两个编码在多数区域兼容,只是在 0x80 到 0x9F 这个范围映射不一致,为此花了一整天逐行对比编码表才定位到问题。
有个真正的问题切实存在,地球上存在着几十类彼此互不兼容的编码方案,你运用某种编码保存的文件,当拿到编码设定为 Shift-JIS 的系统里面开展打开操作时,看到的内容全部都是如同乱码一般的字符,每一种语言都具备其自身独特的编码标准,像是日语存在 Shift-JIS 和 EUC-JP格式,韩语有着 EUC-KR格式,繁体中文有 Big5格式,对于简体中文而言会涉及某种编码以及 GBK格式,这样一种混乱复杂的状况必须要有某个人站出来将其终结,而这个人所对应的答案就是某个事物 那个事物符合要求 那个事物就是答案。
的解决思路
其思路极为径直:在世界范围之内赋予于每一个字符一个独一无二的数字编号。无论属于中文、阿拉伯文、emoji亦或是古埃及象形文字,每一个单独的字符都对应一个属于自身特质的编号,这个独特的编号被称作码点(code point),其书写方式为U加上十六进制数字。
编码之中,U+0041所对应的字符是A,此情况跟ASCII编码保持一致sublime text 3 乱码,这并非是一种偶然巧合 ,身为汉字国际码标准定义者的设计者特意保留了ASCII编码的编号范围 ,以此来确切保证ASCII编码文本能够直接映射到特定位置 ,对应成相应字节序列。而在表意文字统一码标准里 ,U+4E2D所对应的字符是“中” ,在表情符号标准里 ,U+1F600所对应的字符是特定表情字符。
这会儿,已然定义了差不多 15 万个字符,并且仍在持续增长。最新予以发布的 16.0 版本新增了超五千个字符,其中涵盖了部分西非地区使用的文字以及乐器相关的符号。版本的迭代始终都在持续推进着,新的表情符号像和古时所用的文字不断被增添进去。
需留意的是,仅规定字符的编号,并不规定如何于计算机里存储这些编号。恰似我给予你一个电话号码,然而我并不管控你是书写于纸张上、存储于手机当中还是铭记在脑海里。UTF - 8、UTF - 16、UTF - 32皆是存储方案,具备各异的设计取舍。同一个字符“中”,其编号为U + 4E2D,不过在不同存储方案里所占用的字节数不一样,字节内容同样不同。
平面的划分
將碼點空間劃分為十七個平面,每個平面有六五五三六個碼點,爲何恰好是十七個呢?箇中緣由是,碼點的總涵蓋範圍乃是U 加零零零零 直至U 加,總計有若干個碼點,恰好能夠劃分成十七乘以六五五三六的結果。
平面0被称作基本多语言平面(BMP),其范围是从U + 0000到U + FFFF ,它覆盖了绝大多数现代语言所使用的字符 ,诸如你日常所看到的中文、英文、日文、韩文基本上都处于这个平面之内 ,BMP的设计意图是将常用字符都安置在这里 ,通过一个16位的编码单元便能够进行表示。
辅助多语言平面 (SMP) 的平面 1,范围是 U+10000 到 U+1FFFF,主要放置历史文字和 emoji,这里是诸多后期出现的 bug 的根源所在,emoji 未处于 BMP 内,致使出现了“中 ''. === 2”这一广为人知的怪异现象。
平面2乃是表意文字补充平面(SIP),范围处于U+20000至U+2FFFF之间,那里是CJK统一表意文字的扩展区,生僻汉字在这个区域存在着,例如(U+20000提及的)乃是一个极少会被使用到的汉字,其于康熙字典当中有相关收录,这是事实。
留出了平面3至13,平面14到16用于私用及特殊之功效,供厂商自行定义字符。
BMP 内的字符,采用 UTF - 16 编码,所需只是 2 个字节,而 BMP 外的却需要 4 个字节,这种差异,直接对中字符串长度的表现产生了影响。说 BMP 外的字符“需要代理对”并不准确,准确的表述应是“UTF - 16 运用代理对来编码 BMP 外的字符”。
UTF-8 的编码规则
目前互联网上最流行的编码方案是UTF - 8,UTF - 8设计极为巧妙,它是由Ken和Rob Pike于1992年设计的,最初被用于Plan 9操作系统,而后被Unix和Linux广泛采用,最后成为Web的事实标准。
它的核心规则在于,依据码点的数值范围,运用不同长度的字节序列,以此来进行相关操作。
字节为 1 时,其覆盖范围是从 U+0000 到 U+007F,此范围恰好就是 ASCII 的范围,这表明所有 ASCII 文本从本质上来说就是合法的 UTF-8 文本,你无需进行任何形式的转换,对于一个纯粹为英文的 HTML 文件而言,将其保存为 ASCII 格式与保存为 UTF-8 格式时,字节所呈现的内容是完全一样的。
2字节,其覆盖范围为U+0080到U+07FF,存在11位有效数据位,主要用于覆盖拉丁语系扩展字符,像é、ñ、ü这类带有重音符号的字母。
3字节,覆盖U+0800至U+FFFF,有16位有效数据位,包括BMP内的绝多数字符,中文字符且日文假名、韩文谚文都在此处,一个中文字在UTF-8里占3个字节,这是文件体积变大的原因之一。
4 字节,其范围内覆盖 U+10000 到 U+状态,有着 21 位有效数据位存在于此现象中,BMP 范畴之外的字符以及 emoji皆处于此情形下,并且其中一个 emoji占有 4 字节度量值。
延续字节是那种前缀为 10 开头的字节,单字节字符是 0 开头的字节,多字节字符的起始字节是 110、1110、11110 这样开头的字节,字节在序列里的角色因为这种前缀设计而变得一目了然。
UTF-8 有个很棒的特性是自同步,要是你从一个字节序列的中间部位开始读取,至多跳过一个字符便能找到同步位置,源于延续字节皆以 10 起始,你只要找到首个并非以 10 开头的字节,就晓得一个新字符开始了,这在网络传输里数据包被截断、或者文件损坏的情形下尤为有用,与之相比,GBK 编码可不具备这个特性,丢失一个字节兴许致使后面全都乱套。
拿"中"字(U+4E2D)为例算一下编码过程:
U+4E2D = 0100 1110 0010 1101
U+4E2D 在 U+0800 到 U+FFFF 之间,用 3 字节模板
模板:1110xxxx 10xxxxxx 10xxxxxx
填入:11100100 10111000 10101101
十六进制:E4 B8 AD
这乃是为何你于 UTF - 8 编码的文件当中看到“中”字所对应的字节却是 E4 B8 AD。
又以 A(U+0041)为例,它处于 ASCII 范围之中,采用 1 字节模板:
U+0041 = 0100 0001
1 字节模板:0xxxxxxx
填入:01000001
十六进制:41
A所对应的UTF - 8编码是0x41而已,这和ASCII是趋向一致状态 的。
选取 é(也就是 U+00E9)来举例,它处于 U+0080 直至 U+07FF 这个范围里面。
U+00E9 = 0000 0000 1110 1001
2 字节模板:110xxxxx 10xxxxxx
填入:11000011 10101001
十六进制:C3 A9
é于UTF-8里是C3 A9,于Latin-1里是E9,要是你将UTF-8编码的数据当作Latin-1读取,便会展为两个字符é,此模式在乱码分析里颇为常见。
UTF-16 和 的坑
UTF - 16 针对 BMP 内所含的字符,采用 2 `字节去进行存储,而对于 BMP 外的字符,则运用 4 个字节来存储。其设计目标在于,当 BMP 字符占据多数情形时,维持较高的存储效率。
有一些字符在BMP之外,于UTF - 16里会借助代理对进行表示,码点首先要减去特定值后获得到一个20位的中间值,将这个中间值的高10位加上特定值从而得到对应的高位代理,再把该中间值的低10位加上特定值进而得到相应的低位代理。
拿 (U+1F600)举例:
码点减去 0x10000:0x1F600 - 0x10000 = 0xF600
0xF600 = 0000 1111 0110 0000 0000
高 10 位:0000111101 = 0x03D
低 10 位:1000000000 = 0x200
高位代理:0x03D + 0xD800 = 0xD83D
低位代理:0x200 + 0xDC00 = 0xDE00
UTF-16 编码:0xD83D 0xDE00
关于Java以及的字符串,其内部运用UTF - 16编码,这便是为何在其中,空字符串返回值为2,原因在于,一个表情符号占据了两个UTF - 16代码单元:
''.length // 2
'中'.length // 1(在 BMP 内,一个代码单元)
[...''].length // 1(通过迭代器按字符分割)
这类差异致使了诸多前端 bug,众多输入框限定为“最多 140 个字符”,于用 value 检查之际,emoji 被当作两个字符计算,用户分明仅输入了 100 个字外加一个表情,却被提示超出长度限制,恰当的做法是采用 . 或者 Array.from(str). 去获取实际的字符数。
更有一个隐蔽性更强的问题存在着,那就是字符串反转,对于‘abc’运用.split('',).().join(‘’)后所得到的结果并非是‘cba’,反而是呈现出了乱码的状态,这是由于split('')是依照UTF - 16代码单元来进行分割操作的,从而将代理对给拆开了,而正确的反转方式应当是().join('')。
UTF-32 为什么不是首选
按照 UTF - 32 的编码方式,它采用固定的 4 个字节来存储单个码点,这种方式具备简单直接的特点,并且没有产生任何会导致歧义的情况,其每个字符均固定占用 4 字节,在此种情况下随机访问的效率达到最高程度,即若想要获取第 N 个字符,直接前往第 N×4 字节所处位置进行读取操作即可。
事情在于过分地浪费了,“中”字(U+4E2D)于 UTF-32 里呈现为 00 00 4E 2D,相较于 UTF-8 的 E4 B8 AD,多出了一个字节,纯粹的英文文本运用 UTF-32 进行存储,相较于 UTF-8 而言大了 4 倍,一个 1KB 的英文 HTML 页面借助 UTF-32 来存储,将会转变为 4KB。
一个别的问题系字节序,4 字节的整数存有大端与小端这两种表示,UTF - 32 文件常常要于开头放置 BOM 用以标识字节序,这增添了处理的复杂度,于不同字节序的机器之间传输 UTF - 32 数据,要是不处理字节序问题,读出来的皆为乱码。
UTF - 32基本上不被用于进行存储以及传输,少数系统内部处理的时候会用到它,像某些API的内部字符串表示这种情况sublime text 3 乱码,在大部分情形下,UTF - 8是更为良好的选项,联盟自身也推举优先采用UTF - 8。
三种方案的横向对比
三种方案各有优劣,选哪个取决于场景:
UTF-8 身为互联网当中的事实标准,与 ASCII 文本相兼容,具备存储效率高的特性,能够实现自同步,不存在字节序方面的问题。针对英文文档而言,采用 UTF-8 方式几乎不会使体积有所增加。中文文档的体积大概是 GBK 的 1.5 倍,其中一个汉字在 GBK 里占据 2 字节,在 UTF-8 里占据 3 字节,增大了 50%。然而,这样的代价是具有价值的,原因在于 UTF-8 能够同时对中文、日文、韩文、emoji 进行处理,而无需进行编码切换。目前,Web页面、API接口以及数据库连接默认均采用UTF-8。自MySQL 8.0起,其默认字符集便是如此 ,能够完整地支持4字节emoji。
字符最多的场景处于UTF - 16时,储存效率最高是在BMP内。中文、日文、韩文都于BMP内,采用UTF - 16存储的话是2字节,相比于UTF - 8的3字节更为节省空间。NT系列以及Java/内部运用UTF - 16。然而BMP外的字符存在代理对,处理较为复杂,相比UTF - 8而言。于判断一个字符串字节长度时,需要检查每个字符是否处于BMP内。
UTF - 32有着固定长度这样一个优势,其随机访问的效率是比较高的,可其存在体积大以及字节序方面的问题,代价是有的,除开某些存在需要随机访问字符的底层处理场景之外,它基本上是不会被使用的。
要是你正处于挑选数据库编码的状况下,MySQL 自 5.5.3 起始所支持的 乃是 UTF-8 的完备实现。旧版本的 utf8 别名实际上仅支持至多 3 字节,会出现截断 emoji 的情况。
乱码是怎么产生的
乱码的原因非常简单:写入时用一种编码,读取时用另一种。
最为常见的情形是,GBK编码的文本被当成UTF-8来读取。中文的字符,在GBK里是2字节,在UTF-8里是3字节。当UTF-8解码器碰到GBK编码的字节序列时,会发觉许多不合法的字节组合,进而输出一堆 ���(U+FFFD替换字符)。有一个采用GBK编码的“中”字,其编码为(D6 D0),当运用UTF-8解码时,会发觉D6并非合法的起始字节,而且两个字节均无法与UTF-8模板相匹配,最终输出两个 ���。
另有一类情形更为隐匿 ,存在着名为 “宽吞” 的属性特征。ISO - 8859 - 1 具有这样 的能力 ,即能够对任意字节序列履行解码操作 ,且不会产生报错现象 ,究其原因在于它将从 0x80 至 0xFF 的全部字节均映射为了有效的字符。倘若把经由 UTF - 8 编码的数据误作 进行解码处理 ,随后再依据 UTF - 8 予以重新编码 ,便会呈现出特定形态的乱码状况。就好比 é 的 UTF - 8 编码是 C3 A9 ,当被 解码之后会转变为两个字符 Ã(U + 00C3)以及 ©(U + 00A9),之后再次按照 UTF - 8 编码便显示为 é。这种模式具备显著特性,那就是一旦瞧见以 à 作为起始的情况,大体上能够判定是 UTF-8 出现了误读现象。
早前有一位同事,其PHP程序所输出的JSON,于浏览器里呈现为ç§这般的乱码。致使如此的缘由在于,PHP默认情况下将非ASCII字符转义成\uXXXX形式,于是为了“优化”便关掉了转义选项,直接以原始UTF - 8予以输出。页面在加载之际,响应头未声明= utf - 8,浏览器采用默认的ISO - 8859 - 1解码。致使的结果便是,UTF - 8的每个字节被逐个解释成了一个字符。
处理这类问题的法子仅有一个,即从起始点直至末尾的编码维持一致!用于数据库连接这一步骤要使用SET NAMES,然后页面声明为 =utf-8,再者文件保存选定UTF-8 BOM模式,接着编辑器的编码同样设置成UTF-8,顺带HTTP响应头要添加-Type: text/html; =utf-8 ,如此全链路达成统一,问题就会自然而然地消逝!
BOM 带来的麻烦
编号排列方法(字节次序标记)是设于文档开端的若干特别字节,用以区分编码形式以及字节顺序。
表述的是,UTF - 8的BOM呈现为EF BB BF ,UTF - 16 LE呈现样子是FF FE,UTF - 16 BE呈现样子为FE FF。
平台的编辑器,其默认情况下,会于UTF-8文件开头添加BOM,记事本的“另存为UTF-8”所保存的文件就带有BOM,然而Unix/Linux下的工具并不认识它,编译器、脚本解释器、Shell会将EF BB BF当作普通文本内容来处理,致使第一行出现那种不可见的额外字符。
我于前端项目当中碰到过此类问题,某位同事在其上对一个CSS文件予以编辑,之后添加了BOM,在浏览器解析CSS那儿,第一行被诠释成无效内容,致使整个样式文件加载出现异常,于构建工具里增添自动去除BOM的处理步骤后,问题得以解决,Git的core配置也无法处理BOM问题,因为BOM属于内容层面,并非换行符层面。在Linux系统里,能够运用file命令去核查文件是不是含有BOM:file .css ,要是输出之中涵盖"UTF-8 (with BOM)" 那就得进行处理。
三种编码方案对同一个字符的字节表示对比:
工具验证
在调试编码问题之际,我时常要去确认某一个字符于各异编码情形下的字节序列。对于转换 - 编码转换 | 船长工具箱而言,能够直接输入字符进而查看其UTF - 8、UTF - 16、UTF - 32编码结果。当排查乱码之时,将可疑的乱码字符放置进去转上一圈 ,通常就能瞧出来它是从哪一种编码“误读”而来的。仿若瞧见é,于工具之中查找一下Ã以及©所对应的UTF-8编码,接着与é的UTF-8编码予以对比,很快便可确认是UTF-8→的误读链条。
写在最后
字符编码方面的问题,平常情况下不会主动来寻你,然而一旦遭遇那可就是耗费大半天时间。在领会了、UTF - 8、UTF - 16之间的关系之后,开展排查乱码问题的工作会快出许多。重点在于记牢一项原则:全链路一律采用UTF - 8。关于数据库、文件、HTTP响应头、HTML页面,全都设置成UTF - 8,绝大多数的编码问题便不会出现。
倘若你当下这会儿尚未遭遇到编码方面的问题,很可能仅是凭借着运气较好而已,早晚终究势必会碰到的哒。预先去弄明白这些基础性的原理,当遭遇之时才不至于手忙脚乱的哟。
如有侵权请联系删除!
Copyright © 2023 江苏优软数字科技有限公司 All Rights Reserved.正版sublime text、Codejock、IntelliJ IDEA、sketch、Mestrenova、DNAstar服务提供商
13262879759
微信二维码