9.4 什么是字符
字符串是什么?本节我们会追溯到计算机诞生以前,了解何为字符,并学习字符集和字符的编码方式。字符串一词所指的内容在不同语言中差异很大。
字符集和字符的编码方式
因国家和文化不同,提到字符,大家关联到的事物可能差异很大。在英国,人们可能会关联到 abc 等字母。在法国,人们除了会关联到字母,可能还会想到 œ、ç 这样的特殊字符。
韩国人可能把 Hangul(韩文字母)当作字符,而日本人则会把平假名、片假名和汉字当作字符。那◇是字符吗? J 是字符吗?┐又是字符吗?
说到底,字符只不过是人们约定好的命名为字符的一系列符号的集合而已。这一符号的集合被称为字符集或字符包。字符集因国家和文化不同千差万别。
另一方面,要将字符集通过数字化的数据表现出来,就必须考虑如何对字符进行编码。编码方式也只不过是人们约定的一系列规则,有些甚至可能是随意决定的。比如,平假名中的た用 + = 22 表示,这一规则就是随意决定的。编码方式必须为字符编码方和编码解码方共有,和没有掌握这一编码方式的人是无法进行信息交互的。
222002 年左右在年轻女性中流行的“少女字符”中就用 + =来表示た。仔细看来,它和平假名中的た确实有些形似。
字符编码方式的发展历程,是两种观点相互角力的历史。一种观点认为应该按效率和满足个别需求的原则创造新的编码方式,与此针锋相对的观点认为过多的编码方式为信息交互带来不便,应该进行标准化。接下来我们回顾一下这段历史。
计算机诞生以前的编码
摩斯码
在电影等一些场合,我们经常会见到摩斯码 23。它把用短·长·长·短·短·短·长·短·长·短 24 来表示 ABC,通过控制与无线发报机相连的按键的“通”与“断”信号进行通信(图 9.13)。
23摩斯符号(摩斯信号)这种编码方式发明于 1836 年左右,于 1865 年由 International Telegraphy Congress 采纳为一种国际标准。
241836 年摩斯思考出的符号和在 1865 年标准化的符号在 C 和 F 等部分字符和数字的编码方式上有差别。

图 9.13 几个字符的摩斯码表达
摩斯码用短时间接通的短点和是其 3 倍时长的接通的长点的组合来表达字符。短点和长点之间要夹带断开状态,否则点与点之间就没有了界限,会变得无法区分和理解。点之间的断开状态时长是 1 个短点,字符间是 3 个短点,词语间则是 7 个短点。
按照这种方式来编码求救信号 SOS 时,字符串会有多长呢? S 是短·短·短有 5 个短点时长,O 是长·长·长,有 11 个短点时长,再加上字符间的短点时长,总共有 5+3+11+3+5=27 个 25。
25在实际的电信通讯中,会将 SOS 作为特殊信息进行处理,规定在 S 和 O 之间仅保留一个短点时长。在与其他种编码方式进行效率比较时,暂不考虑电信中的特殊情况。
能用摩斯码编码的字符集有字母、数字和其他一些符号,其中字母不区分大小写。估计当时人们认为为区分大小写而增加字符的话成本较高,权衡之后决定不区分了。
博多码
摩斯码是基于手动按压开关送信然后通过耳朵接收的设想而设计出来的一种编码方式。使用这种方式,1 秒钟能送出的量和能接收的量都很有限。随着通信需求的不断提高,迫切需要一种更为高速的通信方法。
于是后来出现了电传打字机终端。把打字机与电话线相连,通过敲击键盘输入字符,接收端通过打印机输出接收到的字符。后来,针对大量信息交互的更为高速的通信需求,出现了纸带穿孔机和读取设备相连接的方式。人们把需要传送的信息用穿孔纸带上的孔点记述下来,设备读取这些孔点后再把信息传送出去。
电传打字机终端连接的国际通信网络叫电传网,于 1931 年首次提供服务。电传网使用的编码方式是博多码,它在 1905 年被提出,在 1931 年成为了一种标准规格。
博多码的特征是,一个字符由 5 个通与断(5 比特)的组合来表现(图 9.14)。也就是说,SOS 只需要 15 个比特就能表示了。这大约是摩斯码的一半,似乎是一种效率更高的通信手段。博多码的字符集由大写字母、数字和一些其他符号组成。突出特点是它在字符集中添加了空格(不打印输出只是将光标向右移动一个字符位置的命令)、换行(光标向下一行移动的命令)、归位(光标向行首移动的命令)等控制码。

图 9.14 几个字符的博多码表达
然而,5 个比特只能表达最多 32 个字符。字母 26 个,数字 10 个,加起来就已经是 36 个了。那么如何实现 5 个比特表达 1 个字符的目标呢?
答案是使用切换。事先定义好此处是由数字模式的比特列(FIGS)起始还是字母模式的比特列(LTRS)起始,再实现两种模式的切换。这种切换叫做 shift。比如 10011 这一比特列,在字母模式下意思是 W,在数字模式下意思变成 2.
EDSAC 的字符编码
我们终于要开始展开与计算机有关的话题了。电传网出现大约 20 年后,1949 年,一种叫做 EDSAC 的计算机问世了。EDSAC 和电传网一样,用 5 个比特来表达 1 个字符,并且也用一排开有 5 个孔点的纸带进行输入。这种计算机应该比较容易生产。然后,将数字与字母通过 shift 切换输出。这一点和博多码的理念是一致的。但是,哪个字符分配到哪个比特列的分配方式和博多码是不一样的。
ASCII 时代和 EBCDIC 时代
EDSAC 出现后的十年间出现了各式各样的计算机,它们使用的字符的编码方式也各不相同。这样一来,在一种计算机上读取其他种计算 机输出的字符时,就需要一个个的转换,十分麻烦。为了更轻松地实现字符间的转换,人们展开了将计算机字符编码方式标准化的运动。这就是 ASCII 码,它是 American Standard Code for Information Interchange 的缩写,也就是信息交互的美国标准符号,于 1963 年被制定出来。
ASCII 中,1 个字符用 7 个比特进行编码。7 个比特可以表达 128 个字符,因此不再需要切换。ASCII 码的字符集比 EDSAC 大很多,包括大量的符号、控制码以及小写字母。
如果计算机的生产商都统一使用 ASCII 码,那么信息的交互就能变得很轻松。这应该就是当初的理想,然而现实与理想是有差距的。在 ASCII 码制定的 1963 年,当时在计算机制造市场拥有大多数份额的 IBM 公司公布了与 ASCII 码不同的 8 比特编码方式。这就是 EBCDIC26(图 9.15)。
26EBDIC 是 Extended Binary Coded Decimal Interchange Code 的简称,和这里的主题关系不大。字符集和 ASCII 码不完全一致,但就能显示的字符来说基本是相同的,这里做简单处理。

图 9.15 ASCII 码和 EBCDIC 码中大小写字母和符号的编码方式
如果大家都使用同一种方法当然是更好的,但大公司往往主张大家去使用它们自家产品的方法。到现在之所以也有很多用户在使用这家公司的产品的执行方法,其中一个理由就是因为迁移成本太高。而使用其竞争厂商的产品的用户往往十分艰难,这加剧了拥有多数市场份额的公司进一步获取更多用户 27。
27这个不仅限于发生在 IBM 身上,在操作系统市场巨头的微软公司和智能手机巨头的苹果公司身上也发生着类似的事情。
最后,统一编码方式的理想未能实现,EBCDIC 自身也出现了很多种亚种。比如,比特列 01011010 对应的字符,在 HP 的 EBCDIC 中是 ],在 IBM 的 EBCDIC 中是!,而在 ASCII 中则是 Z28。
28谨慎起见,这里也把公司全名写出来。IBM 是 International Business Machine Corporation 的略称,HP 是 Hewlett-Packard Company(惠普公司)的略称。两者都是著名的计算机生产厂商。时至 2012 年,即使使用略称相信大家也能明白。笔者于上世纪 80 年代写的一篇文章中用了“世界上首次量产计算机的 RR 公司”,结果有不少读者没有认出来,这还曾让我烦恼过。正确答案是 Reminton Rand。它在 1955 年合并后改成了别的名称,之后又再次发生合并,演变成了今天的 Unisys Corporation。
日语的编码
计算机的使用进一步普及,逐渐延伸到企业的业务领域,于是催生了新的需求。这不仅是 ASCII 中能表达的字母、数字和符号,也有对日语字符表达的需求。然而,一个字节(8 比特)最多只能表达 256 种符号,于是开始了使用多个字节表达日语中的字符。
这种编码方式也有很多种类型。这里介绍三种现在也频繁出现的类型:ISO-2022-JP29、Shift_JIS30 和 EUC-JP31,32。
29也俗称 JIS 码等。
30“Windows 中使用的是 JIS”的这种说法并不正确。严格来讲,是微软公司对 Shift_JIS 进行了扩展,在 Windows 中使用了并不具有完全互换性的 Windows-31J(或称为 CP932)。
31EUC 是 Extended UNIX Code 的略称。除 EUC-JP 外,还有针对韩语的 EUC-KR 等。
32本章一直使用“编码方式”这一用词。编码方式= encode,用 en+code 来表示“使之成为编码”的意思。也有称 Shift_JIS 等编码方式为字符码的叫法。
图 9.16 展现的是将 aaa あああ aaa 用各种编码方式转换为字节序列的情形

图 9.16 三种编码方式中 aaa あああ aaa 的编码
ISO-2022-JP
在 ISO-2022-JP 编码方式中,aaa 和あああ之间有不与任何字符对应的 3 个字节,在图中用灰色显示。这是在博多码和 EDSAC 中使用的切换命令,用来切换显示字母的模式和显示平假名的模式。
在字母和符号用 1 个字节表示的模式中,24 是 $,22 是 " 。前面添上切换命令(1b 24 42,以下代码中是 \x1b$B)之后,进入平假名表达模式,24 22 作为整体,被识别为あ。
Python 2.7
>>> print '$"$"$"'.decode('iso-2022-jp')
$"$"$"
>>> print '\x1b$B$"$"$"'.decode('iso-2022-jp')
あああ
Shift_JIS
ISO-2022-JP 中单字节的字符 $ 和 双字节的字符あ的前面一个字节都是 24。Shift_JIS 编码约定,用于表现单字节的字符的字节不会被用在双字节的字符的前面一个字节上。比如,あ前面一个字节是 82,它没有用于任何一个单字节的字符。于是,即使没有切换命令也可以进行这样的判断:这个字符不是用来表达单字节字符的,而是双字节字符前面的一个字符。
EUC-JP
EUC-JP 编码约定,双字节字符中的第一个字节和第二个字节都不出现在单字节字符的表达中。
Shift_JIS 编码对程序的破坏
Shift_JIS 编码和 EUC-JP 编码的区别在于,双字节字符的第二个字节是否回避单字节字符的表达。这个区别对于程序员来讲是个很大的问题。这是因为,在 Shift_JIS 编码中,双字节字符的第二个字节有可能是程序中有特殊意义的字符。
比如,ドレミファソラシド(哆来咪发唆拉西哆)中的ソ(唆)、表示中的表、申し込む中的申的第二个字节和反斜杠(\)是同一个字节 33。正因为如此,将这些字符使用在源代码中将不会得到期望的输出。
33此外,关于这个问题还可能发生在哪些字符上,请参照维基百科中 Shift_JIS 的“第二字节可能是 5C 等时的问题”部分。http://ja.wikipedia.org/wiki/Shift_JIS。
Perl
print("ドレミファソラシド\n");
print("表示\n");
print("申し込む\n");
出力
输出ドレミファャ宴Vド
侮ヲ
垂オ込む
或者将这些字符使用在字符串的最后时会导致错误发生。
print("图表");
错误输出
Can't find string terminator '"' anywhere before EOF at sjis2.pl line 1.
以上两个例子是在 Perl 程序中进行说明的。这些是在 Windows 中编写 Perl 程序时经常遇见的问题。但这一现象不是 Perl 程序中特有的。比如下面的 C++ 程序,注释符后一行代码无法执行。这是因为,注释的最后一个字符“能”字的第二个字节和反斜杠相同,因为有这个反斜杠,换行符就略过了,于是把后一行也包含在注释的内容里面了。
C++
#include <stdio.h>
int main(){
printf("1\n");
// 不好的注释:某某功能
printf("2\n");
printf("3\n");
}
输出:注意 2 没有输出来
1
3
要解决这个问题,有人认为可以在不能正常显示的字符后面补上反斜杠的方法。也有人提议不要使用 Shift_JIS,使用 EUC-JP 或者 UTF-8 就行了。然而,这个问题原本是因为语言处理器不知道源代码是用何种编码方式编码而引起的。它不知道源代码是用 Shift_JIS 编码编写的,因此误把日语字符的第二个字节当作了 ASCII 码中的反斜杠。
魔术注释符
为了能让语言处理器正确地处理包含多字节字符的源代码,就需要告诉它源代码的编码方式。其中一个方法就是使用魔术注释符。魔术注释符最早是编辑器的一个功能。在 Emacs 和 Vim 等文本编辑器中,用特殊的记号事先写明文件的编码方式,编辑器要打开这一文件时就会以这一编码方式读取文件。
Emacs
# -- coding: shift_jis --
Vim
# vim: set fileencoding=shift_jis :
语言处理器如果按这种方式去读,就能知道源代码中字符的编码了,这样一来问题就可以得到解决了。这一提案在 2001 年作为 Python 语言的扩展方案被公布出来 34。现在 Ruby 语言、Perl 语言和 Scheme 语言的处理器 Gauche 等都采用了这一方案。
34PEP 0263-Defining Python Source Code Encodings,http://www.python.org/dev/peps/pep-0263.
Python 语言进一步采取了更为激进的设计方法。源代码中只要是使用了 ASCII 码以外的字符,但没有使用魔术注释符时,都将导致语法错误。比如,源代码中用日语写了注释,就会带来以下错误。
Python语言中的错误消息
SyntaxError: Non-ASCII character '\xe6' in file tmp.py on line 1, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details
这段错误消息表示在文件中发现了非 ASCII 码的字符,但没有声明编码方式。
正因为此种设计,我们在编写程序时,要注意不要忘记向语言处理器声明编码方式,否则会出现一些问题。与此同时,也有用户对 Python 语言就因为仅有少量日语注释而可能导致错误这一特点表示不满。这的确是一个令人头痛的问题。
Unicode 带来了统一
至此,我们学习了表现日语字符的三种主要编码方式、它们之间的差异以及由此带来的问题。
这里我们再来回顾一下字符集和编码方式的历史。我们说过在日本国内曾经有多种字符的编码方式,听起来有些麻烦。然而把目光投向全球,这个问题就变得更加严重了。各语言表达各自的字符时都具有自己独特的字符集。编码方式必然也因国家不同而各不相同。
随着互联网的发展,各国间的数据交互越来越频繁,这个问题也越来越突显。处理希伯莱语要用 ISO-8859.8 的编码方式,处理俄语要用 KOI8,处理中文繁体字要用 BIG5,诸如此类,各国的规则必须要一一记住。那有没有更简便的方法呢?
于是,我们开始了尝试设计一种能编码世界上所有字符的编码方式。1984 年国际标准化组织(ISO) 开 始 了 Universal Character Set(UCS)的标准化作业。另外,美国施乐公司(Xerox)35 于 1987 年左右开始进行同样的尝试,并于 1989 年公布了初稿 Unicode Draft 1。最终,ISO 制定的规则被否决,与 Unicode 融合之后于 1993 年成为国际标准 36。
35施乐公司主要生产激光打印机,于 1970 年成立了帕罗奥多研究中心(Palo Alto),对计算机发展起到了举足轻重的作用。图形用户接口(鼠标和画面上的按钮等)和以太网(Ethernet,在 LAN 中最为广泛使用的规格)都是在这个研究中心诞生的。
36Unicode 是在施乐公司初稿的基础上,由相关计算机企业联名组成的非营利机构 Unicode 联盟统一制定的。
就这样,一个包含世界上所有字符的字符集合——Unicode 诞生了(图 9.17)37。
37严格来讲,Unicode 和 ISO 制定的规格 ISO/IEC 10646(UCS: Universal Coded Character Set)不是同一种规格。其区别本书不做论述,这里不区别 UCS 和 Unicode,只进行简单说明。

图 9.17 涵盖各国字符的统一字符集
也许有读者听说过日语字符串编码方式 UTF-8。它就是一种用来编码 Unicode 这样统一之后的字符集的编码方式 38。
38字符集得到了统一,但为适应不同的需求,字符的编码方式还有很多种,如 UTF-7 和 UTF-16 就是其中两种。
在本节的开始我们问了这样一个问题:◇是字符吗,J 是字符吗,┐又是字符吗?
答案是肯定的。对于这些问题,谁都可以随意去决定。但是,在这种状态下,使用时会带来诸多不便。于是大家商议出了统一的规则,这就是 Unicode。在 Unicode 中,这三种都规定为算是字符。
