ASCII码和字符转换

数字计算机中的存储器唯一可以存储的是比特,因此如果要想在计算机上处理信息,就必须把它们按位存储。通过之前的学习,我们已经掌握了如何用比特来表示数字和机器码。现在我们面临的一大挑战是如何用它来存储文本。

字符编码集与字符编码

为了将文本表示数字形式,我们需要构建一种系统来为每一个字母赋予一个唯一的编码。数字和标点符号也算做文本的一种形式,所以它们也必须拥有自己的编码。简而言之,所有由符号所表示的字母和数字(Alphanumeric)都需要编码。具有这种功能的系统被称为字符编码集(Coded Character Set),系统内的每个独立编码称为字符编码(Character Codes)。

确定编码的比特数

首先,我们要确定构成这些编码究竟需要多少比特。

极力阐述一个重要的观点,那就是文本与其印刷在纸上时采用的二维码排版格式是两码事。文本可以看成是一维的由字母,数字和标点符号组成的数据流。

莫尔斯码和布莱叶盲文能运用到计算机中吗?难上加难。

莫尔斯码是变量自适应长度(Variable-Width)编码,非常适合电报系统,但并不适用于计算机。另外也不区分大小写。

布莱叶盲文固定宽度的特点非常适合计算机使用。类似的拥有固定宽度的编码是莫里编码。

1874年由法国电报服务公司(French Telegraph Service)职员埃米尔∙波多(Emile Baudot)发明了可以打印的电报机,划时代的波多电传码也应运产生。这种编码十分“经济划算”,每一个文本字符都采用5位编码。他的编码1877年被服务公司采纳,后来由DonaldMurray修改并在1931年被CCITT,即现在的国际电联(ITU)标准化。该编码的正式名称是国际电报字母表NO.2或ITA-2,在美国通常称为波多印字电报制(Baudot),尽管更科学的叫法为莫里(Murray)编码。

随着20世纪的到来,Baudot被广泛应用于电传打字机(teletypewrite)。Baudot 电传打字机配备了一个输入键盘,这款键盘有些像打字机,但只有30个键和一个空格键。电传打字机键盘上的每一个键实际上都起到了转换器的作用,它负责产生二进制编码并且通过输出电缆逐位传输出去。电传打字机也具备打印功能,通过输入电缆读取编码,触发电磁铁,从而将字符打印在纸上。

由于Baudot对每个字符采用5位编码,整个系统由32个编码所组成,这些编码的十六进制取值范围从00h到1Fh。下表给出了32个不同编码的十六进制形式及其所对应的字母表中的字符。

十六进制码 Baudet字符 十六进制码 Baudet字符
00   10 E
01 T 11 Z
02 CarriageReturn(回车) 12 D
03 O 13 B
04 Space(空格) 14 S
05 H 15 Y
06 N 16 F
07 M 17 X
08 LineFeed(换行) 18 A
09 L 19 W
0A R 1A J
0B G 1B FigureShift(数字转义)
0C I 1C U
0D P 1D Q
0E C 1E K
0F V 1F LetterShift(字符转义)

编码00h被保留下来,没有指派给任何值。剩下的31个编码中,字母表中的字符占了26个,其余5个用来调整格式,如上表中的楷体排版的语句所示。

编码04h用来表示空格,通常用于分隔单词。编码02h和08h表示的是回车和换行。这些都是电传打字机中的专业术语。当使用电传打字机上打字,一旦到了一行的末尾时,我们通常会按下一个操作杆或按钮。这个操作其实包括两个动作:第一个动作是,使打印机的滑架回到起始位置,这样打印下一行时可以从纸的最左边开始,这就是回车。第二个动作是,将打印机的滑架移至正在使用中的位置的下一行,这就是换行。在Baudot编码系统中,这两个编码由专门的按键产生。Baudot电传打字机在打印的时候会响应这两个编码以完成相应的操作。

Baudot系统里怎么没有数字和标点符号呢?其实这是因为编码1Bh中暗藏玄机,它的实际作用是数字转义(Figure Shift)。数字转义编码后的所有的编码都会被解释为数字或标点符号,直到遇到字符转义编码(1Fh),一切就又被解释为字符。下表展示了十六进制编码以及所对应的数字和标点符号。

十六进制码 Baudot字符 十六进制码 Baudot字符
00   10 3
01 5 11 +
02 CarriageReturn 12 WhoAreYou?
03 9 13 ?
04 Space 14
05 # 15 6
06 16 $
07 17 /
08 LineFeed 18 -
09 ) 19 2
0A 4 1A Bel(响铃)
0B & 1B FigureShift
0C 8 1C 7
0D 0 1D 1
0E 1E (
0F = 1F LetterShift

表中列出的是这几个编码是美国的使用时的含义。响铃编码令电传打字机发出清脆的铃声。“Who Are You”编码用来让打字员激活身份识别机制。

像莫尔斯码一样,这种5位的编码并没有提供区分大,小写的方法。下面这个橘子:

I SPEND $25 TODAY.

表示成编码的十六进制数据流就是:

0C 04 14 0D 10 06 12 04 1B 16 19 01 1F 04 01 03 12 18 15 1B 07 02 08

请注意三个转义码的使用:1Bh出现在数字之前,1Fh出现在数字之后,而数字结束之后又出现了1Bh。这一行编码以回车,换行符结尾。

问题出来了,如果把相同的数据流再一次输入到电传打印机,情况就大不一样了。

如下所示:

I  SPENT  $25  TODAY.

8 ‘03,5  $25  TODAY.

这是由于在接受第二行编码之前打印机接受到的最后一个转义码是数字转义码,所以当遇到第二行开头几个编码时,打印机将它们解释为数字。

这种问题产生的根源就是采用了转义码。尽管Baudot电传码是简单实用的编码,但是我们更希望采用能唯一表示字符,数字及标点符号的编码方案,如果能区分大小写就更好了。

如果想直到比Baudot更好用的编码系统中一个编码需要多少比特,我们需要做几个小加法:所有的大小写字母加起来共需52隔编码,数字10个编码,加起来有62个,再加上标点符号,数量超过了64个,也就是说,一个编码至少需要64比特。但无论如何字符数应该不超过128个,也就是说编码长度不会超过7位。

所以,答案就是7.再采用7位编码时,不需要转义字符,而且可以区分字母的大小写。

ASCII

这些字符编码是什么样子的呢?其实我们可以随意编码。但是随意的编码并不太合适。我们需要所有人都遵循并使用统一化的编码,计算机的存在才有意义。这样以来,使用不同方法制造出的计算机之间就可以互相兼容,甚至可以互相交流文本信息。

幸运的是,我们已经有了这样一个标准,即美国信息交换标准代码(American Standard Code for Information Interchange),简写为ASCII码。它1967年正式公布,此后一直是计算机工业界最为重要的标准。除了一个大的例外(在后面讲到),可以肯定的是,无论什么时候处理文本,总会以某种方式涉及到ASCII码。

ASCII码是7位编码,它的二进制取值范围为0000000~1111111,对应于十六进制就是00h~7Fh。现在我们一起来讨论下ASCII码,但我们不建议从开始学起,因为相对于后面的编码,前32个编码理解起来还有一点难度。所以我们从第2组32个编码开始学习,它包括标点符号和10个数字。下面列出了这32个字符及相应的十六进制编码。

十六进制码 ASCII字符 十六进制码 ASCII字符
20 space 30 0
21 31 1
22 32 2
23 # 33 3
24 $ 34 4
25 % 35 5
26 & 36 6
27 37 7
28 ( 38 8
29 ) 39 9
2A * 3A :
2B + 3B ;
2C , 3C <
2D - D =
2E . 3E >
2F / 3F ?

值得注意的是20h代表空格符,它的作用是将单词或句子隔开。

接下来的32个编码是大写字母和一些附加的标点符号的编码。除了@符号和下画线之外,其余的符号很难在打字机上找到。

十六进制码 ASCII字符 十六进制码 ASCII字符
40 @ 50 P
41 A 51 Q
42 B 52 R
43 C 53 S
44 D 54 T
45 E 55 U
46 F 56 V
47 G 57 W
48 H 58 X
49 I 59 Y
4A J 5A Z
4B K 5B [
4C L 5C \
4D M 5D ]
4E N 5E ^
4F O 5F -

接下来的32个编码是所有小写字母和一些附加的标点符号及其对应的十六进制编码,这些字符也很在打字机上出现。

十六进制码 ASCII字符 十六进制码 ASCII字符  
60 ` 70 p  
61 a 71 q  
62 b 72 r  
63 c 73 s  
64 d 74 t  
65 e 75 u  
66 f 76 v  
67 g 77 w  
68 h 78 x  
69 i 79 y  
6A j 7A z  
6B k 7B {  
6C l 7C    
6D m 7D }  
6E n 7E ~  
6F o      

注意,表的最后不包括7Fh及其对应的字符。如果你统计一下,就会发现这三张表共涵盖了95个字符。由于ASCII码的编码长度为7位,所以最多可以表示128个编码,这样算下来还剩33个编码可用。下面我们通过几个简单的例子学习一下编码。

像这样一个字符串:

Hello, you!

转换成ASCII码,用十六进制表示如下:

48  65  6C  6C  6F  2C  20  79  6F  75  21

这段编码中,除了普通的字符,逗号(编码2C),空格(编码20)和感叹号(编码21)容易遗漏,需要额外注意。

我们再来看一个例子:

I am 12 years old.

它用ASCII码表示为:

49  20  61  6D  20  31  32  20  79  65  61  72  73  20  6F  6C  64  2E

有意思的是数字12的表示方法。在这段编码串中,它被表示成十六进制数31h和32h,也就是数字1和2的ASCII码的组合。当数字12以文本流的身份出现时,不应该用十六进制码01h和02h,或者BCD码12h,或者0Ch来表示。因为这些编码在ASCII码中表示其他的意思。

字符转换

在ASCII码中,一个大写字母与其对应应的小写字母的ASCII码值相差20h。这种规律大大简化了程序代码的编写,例如一段将特定的字符串变成大写的程序。假设有一个字符串存放在内存的某个区域,每个字符占据一个字节。下面是一段8080子程序,初始状态下字符串的首地址存放在寄存器HL中;寄存器C存放字符串的长度,也就是字符的个数。

Capitalize: MOV A, C    ; c表示剩余的字符数
            CPI A, 00h  ; 与0进行比较
            JZ AllDone  ; 如果剩余的字符数为0,程序结束
            MOV A, [HL] ; 取得下一个字符
            CPI A, 61h  ; 判断A代表的字符的ASCII码是否比`a'小
            JC SkipIt   ; 如果比`a'小,就跳过
            CPI A, 78h  ; 判断是否比`z'大
            JNC SkipIt  ; 如果是,则跳过
            SBI A, 20h  ; 判断是否小写,如果是,则减20h
            MOV [HL], A ; 保存修改过的字符
SkipIt:     INX HL      ; 指向下一个字符
            DCR C       ; 计数器减一
            JMP Capitalize; 返回到程序起始处
AllDone:    RET

还有另外一种方法也可以将小写字母减去20h而转换成大写字母,如下所示:

ANI A, DFh
``

ANI指令(AND Immediate)用来"与"一个立即数。在上面这个例子中,累加器的数值与DFh执行“按位与”操作,其中DFh转换成二进制数就是11011111.“按位与”操作就是把两个数分别转换成二进制,然后将对应的位进行“与”操作。这个例子中,除了自左向右数的第3位被置成0外,A中的其他位均被保留。通过将这一位设置为0,我们实现了将小写字母的ASCII码转换成大写字母的目的。

### ASCII中33个控制字符

| 十六进制码 | 缩写词 | 控制字符名称        |
| ---------- | ------ | ------------        |
| 00         | NUL    | 空                  |
| 01         | SOH    | 标题开始            |
| 02         | STX    | 文本开始            |
| 03         | ETX    | 文本结束            |
| 04         | EOT    | 传输结束            |
| 05         | ENQ    | 询问                |
| 06         | ACK    | 应答                |
| 07         | BEL    | 响铃                |
| 08         | BS     | 退格                |
| 09         | HT     | 水平制表            |
| 0A         | LF     | 换行                |
| 0B         | VT     | 垂直制表            |
| 0C         | FF     | 换页                |
| 0D         | CR     | 回车                |
| 0E         | SO     | 移出                |
| 10         | DLE    | 转义                |
| 11         | DC1    | 设备控制1           |
| 12         | DC2    | 设备控制2           |
| 13         | DC3    | 设备控制3           |
| 14         | DC4    | 设备控制4           |
| 15         | NAK    | 否定应答            |
| 16         | SYN    | 同步                |
| 17         | ETB    | 传输块结束          |
| 18         | CAN    | 作废                |
| 19         | EM     | 载体结束            |
| 1B         | ESC    | 扩展                |
| 1C         | FS     | 文件分隔或信息分隔4 |
| 1D         | GS     | 组分隔或信息分隔3   |
| 1E         | RS     | 记录分隔或信息分隔2 |
| 1F         | US     | 单元分隔或信息分隔1 |
| 7F         | DEL    | 删除                |

前面讲到的95个编码也被称为图形文字(graphic characters),因为它们可以被显示出来。其实ASCII码还包含33个控制字符(control characters),它们用来执行某一特定功能,因而不用显示出来。为了完整地讨论ASCII编码,下面将这33个控制字符也列举了出来,有一些的确很难理解,不过不用在意。很难理解的编码只用在电传打字机上,而如今也渐渐离开了人们的视线。

人们最初的想法是可以在图形字符中使用控制字符,以便对文本格式进行基本的调整。

我们看看下面这个十六进制字符串:

41 09 42 09 43 09
```

编码09代表水平制表符,简写为Tab。假设打印的过程中,所有水平排列字符的起始位置都为0,Tab的作用是在下一个水平位置即在距前一个字符的间距为字符长度8倍的位置打印下一个字符,如下所示:

A      B      C

这种简单有效方法使得字符可以保持按列对齐。

有一些控制字符甚至沿用至今,例如换页符(12h),它使得打印机跳出当前页,并开始准备打印下一页。

回退符可以用来打印复合字符,尤其是一些旧的打印机上。假设计算机要控制电传打字机,使其不仅打印小写字母e,还要将其重音标记出来,即è。我们可以用会退符来实现,十六进制码为:65 08 60.

计算机中的回车和换行与Baudot码中表示的意思相同,它们可以算得上是控制字符中最重要的两个字符。在打印机中,回车符使得打印头换行并转移至当前页面的最左端,换行符使打印头转移至当前位置下一行。这两种操作都使得打印头移至新的一行。回车符通常用来另起一行继续打印,换行符通常在不需要移到页面最左端而换行时使用。

EBCDIC

尽管ASCII码在计算机领域可谓是一统江湖,但许多IBM大型机上却没有采用这种标准。例如,System /360 产品北部采用的是IBM自发研制的8位字符编码系统,也被称为扩展的BCD交换码(Extended BCD Interchange Code),或EBCDIC(英文中发音为EBB-see-dick)。EBCDIC是早期的6位BCDIC编码的扩展形式,BCDIC的起源于IBM的打孔机。一张打孔机——存储容量为80个文本字符——1928年由IBM受创并沿用了将近50年,它的外观如下图所示。

在考虑打孔卡与8位EBCDIC字符码的关系时,需要知道,在几代技术的影响下,这种编码也历经几十年的演变。因此,打孔卡与EBCDIC之间的逻辑性和一致性也逐渐消失了。

打孔卡上每一列穿出的一个或多个矩形孔代表一个字符,而这些字符一般也打印在卡片的顶部。最下面的10行由数值标示,自上向下分别为第0行,第1行直到第9行。第0行上面的一行通常不出现数字,称为第11行,顶端为第12行,这里没有第10行。

以下面列举一些IBM打孔卡的常用术语:第0~9行称为数字行(digit rows)或数字穿孔(digit punches),第11行和12行被称作区域行(zone rows)或区域穿孔(zone punches)。由于不统一,IBM打孔卡用起来有时会有些混乱:比如有的卡片把第0行和第9行看做是区域行而不是数字行。

一个EBCDIC字符码由8位比特组成,进一步可以细分为高半字节(4比特)与低半字节。低半字节是BCD码,与字符的数字穿孔保持一致,高半字节与区域穿孔的编码保持一致(而且与区域穿孔一一对应)。回忆一下第19章的BCD编码原理,其本质是采用二进制数对十进制数进行编码(binary-coded decimal)——其中数字0~9都利用不同的4位二进制数进行编码。

数字0~9并不需要区域穿孔进行额外表示,它们的EBCDIC编码的高半字节是1111,代表了区域穿孔不起作用,而0~9的EBCDIC编码的半字节是数字穿孔的BCD码,如下所示。

十六进制码 EBCDIC字符
F0 O
F1 1
F2 2
F3 3
F4 4
F5 5
F6 6
F7 7
F8 8
F9 9

大写字母有一些有趣的规律。如果区域穿孔只出现在第12行,则高半字节标识为1100;如果只出现在第11行,则高半字节标识为1101;如果出现在第0行,则高半字节标识为1110.下表给出了大写字母及其对应的EBCDIC编码。

十六进制码 EBCDIC字符 十六进制码 EBCDIC字符 十六进制码 EBCDIC字符
C1 A D1 J    
C2 B D2 K E2 S
C3 C D3 L E3 T
C4 D D4 M E4 U
C5 E D5 N E5 V
C6 F D6 O E6 W
C7 G D7 P E7 X
C8 H D8 Q E8 Y
C9 I D9 R E9 Z

值得注意的是R与S之间编号有跳变。有时在编写程序的时候,尤其是程序中用到EBCDIC编码时,这个容易被忽视的小细节往往会令人抓狂。

小写与大写字母的数字穿孔是相同的,但它们的区域穿孔不同。在a~i的小写字母,穿孔位于第12行和第0行,高半字节对应的编码为1000;在j~r的小写字母,穿孔位于第12行和第11行,高半字节对应的编码为1001;在s~z的小写字母,穿孔位于第11行和第0行,高半字节对应的编码为1010.小写字母在EBCDIC字符及其对应的十六进制编码如下表所示。

十六进制码 EBCDIC字符 十六进制码 EBCDIC字符 十六进制码 EBCDIC字符
81 a 91 j    
82 b 92 k A2 s
83 c 93 l A3 t
84 d 94 m A4 u
85 e 95 n A5 v
86 f 96 o A6 w
87 g 97 p A7 x
88 h 98 q A8 y
89 i 99 r A9 z

当然,标点符号和控制符号也有自己的EBCDIC编码,但对于这些字符的编码系统没有必要去深究。

仔细观察IBM打孔卡,其中每一列细细数下共有12个孔,每个孔代表1位,也就是说可以提供12位的编码信息。我们其实可以用打孔卡上每一列12孔中的7个来表示ASCII码。但是,这种方案有一个非技术方面的缺陷,那就是太多的穿孔将使得卡片变得很脆弱,容易折断。

采用8位编码的EBCDIC中其实还有很多编码未定义,这也说明当年ASCII码采用了7位编码也是合乎情理的。在ASCII码刚刚问世的那个年代,存储器的价格贵的令人咋舌,有一些贵点认为ASCII码可以用6位编码并配合转义字符来使用,这样既区分大小写又节约了存储器。这种方案并没有被采纳,当时还有一些人认为ASCII码应采用8位编码,他们对计算机的体系结构有了大胆的推测,即计算机应该按字节存储,7位存储是不合适的。今天来看,8位的字节存储已经作为了一项标准。尽管ASCII码从技术的本质上来看是7位编码,但仍以8位的形式存储。

ASCII的发展与Unicode

`
在字节与字符之间建立一种等价关系大大简化了我们的工作,举例来说,如果要粗略估计一个文本文件所需要的存储空间,只要统计字符数就可以了。一个字符为一个字节。

尽管ASCII码是计算机领域最重要的标准,但它并不是十全十美。它的问题就蕴含在它的全称中——American Standard Code for Information Interchange,它是太美国化了!没有英国的英镑符号,西欧国家语言中的重音符号。简单的7位编码在面对数以万计的中国,日本,韩国的象形文字也显得力不从心。

在ASCII码的发展历程中,尽管没有在引入非拉丁字母方面做过工作,但开发者也一直在积极思考与改进编码系统,使其适应于其他国家。根据公布的ASCII码标准,有10个ASCII码保留位(40h,5Bh,5Ch,5Dh,5Eh,60h,7Bh,7Ch,7Dh和7Eh)可被重新定义,这样就便于特定国家的使用。另外,英镑符号(£)可以在需要时替换特殊符号(#),通用货币符号(¤)可以在需要时替换美元符号($)。当然为使得这一替换过程不发生混淆,如果在文本文件中使用了这些重定义得符号,相关人员都必须知道这些变化。

由于许多计算机系统按8位来存储字符,则可以设计扩展的ASCII码字符集来包含256个字符而不仅仅是128个。在这样的字符集里,代码00h~7Fh定义成与ASCII码一致;代码80h~FFh可定义成表示另外的字符。这种技术已被用来定义附加的字符代码,包含重音字母及非拉丁字母。作为例子,这里有一个96个字符的ASCII码的扩展,称之为第1号拉丁字母表,定义的字符编码从A0h~FFh。

在该表里,十六进制字符编码的高半字节由最高行给出,低半字节由左边列给出:

代码A0h对应的字符为非断开空格。通常计算机处理格式文本是按照行和段来编排的,每一行以空格符号断开,对应的ASCII码为20h。代码A0h用来显示一个空格,但不能用来断开一行。非断开空格可以用在如“WWII”这样的文本中。代码ADh定义成软连字符,该连字符用来分开一个字中间的音节,且只在需要连接被两行分开的一个单词时才使用。

遗憾的是,近几十年来出现了许多不同的ASCII码的扩展,导致了混淆和不兼容性。ASCII码通过扩展甚至可以编码中文、日文和韩文的笔画。有一个流行的编码叫作Shift-JIS(Japanese Industrial Standard,日本工业标准),其代码81h~9Fh用来表示2字节字符编码的起始字节。以这种方法,Shift-JIS可编码约6000个额外字符。遗憾的是,Shift-JIS不是使用这种技术的唯一系统。在亚洲,还有三个很流行的双字节字符集系统(double-byte character sets, DBCS)

双字节字符的缺有很多版本,但兼容性并不是它最主要的问题。它的另一个缺陷是,一些字符,特别是通用的ASCII码字符,是用单个字节编码表示的,相比而言,成千上万的象形文字则是双字节编码,这在无形之中增加了是用这种字符集的难度。

业界一直有一个目标,那就是建立一个独一无二的字符编码系统,它可以用于世界上所有语言文字,从1988年开始,几个著名计算机公司合作研究出一种用来替代ASCII码的编码系统,取名为Unicode(统一化字符编码标准)。相对于ASCII的7位编码,Unicode采用了16位编码,每一个字符需要2个字节。也就是说Unicode的字符编码范围为0000h~FFFFh,总共可以表示65, 536个不同字符。全世界所有的人类语言,尤其是经常出现在计算机通信过程中的语言,都可以使用同一个编码系统,而且这种系统还具有很高的扩展性。

Unicode编码其实并不是从零开始设计的,可以说Unicode做出了有效地改进,但这也不能确保它被全世界广泛采纳。ASCII码,包括数不清的有一点小缺陷的扩展ASCII码已经在计算机领域根深蒂固,想一下子就取代他们并不是轻而易举的。

对于Unicode来说,它唯一的问题,就是它改变了字符与存储空间之间“单字符,单字节”的等价对应关系。采用ASCII编码方式存储的著名《怒火之花》,其所占据的存储空间约为1MB。而如果采用Unicode编码,约占2MB。为了使编码洗头膏兼容,Unicode在存储空间上付出了相应的代价。

Loading Disqus comments...
Table of Contents