本文章首发于我的博客,鉴于 sf 博客样式具备赏心悦目的美感,遂发表于此。javascript
本文还在持续更新中,最新版请移步原博客。html
最近在看《Dive Into Python 3》,第四章讲了字符串相关知识,看后才发现,字符串远比咱们想象的要复杂多。就像该书所说的java
Everything you thought you knew about strings is wrong.python
是的,我以前对字符串的理解都是错的。linux
也许你会诧异,字符串有什么难的,即使遇到乱码的状况随便 Google 下就能找到解决方法,可是这样你不以为有种被动的感受嘛,我以为和学习任何东西同样,学习编程首要是学习其思想,知道某事物为何(why)要这么作,至于如何作(how)那只是前辈们提出的解决方案,咱们能够参考,随便掌握下来。git
本文下面首先讲解字符、字符串、编码、ASCII、Unicode、UTF-8 等一些基本概念,而后会介绍在使用计算机时是如何如编码打交道的,也就是实战部分。
但愿你们在阅读完本文后,都能对 string 有一全新的认识。github
当咱们谈到字符串(string或text)时,你可能会想到“计算机屏幕上的那些字符(characters)与符号(symbols)”,你正在阅读的文章,无非也是由一串字符组成的。可是你也许会发现,你没法给“字符串”一明肯定义,可是咱们就是知道,就像给你一个苹果,你能说出其名字,可是不能给出准肯定义同样。这个问题先放一放,后面我再解释。编程
咱们知道,计算机并不能直接处理操做字符与符号,它只认识 0、1 这两个数字,因此若是想让计算机显示各类各样的字符与符号,就必须定义它们与数字的一一映射关系,也就是咱们所熟知的字符编码(character encoding)。你可简单的认为,字符编码为计算机屏幕上显示的字符与这些字符保存在内存或磁盘中的形式提供了一种映射关系。字符编码纷繁复杂,有些专门为特定语言优化,像针对简体中文的编码就有 GB2312,GBK(GB 是 Guajia Biaozhun 的简写)等,针对英文的 ASCII;另外一些专门用于多语言环境,像后面要讲到的 UTF-8。vim
咱们能够把字符编码看做一种解密密钥(decryption key),当咱们收到一段字节流时,不管来自文件仍是网络,若是咱们知道它是“文本(text)”,那么咱们就须要知道采用何种字符编码来解码这些字节流,不然,咱们获得的只是一堆无心义的符号,像 ������。api
计算机最先起源于以英文为母语的美国,英文中的符号比较少,用七个二进制位就足以表示,如今最多见也是最流行的莫过于 ASCII 编码,该编码使用 0 到 127 之间的数字来存储字符(65表示“A”,97表示“a”)。
咱们知道一个字节是 8 位,ASCII 编码其实只使用了其中的低 7 位,还剩下 1 位。在早期,不少 OEM 厂商就想着能够利用 128-255 来表示一些特殊符号,其中比较有名的是 IBM-PC 提出了 OEM 字符集(character set),为欧洲国家的语言提供注音以及一些线画符号(line drawing characters),以下图,利用这些人们能够在计算机上画一些有趣的图形。
随着计算机的普及,使用计算机的人再也不仅仅局限于以英文为母语的国家,不一样国家的 OEM 字符集也如雨后春笋,层出不穷。因为0-127的编码已经固定下来,因此它们大都使用128-255来编码本身的符号集。例如,有些地方用 130 表示 é
,但在以色列表示为 Hebrew letter Gimel ג
。
这种 OEM 厂商“混战”的状况最终被 ANSI 标准制止,在 ANSI 标准中,对于0-127之间的编码与 ASCII 保持一致,最高位用 0 填充。可是对于128以及之上的编码,争议比较大,不一样地区每每不一致,这些不一样的编码,致使了不一样的 code pages 的产生。
能够看到,只是单字节的编码问题就已经存在这么严重的问题了,像中文(Chinese),日文(Japanese),韩文(Korean)(业界通常称为 CJK)等象形(表意)文字(ideograph-based language),字符数量比较多,1 个字节是放不下的,因此须要更多的字节来进行字符的编码。和单字节编码同样,若是没有统一的规范,不一样国家本身定制本身的编码标准,那么不一样国家之间是没法进行交流的,因此,须要一个囊括世界上全部字符的编码方案的出现,可是这里还有个问题,若是用多字节来对字符进行编码,那么对于 ASCII 字符来讲,是比较浪费空间的,针对这两个问题,聪明的人们提出了一全新的字符编码方案——Unicode。
Unicode 的全称是 universal character encoding,中文通常翻译为“统一码、万国码、单一码”。
Unicode 主要解决了前面所提到的两个问题:
统一世界上全部字符的编码,使得语言不一样的国家也能正常交换信息
提出了一个中间层,使得字符的编码与存储形式分离解耦,这样不一样国家就能够采用不一样的存储方案,来解决单字节表达字符数有限与多字节编码浪费的矛盾。
Unicode 的关键创新点在于为字符编码与最终的存储形式加了一中间层(术语为 code points),这样,当一种语言有新字符产生时,只需分配新的 code point 便可,具体的存储形式(一个字节仍是两个字节,采用大端仍是小端)不须要关心。
这让我想到了以前在 SICP 看到的一句话,真是软件开发领域的银弹
任何问题,均可以经过增长一层抽象来解决。
Unicode 中采用四个字节来定义 code point,每个 code point 都表明世界上惟一的字符,不会出现同一 code point 在不一样国家表示不一样字符的状况。好比,U+0041
老是表明A
,即使某语言中没有这个字符。
Unicode 的存储形式通常称为UTF-*
编码,其中 UTF 全称为 Unicode Transformation Format
,常见的有:
UTF-32 编码是 Unicode 最直接的存储方式,用 4 个字节来分别表示 code point 中的 4 个字节,也是 UTF-*
编码家族中惟一的一种定长编码(fixed-length encoding)。UTF-32 的好处是可以在O(1)
时间内找到第 N 个字符,由于第 N 个字符的编码的起点是 N*4 个字节,固然,劣势更明显,四个字节表示一个字符,别说以英文为母语的人不干,咱们中国人也不干了。
UTF-16 最少能够采用 2 个字节表示 code point,须要注意的是,UTF-16 是一种变长编码(variable-length encoding),只不过对于 65535 以内的 code point,采用 2 个字节表示而已。若是想要表示 65535 之上的字符,须要一些 hack 的手段,具体能够参考wiki UTF-16#U.2B10000_to_U.2B10FFFF。很明显,UTF-16 比 UTF-32 节约一半的存储空间,若是用不到 65535 之上的字符的话,也可以在O(1)
时间内找到第 N 个字符。
UTF-16 与 UTF-32 还有一个不明显的缺点。咱们知道不一样的计算机存储字节的顺序是不同的,这也就意味着
U+4E2D
在 UTF-16 能够保存为4E 2D
,也能够保存成2D 4E
,这取决于计算机是采用大端模式仍是小端模式,UTF-32 的状况也相似。为了解决这个问题,引入了 BOM (Byte Order Mark),它是一特殊的不可见字符,位于文件的起始位置,标示该文件的字节序。对于 UTF-16 来讲,BOM 为U+FEFF
(FF 比 FE 大 1),若是某 以 UTF-16 编码的文件以FF FE
开始,那么就意味着字节序为小端模式,若是以FE EE
开始,那么就是大端模式。
UTF-16 对于以英文为母语的人来讲,仍是有些浪费了,这时聪明的人们(准确说是Ken Thompson与Rob Pike)又发明了另外一个编码——UTF-8。在 UTF-8 中,ASCII 字符采用单字节。其实,UTF-8 前 128 个字符与 ASCII 字符编码方式一致;扩展的拉丁字符像ñ
、ö
等采用2个字节存储;中文字符采用 3 个字符存储,使用频率极少字符采用 4 个字节存储。因而可知,UTF-8 也是一种变长编码(variable-length encoding)。
UTF-8 的编码规则很简单,只有二条:
对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 code point。所以对于英语字母,UTF-8编码和ASCII码是相同的。
对于n字节的符号,第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一概设为10。剩下的没有说起的二进制位,所有为这个符号的 code point。
经过上面这两个规则,UTF-8 就不存在字节顺序在大小端不一样的状况,因此用 UTF-8 编码的文件在任何计算机中保存的字节流都是一致的,这是其很重要一优点;UTF-8 的另外一大优点在于对 ASCII 字符超节省空间,存储扩展拉丁字符与 UTF-16 的状况同样,存储汉字字符比 UTF-32 更优。
UTF-8 的一劣势是查找第 N 个字符时须要O(N)
的时间,也就是说,字符串越长,就须要更长的时间来查找其中的每一个字符。其次是在对字节流解码、字符编码时,须要遵循上面两条规则,比 UTF-1六、UTF-32 略麻烦。
随着互联网的兴起,UTF-8 是逐渐成为使用范围最广的编码方案。
咱们在互联网上查找编码相关资料时,常常会看到UCS-2
、UCS-4
编码,它们和UTF-*
编码家族是什么关系呢?要想理清它们之间的关系,须要先弄清楚,什么是 UCS。
UCS 全称是 Universal Coded Character Set,是由 ISO/IEC 10646定义的一套标准字符集,是不少字符编码的基础,UCS 中大概包含 100,000 个抽象字符,每个字符都有一惟一的数字编码,称为 code point。
在19世纪八十年代晚期,有两个组织同时在 UCS 的基础上开发一种与具体语言无关的统一的编码方案,这两个组织分别是 IEEE 与 Unicode Consortium,为了保持这两个组织间编码方案的兼容性,两个组织尝试着合做。早期的两字节编码方案叫作“Unicode”,后来更名为“UCS-2”,在研发过程发,发现 16 位根本不可以囊括全部字符,因而 IEEE 引入了新的编码方案——UCS-4 编码,这种编码每一个字符须要 4 个字节,这一行为马上被 Unicode Consortium 制止了,由于这种编码太浪费空间了,又由于一些设备厂商已经对 2 字节编码技术投入大量成本,因此在 1996 年 7 月发布的 Unicode 2.0 中提出了 UTF-16 来打破 UCS-2 与 UCS-4 之间的僵局,UTF-16 在 2000 年被 IEFE 组织制定为RFC 2781标准。
因而可知,UCS-*
编码是一历史产物,目前来讲,统一编码方案最终的赢家是 UTF-*
编码。
根据UTF-16 FOR PROCESSING,如今流行的三大操做系统 Windows、Mac、Linux 均采用 UTF-16 编码方案,上面连接也指出,现代编程语言像 Java、ECMAScript、.Net 平台上全部语言等在内部也都使用 UTF-16 来表示字符。
上图为 Mac 系统中文件浏览器 Finder 的界面,其中全部的字符,在内存中都是以 UTF-16 的编码方式存储的。
你也许会问,为何操做系统都这么偏心 UTF-16,Stack Exchange 上面有一个精彩的回答,感兴趣的能够去了解
为了适应多语言环境,Linux/Mac 系统经过 locale 来设置系统的语言环境,下面是我在 Mac 终端输入locale
获得的输出
LANG="en_US.UTF-8" <==主语言的环境 LC_COLLATE="en_US.UTF-8" <==字串的比较排序等 LC_CTYPE="en_US.UTF-8" <==语言符号及其分类 LC_MESSAGES="en_US.UTF-8" <==信息显示的内容,如功能表、错误信息等 LC_MONETARY="en_US.UTF-8" <==币值格式的显示等 LC_NUMERIC="en_US.UTF-8" <==数字系统的显示信息 LC_TIME="en_US.UTF-8" <==时间系统的显示资料 LC_ALL="en_US.UTF-8" <==语言环境的总体设定
locale 按照所涉及到的文化传统的各个方面分红12个大类,上面的输出只显示了其中的 6 类。为了设置方便,咱们能够经过设置LC_ALL
、LANG
来改变这 12 个分类熟悉。其优先级关系为
LC_ALL
>LC_*
>LANG
设置好 locale,操做系统在进行文本字节流解析时,若是没有明确制定其编码,就用 locale 设定的编码方案,固然如今的操做系统都比较聪明,在用默认编码方案解码不成功时,会尝试其余编码,如今比较成熟的编码测探技术有Mozila 的 UniversalCharsetDetection 与 ICU 的 Character Set Detection 。
通常来讲,高级编程语言都提供都对字符的支持,像 Java 中的 Character 类就采用 UTF-16 编码方案。
这里有个文字游戏,通常咱们说“某某字符串是XX编码”,其实这是不合理的,由于字符串压根就没有编码这一说法,只有字符才有,字符串只是字符的一串序列而已。
不过咱们平时并无这么严谨,不过你要明白,当咱们说“某某字符串是XX编码”时,知道这其实指的是该字符串中字符的编码就能够了。
这也就回答了本文一开始提到问题,什么是字符串,这里用《Diving into Python 3》书上的一句话来总结下:
Bytes are not character, bytes are bytes. Characters are an abstraction. A string is a sequence of those abstraction.
咱们能够作个简单的实验来验证 Java 中确实使用 UTF-16 编码来存储字符:
public class EncodingTest { public static void main(String[] args) { String s = "中国人a"; try { //线程睡眠,不要让线程退出 Thread.sleep(10000000); } catch (InterruptedException e) { e.printStackTrace(); } } }
在使用 javac 编译这个类时,javac 会按照操做系统默认的编码去解析字节流,若是你保存的源文件编码与操做系统默认不一致,是可能出错的,能够在启动 javac 命令时,附加-encoding <encoding>
选项来指明源代码文件所使用的编码。
# 编译生成 .class 文件 javac -encoding utf-8 EncodingTest.java # 执行该类 java EncodingTest # 使用 jps 查看其 pid,而后用 jmap 把程序运行时内存的内容 dump 下来 jmap -dump:live,format=b,file=encoding_test.bin <pid> # 在 Linux/Mac 系统上,使用 xxd 命令以十六进制查看该文件,我这里用管道传给了 vim xxd encoding_test.bin | vim -
在 vim 中能够看到下图所示片断
其中我用红框标注部分就是上面 EncodingTest 类中字符串s
的内容,4e2d
是“中”的 code point,56fd
是“国”的 code point, 4eba
是“人”的 code point,0061
是“a”的 code point。而在 UTF-16 编码中,0-66535之间的字符直接用两个字节存储,这也就证实了 Java 中的 Character
是使用 UTF-16 编码的。
首先说下 Python 解释器如何解析 Python 源程序。
在 Python 2 中,Python 解析器默认用 ASCII 编码来读取源程序,当程序中包含非 ASCII 字符时,解释器会报错,下面实验在我 Mac 上用 python 2.7.6 进行:
$ cat str.py #!/usr/bin/env python a = "众人过" $ python str.py File "str.py", line 2 SyntaxError: Non-ASCII character '\xe4' in file str.py on line 2, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details
咱们能够经过在源程序起始处用coding: name
或coding=name
来声明源程序所用的编码。
Python 3 中改变了这一行为,解析器默认采用 UTF-8 解析源程序。
按理接下来应该介绍 Python 中对字符的处理了,可是发现这里面东西太多了,介于本文篇幅缘由,这里再也不介绍,后面有空能够单独写篇文件介绍。你们感兴趣的能够参考下面的文章:
在咱们的 Web 浏览器接收到来自世界各地的 HTML/XML 时,也须要正确的编码方案才可以正常显示网页,在现代的 HTML5 页面,通常经过下面的代码指定
<meta charset="UTF-8">
4.0.1 以前的 HTML 页面使用下面的代码
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
XML 使用属性标注其编码
<?xml version="1.0" encoding="UTF-8" ?>
仔细想一想,这里有个矛盾的地方,由于咱们须要事先知道某字节流的编码才能正确解析该字节流,而这个字节流的编码是保存在这段字节流中的,和“鸡生蛋,蛋生鸡”的问题有点像,其实这并非一个问题,由于大部分的编码都是兼容 ASCII 编码的,而这些 HTML/XML 开始处基本都是 ASCII 字符,因此采用浏览器默认的编码方案便可解析出该字节流所声明的编码,在解析出该字节流所用编码后,使用该编码从新解析该字节流。
这篇文章我用了周末 2 天时间才完成,在 wiki 上搜索的资料时,须要消耗较大精力去整理,由于各个编码都不是孤立存在的,要想完整了解某编码,须要从起发展历史开始,在不一样编码条目间来回切换,才能对其有深刻理解。这是我意料以外的。
字符串,这个既熟悉又陌生的东西,相信你们在看本文后,你们都可以对其有一全新的认识。