Google
 

浅谈文字编码和Unicode(下)

3 字符编码模型

程序员经常会面对复杂的问题,而降低复杂性的最简单的方法就是分而治之。Peter Constable在他的文章"Character set encoding basics Understanding character set encodings and legacy encodings"中描述了字符编码的四层模型。我觉得这种说法确实可以更清晰地展现字符编码中发生的事情,所以在这里也介绍一下。

3.1 字符的范围(Abstract character repertoire)

设计字符编码的第一层就是确定字符的范围,即要支持哪些字符。有些编码方案的字符范围是固定的,例如ASCII、ISO 8859 系列。有些编码方案的字符范围是开放的,例如Unicode的字符范围就是世界上所有的字符。

3.2 用数字表示字符(Coded character set)

设计字符编码的第二层是将字符和数字对应起来。可以将这个层次理解成数学家(即从数学角度)看到的字符编码。数学家看到的字符编码是一个正整数。例如在Unicode中:汉字“字”对应的数字是23383。汉字“”对应的数字是134192。

在写html文件时,可以通过输入"字"来插入字符“字”。不过在设计字符编码时,我们还是习惯用16进制表示数字。即将23383写成0x5BD7,将134192写成0x20C30。

3.3 用基本数据类型表示字符(Character encoding form)

设计字符编码的第三层是用编程语言中的基本数据类型来表示字符。可以将这个层次理解成程序员看到的字符编码。在Unicode中,我们有很多方式将数字23383表示成程序中的数据,包括:UTF-8、UTF-16、UTF-32。UTF是“UCS Transformation Format”的缩写,可以翻译成Unicode字符集转换格式,即怎样将Unicode定义的数字转换成程序数据。例如,“汉字”对应的数字是0x6c49和0x5b57,而编码的程序数据是:

	BYTE data_utf8[] = {0xE6, 0xB1, 0x89, 0xE5, 0xAD, 0x97};	// UTF-8编码
	WORD data_utf16[] = {0x6c49, 0x5b57};				// UTF-16编码
	DWORD data_utf32[] = {0x6c49, 0x5b57};				// UTF-32编码

这里用BYTE、WORD、DWORD分别表示无符号8位整数,无符号16位整数和无符号32位整数。UTF-8、UTF-16、UTF-32分别以BYTE、WORD、DWORD作为编码单位。

“汉字”的UTF-8编码需要6个字节。“汉字”的UTF-16编码需要两个WORD,大小是4个字节。“汉字”的UTF-32编码需要两个DWORD,大小是8个字节。4.2节会介绍将数字映射到UTF编码的规则。

3.4 作为字节流的字符(Character encoding scheme)

字符编码的第四层是计算机看到的字符,即在文件或内存中的字节流。例如,“字”的UTF-32编码是0x5b57,如果用little endian表示,字节流是“57 5b 00 00”。如果用big endian表示,字节流是“00 00 5b 57”。

字符编码的第三层规定了一个字符由哪些编码单位按什么顺序表示。字符编码的第四层在第三层的基础上又考虑了编码单位内部的字节序。UTF-8的编码单位是字节,不受字节序的影响。UTF-16、UTF-32根据字节序的不同,又衍生出UTF-16LE、UTF-16BE、UTF-32LE、UTF-32BE四种编码方案。LE和BE分别是Little Endian和Big Endian的缩写。

3.5 小结

通过四层模型,我们又把字符编码中发生的这些事情梳理了一遍。其实大多数代码页都不需要完整的四层模型,例如GB18030以字节为编码单位,直接规定了字节序列和字符的映射关系,跳过了第二层,也不需要第四层。

4 再谈Unicode

Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。Unicode用数字0-0x10FFFF来映射这些字符,最多可以容纳1114112个字符,或者说有1114112个码位。码位就是可以分配给字符的数字。UTF-8、UTF-16、UTF-32都是将数字转换到程序数据的编码方案。

Unicode字符集可以简写为UCS(Unicode Character Set)。早期的Unicode标准有UCS-2、UCS-4的说法。UCS-2用两个字节编码,UCS-4用4个字节编码。UCS-4根据最高位为0的最高字节分成2^7=128个group。每个group再根据次高字节分为256个平面(plane)。每个平面根据第3个字节分为256行 (row),每行有256个码位(cell)。group 0的平面0被称作BMP(Basic Multilingual Plane)。将UCS-4的BMP去掉前面的两个零字节就得到了UCS-2。

Unicode标准计划使用group 0 的17个平面: 从BMP(平面0)到平面16,即数字0-0x10FFFF。《谈谈Unicode编码》主要介绍了BMP的编码,本文将介绍完整的Unicode编码,并从多个角度浏览Unicode。本文的介绍基于Unicode 5.0.0版本。

4.1 浏览Unicode

先看一些数字:每个平面有2^16=65536个码位。Unicode计划使用了17个平面,一共有17*65536=1114112个码位。其实,现在已定义的码位只有238605个,分布在平面0、平面1、平面2、平面14、平面15、平面16。其中平面15和平面16上只是定义了两个各占65534个码位的专用区(Private Use Area),分别是0xF0000-0xFFFFD和0x100000-0x10FFFD。所谓专用区,就是保留给大家放自定义字符的区域,可以简写为PUA。

平面0也有一个专用区:0xE000-0xF8FF,有6400个码位。平面0的0xD800-0xDFFF,共2048个码位,是一个被称作代理区(Surrogate)的特殊区域。它的用途将在4.2节介绍。

238605-65534*2-6400-2408=99089。余下的99089个已定义码位分布在平面0、平面1、平面2和平面14上,它们对应着Unicode目前定义的99089个字符,其中包括71226个汉字。平面0、平面1、平面2和平面14上分别定义了52080、3419、43253和337个字符。平面2的43253个字符都是汉字。平面0上定义了27973个汉字。

在更深入地了解Unicode字符前,我们先了解一下UCD。

4.1.1 什么是UCD

UCD是Unicode字符数据库(Unicode Character Database)的缩写。UCD由一些描述Unicode字符属性和内部关系的纯文本或html文件组成。大家可以在Unicode组织的网站看到UCD的最新版本

UCD中的文本文件大都是适合于程序分析的Unicode相关数据。其中的html文件解释了数据库的组织,数据的格式和含义。UCD中最庞大的文件无疑就是描述汉字属性的文件Unihan.txt。在UCD 5.0,0中,Unihan.txt文件大小有28,221K字节。Unihan.txt中包含了很多有参考价值的索引,例如汉字部首、笔划、拼音、使用频度、四角号码排序等。这些索引都是基于一些比较权威的辞典,但大多数索引只能检索部分汉字。

我介绍UCD的目的主要是为了使用其中的两个概念:Block和Script。

4.1.2 Block

UCD中的Blocks.txt将Unicode的码位分割成一些连续的Block,并描述了每个Block的用途:

开始码位结束码位Block名称(英文)Block名称(中文)
0000007FBasic Latin基本拉丁字母
008000FFLatin-1 Supplement拉丁字母补充-1
0100017FLatin Extended-A拉丁字母扩充-A
0180024FLatin Extended-B拉丁字母扩充-B
025002AFIPA Extensions国际音标扩充
02B002FFSpacing Modifier Letters进格修饰字符
0300036FCombining Diacritical Marks组合附加符号
037003FFGreek and Coptic希腊文和哥普特文
040004FFCyrillic西里尔文
0500052FCyrillic Supplement西里尔文补充
0530058FArmenian亚美尼亚文
059005FFHebrew希伯来文
060006FFArabic基本阿拉伯文
0700074FSyriac叙利亚文
0750077FArabic Supplement阿拉伯文补充
078007BFThaana塔纳文
07C007FFNKoN'Ko字母表
0900097FDevanagari天成文书(梵文)
098009FFBengali孟加拉文
0A000A7FGurmukhi锡克教文
0A800AFFGujarati古吉拉特文
0B000B7FOriya奥里亚文
0B800BFFTamil泰米尔文
0C000C7FTelugu泰卢固文
0C800CFFKannada卡纳达文
0D000D7FMalayalam德拉维族文
0D800DFFSinhala僧伽罗文
0E000E7FThai泰文
0E800EFFLao老挝文
0F000FFFTibetan藏文
1000109FMyanmar缅甸文
10A010FFGeorgian格鲁吉亚文
110011FFHangul Jamo朝鲜文
1200137FEthiopic埃塞俄比亚文
1380139FEthiopic Supplement埃塞俄比亚文补充
13A013FFCherokee切罗基文
1400167FUnified Canadian Aboriginal Syllabics加拿大印第安方言
1680169FOgham欧甘文
16A016FFRunic北欧古字
1700171FTagalog塔加路文
1720173FHanunoo哈努诺文
1740175FBuhid布迪文
1760177FTagbanwaTagbanwa文
178017FFKhmer高棉文
180018AFMongolian蒙古文
1900194FLimbu林布文
1950197FTai Le德宏傣文
198019DFNew Tai Lue新傣文
19E019FFKhmer Symbols高棉文
1A001A1FBuginese布吉文
1B001B7FBalinese巴厘文
1D001D7FPhonetic Extensions拉丁字母音标扩充
1D801DBFPhonetic Extensions Supplement拉丁字母音标扩充增补
1DC01DFFCombining Diacritical Marks Supplement组合附加符号补充
1E001EFFLatin Extended Additional拉丁字母扩充附加
1F001FFFGreek Extended希腊文扩充
2000206FGeneral Punctuation一般标点符号
2070209FSuperscripts and Subscripts上标和下标
20A020CFCurrency Symbols货币符号
20D020FFCombining Diacritical Marks for Symbols符号用组合附加符号
2100214FLetterlike Symbols似字母符号
2150218FNumber Forms数字形式
219021FFArrows箭头符号
220022FFMathematical Operators数学运算符号
230023FFMiscellaneous Technical零杂技术用符号
2400243FControl Pictures控制图符
2440245FOptical Character Recognition光学字符识别
246024FFEnclosed Alphanumerics带括号的字母数字
2500257FBox Drawing制表符
2580259FBlock Elements方块元素
25A025FFGeometric Shapes几何形状
260026FFMiscellaneous Symbols零杂符号
270027BFDingbats杂锦字型
27C027EFMiscellaneous Mathematical Symbols-A零杂数学符号-A
27F027FFSupplemental Arrows-A箭头符号补充-A
280028FFBraille Patterns盲文
2900297FSupplemental Arrows-B箭头符号补充-B
298029FFMiscellaneous Mathematical Symbols-B零杂数学符号-B
2A002AFFSupplemental Mathematical Operators数学运算符号
2B002BFFMiscellaneous Symbols and Arrows零杂符号和箭头
2C002C5FGlagolitic格拉哥里字母表
2C602C7FLatin Extended-C拉丁字母扩充-C
2C802CFFCoptic科普特文
2D002D2FGeorgian Supplement格鲁吉亚文补充
2D302D7FTifinagh提非纳字母
2D802DDFEthiopic Extended埃塞俄比亚文扩充
2E002E7FSupplemental Punctuation标点符号补充
2E802EFFCJK Radicals Supplement中日韩部首补充
2F002FDFKangxi Radicals康熙字典部首
2FF02FFFIdeographic Description Characters汉字结构描述字符
3000303FCJK Symbols and Punctuation中日韩符号和标点
3040309FHiragana平假名
30A030FFKatakana片假名
3100312FBopomofo注音符号
3130318FHangul Compatibility Jamo朝鲜文兼容字母
3190319FKanbun日文的汉字批注
31A031BFBopomofo Extended注音符号扩充
31C031EFCJK Strokes中日韩笔划
31F031FFKatakana Phonetic Extensions片假名音标扩充
320032FFEnclosed CJK Letters and Months带括号的中日韩字母及月份
330033FFCJK Compatibility中日韩兼容字符
34004DBFCJK Unified Ideographs Extension A中日韩统一表意文字扩充A
4DC04DFFYijing Hexagram Symbols易经六十四卦象
4E009FFFCJK Unified Ideographs中日韩统一表意文字
A000A48FYi Syllables彝文音节
A490A4CFYi Radicals彝文字根
A700A71FModifier Tone Letters声调修饰字母
A720A7FFLatin Extended-D拉丁字母扩充-D
A800A82FSyloti NagriSyloti Nagri字母表
A840A87FPhags-paPhags-pa字母表
AC00D7AFHangul Syllables朝鲜文音节
D800DB7FHigh Surrogates高位替代
DB80DBFFHigh Private Use Surrogates高位专用替代
DC00DFFFLow Surrogates低位替代
E000F8FFPrivate Use Area专用区
F900FAFFCJK Compatibility Ideographs中日韩兼容表意文字
FB00FB4FAlphabetic Presentation Forms字母变体显现形式
FB50FDFFArabic Presentation Forms-A阿拉伯文变体显现形式-A
FE00FE0FVariation Selectors字型变换选取器
FE10FE1FVertical Forms竖排标点符号
FE20FE2FCombining Half Marks组合半角标示
FE30FE4FCJK Compatibility Forms中日韩兼容形式
FE50FE6FSmall Form Variants小型变体形式
FE70FEFFArabic Presentation Forms-B阿拉伯文变体显现形式-B
FF00FFEFHalfwidth and Fullwidth Forms半角及全角字符
FFF0FFFFSpecials特殊区域
100001007FLinear B Syllabary线形文字B音节文字
10080100FFLinear B Ideograms线形文字B表意文字
101001013FAegean Numbers爱琴海数字
101401018FAncient Greek Numbers古希腊数字
103001032FOld Italic古意大利文
103301034FGothic哥特文
103801039FUgaritic乌加里特楔形文字
103A0103DFOld Persian古波斯文
104001044FDeseret德塞雷特大学音标
104501047FShavian肃伯纳速记符号
10480104AFOsmanyaOsmanya字母表
108001083FCypriot Syllabary塞浦路斯音节文字
109001091FPhoenician腓尼基文
10A0010A5FKharoshthi迦娄士悌文
12000123FFCuneiform楔形文字
124001247FCuneiform Numbers and Punctuation楔形文字数字和标点
1D0001D0FFByzantine Musical Symbols东正教音乐符号
1D1001D1FFMusical Symbols音乐符号
1D2001D24FAncient Greek Musical Notation古希腊音乐符号
1D3001D35FTai Xuan Jing Symbols太玄经符号
1D3601D37FCounting Rod Numerals算筹
1D4001D7FFMathematical Alphanumeric Symbols数学用字母数字符号
200002A6DFCJK Unified Ideographs Extension B中日韩统一表意文字扩充 B
2F8002FA1FCJK Compatibility Ideographs Supplement中日韩兼容表意文字补充
E0000E007FTags标签
E0100E01EFVariation Selectors Supplement字型变换选取器补充
F0000FFFFFSupplementary Private Use Area-A补充专用区-A
10000010FFFFSupplementary Private Use Area-B补充专用区-B

Block是Unicode字符的一个属性。属于同一个Block的字符有着相近的用途。Block表中的开始码位、结束码位只是用来划分出一块区域,在开始码位和结束码位之间可能还有很多未定义的码位。使用UniToy,大家可以按照Block浏览Unicode字符,既可以按列表显示:

也可以显示每个字符的详细信息:

4.1.3 Script

Unicode中每个字符都有一个Script属性,这个属性表明字符所属的文字系统。Unicode目前支持以下Script:

Script名称(英文)Script名称(中文)Script包含的字符数
Arabic阿拉伯文966
Armenian亚美尼亚文90
Balinese巴厘文121
Bengali孟加拉文91
Bopomofo汉语注音符号64
Braille盲文256
Buginese布吉文30
Buhid布迪文20
Canadian Aboriginal加拿大印第安方言630
Cherokee切罗基文85
CommonCommon5020
Coptic科普特文128
Cuneiform楔形文字982
Cypriot塞浦路斯音节文字55
Cyrillic西里尔文277
Deseret德塞雷特大学音标80
Devanagari天成文书(梵文)107
Ethiopic埃塞俄比亚文461
Georgian格鲁吉亚文120
Gothic哥特文94
Glagolitic格拉哥里字母表27
Greek希腊文506
Gujarati古吉拉特文83
Gurmukhi锡克教文77
Han汉文71570
Hangul韩文书写系统11619
Hanunoo哈努诺文21
Hebrew希伯来文133
Hiragana平假名89
InheritedInherited461
Kannada卡纳达文86
Katakana片假名164
Kharoshthi迦娄士悌文65
Khmer高棉文146
Lao老挝文65
Latin拉丁文系1070
Limbu林布文(尼泊尔东部)66
Linear B线形文字B211
Malayalam德拉维族文(印度)78
Mongolian蒙古文152
Myanmar缅甸文78
New Tai Lue新傣文80
NkoN'Ko字母表59
Ogham欧甘文字29
Old Italic古意大利文35
Old Persian古波斯文50
Oriya奥里亚文81
OsmanyaOsmanya字母表40
Phags PaPhags Pa字母表(蒙古)56
Phoenician腓尼基文27
Runic古代北欧文78
Shavian肃伯纳速记符号48
Sinhala僧伽罗文80
Syloti NagriSyloti Nagri字母表(印度)44
Syriac叙利亚文77
Tagalog塔加路文(菲律宾)20
TagbanwaTagbanwa文(菲律宾)18
Tai Le德宏傣文35
Tamil泰米尔文71
Telugu泰卢固文(印度)80
Thaana马尔代夫书写体 50
Thai泰国文86
Tibetan藏文195
Tifinagh提非纳字母表55
Ugaritic乌加里特楔形文字31
Yi彝文1220

其中,有两个Script值有着特殊的含义:

UCD中的Script.txt列出了每个字符的Script属性。使用UniToy可以按照Script属性查看字符。例如:

左侧Script窗口中,第一层节点是按英文字母顺序排列的Script属性。第二层节点是包含该Script文字的行(row),点击后显示该行内属于这个Script的字符。这样,就可以集中查看属于同一文字系统的字符。

4.1.4 Unicode中的汉字

前面提过,在Unicode已定义的99089个字符中,有71226个字符是汉字。它们的分布如下:

Block名称开始码位结束码位数量
中日韩统一表意文字扩充A34004db56582
中日韩统一表意文字4e009fbb20924
中日韩兼容表意文字f900fa2d302
中日韩兼容表意文字fa30fa6a59
中日韩兼容表意文字fa70fad9106
中日韩统一表意文字扩充B200002a6d642711
中日韩兼容表意文字补充2f8002fa1d542

UCD的Unihan.txt中的部首偏旁索引(kRSUnicode)可以检索全部71226个汉字。kRSUnicode的部首是按照康熙字典定义的,共214个部首。简体字按照简体部首对应的繁体部首检索。UniToy整理了康熙字典部首对应的简体部首,提供了按照部首检索汉字的功能:

4.2 UTF编码

在字符编码的四个层次中,第一层的范围和第二层的编码在4.1节已经详细讨论过了。本节讨论第三层的UTF编码和第四层的字节序,主要谈谈第三层的UTF编码,即怎样将Unicode定义的编码转换成程序数据。

4.2.1 UTF-8

UTF-8以字节为单位对Unicode进行编码。从Unicode到UTF-8的编码方式如下:

Unicode编码(16进制)UTF-8 字节流(二进制)
000000 - 00007F0xxxxxxx
000080 - 0007FF110xxxxx 10xxxxxx
000800 - 00FFFF1110xxxx 10xxxxxx 10xxxxxx
010000 - 10FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-8的特点是对不同范围的字符使用不同长度的编码。对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同。UTF-8编码的最大长度是4个字节。从上表可以看出,4字节模板有21个x,即可以容纳21位二进制数字。Unicode的最大码位0x10FFFF也只有21位。

例1:“汉”字的Unicode编码是0x6C49。0x6C49在0x0800-0xFFFF之间,使用用3字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将0x6C49写成二进制是:0110 1100 0100 1001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。

例2:“”字的Unicode编码是0x20C30。0x20C30在0x010000-0x10FFFF之间,使用用4字节模板了:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。将0x20C30写成21位二进制数字(不足21位就在前面补0):0 0010 0000 1100 0011 0000,用这个比特流依次代替模板中的x,得到:11110000 10100000 10110000 10110000,即F0 A0 B0 B0。

4.2.2 UTF-16

UniToy有个“输出编码”功能,可以输出当前选择的文本编码。因为UniToy内部采用UTF-16编码,所以输出的编码就是文本的UTF-16编码。例如:如果我们输出“汉”字的UTF-16编码,可以看到0x6C49,这与“汉”字的Unicode编码是一致的。如果我们输出“”字的UTF-16编码,可以看到0xD843, 0xDC30。“”字的Unicode编码是0x20C30,它的UTF-16编码是怎样得到的呢?

4.2.2.1 编码规则

UTF-16编码以16位无符号整数为单位。我们把Unicode编码记作U。编码规则如下:

为什么U'可以被写成20个二进制位?Unicode的最大码位是0x10ffff,减去0x10000后,U'的最大值是0xfffff,所以肯定可以用20个二进制位表示。例如:“”字的Unicode编码是0x20C30,减去0x10000后,得到0x10C30,写成二进制是:0001 0000 1100 0011 0000。用前10位依次替代模板中的y,用后10位依次替代模板中的x,就得到:1101100001000011 1101110000110000,即0xD843 0xDC30。

4.2.2.2 代理区(Surrogate)

按照上述规则,Unicode编码0x10000-0x10FFFF的UTF-16编码有两个WORD,第一个WORD的高6位是110110,第二个WORD的高6位是110111。可见,第一个WORD的取值范围(二进制)是11011000 00000000到11011011 11111111,即0xD800-0xDBFF。第二个WORD的取值范围(二进制)是11011100 00000000到11011111 11111111,即0xDC00-0xDFFF。

为了将一个WORD的UTF-16编码与两个WORD的UTF-16编码区分开来,Unicode编码的设计者将0xD800-0xDFFF保留下来,并称为代理区(Surrogate):

D800DB7FHigh Surrogates高位替代
DB80DBFFHigh Private Use Surrogates高位专用替代
DC00DFFFLow Surrogates低位替代

高位替代就是指这个范围的码位是两个WORD的UTF-16编码的第一个WORD。低位替代就是指这个范围的码位是两个WORD的UTF-16编码的第二个WORD。那么,高位专用替代是什么意思?我们来解答这个问题,顺便看看怎么由UTF-16编码推导Unicode编码。

解:如果一个字符的UTF-16编码的第一个WORD在0xDB80到0xDBFF之间,那么它的Unicode编码在什么范围内?我们知道第二个WORD的取值范围是0xDC00-0xDFFF,所以这个字符的UTF-16编码范围应该是0xDB80 0xDC00到0xDBFF 0xDFFF。我们将这个范围写成二进制:

1101101110000000 11011100 00000000 - 1101101111111111 1101111111111111

按照编码的相反步骤,取出高低WORD的后10位,并拼在一起,得到

1110 0000 0000 0000 0000 - 1111 1111 1111 1111 1111

即0xe0000-0xfffff,按照编码的相反步骤再加上0x10000,得到0xf0000-0x10ffff。这就是UTF-16编码的第一个WORD在0xdb80到0xdbff之间的Unicode编码范围,即平面15和平面16。因为Unicode标准将平面15和平面16都作为专用区,所以0xDB80到0xDBFF之间的保留码位被称作高位专用替代。

4.2.3 UTF-32

UTF-32编码以32位无符号整数为单位。Unicode的UTF-32编码就是其对应的32位无符号整数。

4.2.4 字节序

根据字节序的不同,UTF-16可以被实现为UTF-16LE或UTF-16BE,UTF-32可以被实现为UTF-32LE或UTF-32BE。例如:

字符Unicode编码UTF-16LEUTF-16BEUTF32-LEUTF32-BE
0x6C4949 6C6C 4949 6C 00 0000 00 6C 49
0x20C3043 D8 30 DCD8 43 DC 3030 0C 02 0000 02 0C 30

那么,怎么判断字节流的字节序呢?

Unicode标准建议用BOM(Byte Order Mark)来区分字节序,即在传输字节流前,先传输被作为BOM的字符"零宽无中断空格"。这个字符的编码是FEFF,而反过来的FFFE(UTF-16)和FFFE0000(UTF-32)在Unicode中都是未定义的码位,不应该出现在实际传输中。下表是各种UTF编码的BOM:

UTF编码Byte Order Mark
UTF-8EF BB BF
UTF-16LEFF FE
UTF-16BEFE FF
UTF-32LEFF FE 00 00
UTF-32BE00 00 FE FF

5 结束语

程序员的工作就是将复杂的世界简单地表达出来,希望这篇文章也能做到这一点。本文的初稿完成于2007年2月14日。我会在我的个人主页http://www.fmddlmyy.cn维护这篇文章的最新版本。

 

Google
 

个人主页留言本我的空间我的程序 fmdd@263.net