Friday, January 9, 2009

字符集与编码处理乱谈:C/C++ 中的处理、unicode与字体的关系、几种常用unicode编码的对比

预备知识:
1.系统里面有locale这个东西。比如我的系统就是LC_ALL=zh_CN.utf8

2.我们知道,一个文本文件是由一系列字符序列组成的。
这些字符又是属于某个字符集的。这个字符集可以用某种编码表示。
总而言之,某个cpp文件必须是有某种编码存储的。

3.C/C++里存储字符有两种方法。一是用"ANSI字节序列",即用char str[]这种byte的数组来存储。
因此字符的编码是编译时固定的,如:
一个用utf8编码的cpp文件:char str[]="阿"
则编译后,str的内容为:0xe9 0x98 0xbf,这就是'阿'对应的utf8编码。
如果用sizeof来求str的大小,可以得到4.显然上面已经有了3个byte来存储这个'阿'字。
而还有一个NULL(0)在str[3]的位置。因此是4.
如果用strlen来求str的长度,则可以得到3.

但是如果是用gbk编码的文件:char str[]="阿"
则编译后,str的内容为:0xb0 0xa2,这就是'阿'对应的gbk编码。
用sizeof和strlen,分别得到3与2.

使用文本编辑器保存文件时,如果选择"ANSI",则是使用当前的locale来进行对应的本地化编码存储。

以下转自:http://www.regexlab.com/zh/encoding.htm

理解编码的关键,是要把字符的概念和字节的概念理解准确。这两个概念容易混淆,我们在此做一下区分:

  概念描述 举例
字符 人们使用的记号,抽象意义上的一个符号。 '1', '中', 'a', '$', '¥', ……
字节 计算机中存储数据的单元,一个8位的二进制数,是一个很具体的存储空间。 0x01, 0x45, 0xFA, ……
ANSI
字符串
在内存中,如果"字符"是以 ANSI 编码形式存在的,一个字符可能使用一个字节或多个字节来表示,那么我们称这种字符串为 ANSI 字符串或者多字节字符串 "中文123"
(占7字节)
UNICODE
字符串
在内存中,如果"字符"是以在 UNICODE 中的序号存在的,那么我们称这种字符串为 UNICODE 字符串或者宽字节字符串 L"中文123"
(占10字节)

在C/C++里,mbs(MultiByte characterS,多字节字符) 和wcs(Wide CharacterS,宽字符)是两个完全distinct的概念。
mbs即是字节串,使用char 来存储,而wcs是unicode 字符串,使用wchar_t 来存储。
我们可以使用mbstowcs和wcstombs进行互相转换。参考:
http://www.chemie.fu-berlin.de/chemnet/use/info/libc/libc_18.html#SEC310
要注意的是,必须先使用setlocale设定程序的区域。
但是,程序运行时也受到运行环境的限制,必须先用 locale -a 查看系统支持的locale,
系统支持的locale才可以放在setlocale里面作为参数,否则wcstombs会不成功。

一般来说,可以使用这样的方法处理:
读写文件时,使用mbs进行具体的编码存储(external representation);而在程序内部,则统一使用wcs进行字符串的处理(internal representation)。

C/C++中mbs/wcs的I/O
keyword: wprintf, wcslen, fwide,

在C/C++里,用wchar_t来存储一个unicode字符。
在GNU glibc的实现里,它总是4 bytes。与Unicode的UCS-4编码是对应的。
即wchar_t a='阿';
如果把a当作整数输出,则可以得到963f,这正是``阿'' 的Unicode codepoint.
(捎带一提,与UTF-8不同。UTF-32, UCS-4 与 Unicode codepoint是一一对应的编码,但是UTF-32是UCS-4的子集)

这里要注意一个叫fwide的东西。
根据man page,每一个STREAM都有一个属性,即它是面向字节的流还是面向宽字符的流,并且:

Once  a  stream  has  an orientation, it cannot be changed and persists until the stream is closed.

可以用fwide(stream,0)的返回值来判断流的orientation。但是使用这个语句可能本身就会更改流的orientation。
在输出任何东西到流之前,必须决定这个流的orientation,也是使用fwide
fwide(stream,NEG)表示设置为byte-orientation, NEG < 0
fwide(stream,POS)表示设置为wide character-orientation, POS > 0

这里介绍了C中的字节流与宽字符流。

所以我们面对不同的STREAM要用不同的printf版本,并且要在一个STREAM打开与关闭期间统一使用。
一旦普通版本或者宽字符版本的IO库函数作用于某个stream,这个stream就固定为对应的类型了……
此后使用fwide没有任何作用。(唯一的方法是:使用freopen重新打开流)
printf本身就支持宽字符的输出:使用 %lc  或者 %ls 对应 %c 和 %s 即可。
同样地,wprintf也支持字节的输出(%c %s)
并且,在使用printf的时候,程序输出的``external representation''是根据setlocale设置的具体编码值(即不是unicode的codepoint这种internal representation),如果当前环境不支持该编码,则输出失败。
scanf亦是同理。

总结:
1.读写文件时,使用mbs进行具体的编码存储(external representation);而在程序内部,则统一使用wcs进行字符串的处理(internal representation)。
2.mbs(MultiByte characterS,多字节字符) 和wcs(Wide CharacterS,宽字符)是两个完全distinct的概念。
具体参考资料看这里:
http://www.gnu.org/software/libc/manual/html_node/

----------附:iconv 编码实验for  utf32 / ucs4 ---------------
(假设以下文档的编码与文件名对应。)
当前有一个文档为utf8,内容为"你他妈的 0123 abcd" ,执行命令:
$ iconv -t utf32 utf8 | xxd
0000000: fffe 0000 604f 0000 d64e 0000 8859 0000  ....`O...N...Y..
0000010: 8476 0000 2000 0000 3000 0000 3100 0000  .v.. ...0...1...
0000020: 3200 0000 3300 0000 2000 0000 6100 0000  2...3... ...a...
0000030: 6200 0000 6300 0000 6400 0000            b...c...d...
可见,iconv把utf8转为utf32,并且自动添加BOM=ff fe(big endian)

$ iconv -t ucs4 utf8 | xxd
0000000: 0000 4f60 0000 4ed6 0000 5988 0000 7684  ..O`..N...Y...v.
0000010: 0000 0020 0000 0030 0000 0031 0000 0032  ... ...0...1...2
0000020: 0000 0033 0000 0020 0000 0061 0000 0062  ...3... ...a...b
0000030: 0000 0063 0000 0064                      ...c...d

可见,iconv却没有添加BOM,并且完全是按照字节顺序输出的(因此与big endian machine的读写方法冲突)。

根据wikipedia的介绍,utf32是ucs4的子集,它们都是用4字节来存储每一个字符,因此可以存储所有unicode的codepoint,
也因此它们的编码方式很简单:用每个byte来与codepoint一一对应。
功能上来说,utf32与ucs4是相同的。但是utf32 的document规定了一些unicode相关的语义。

同时有一个有趣的小发现:unicode的字库应该包含unicode所有的所有字符。
然而由于现在unicode的字符空间还没有被分配完,
因此有些合法的codepoint是没有字符的(informally speaking),而只是用它的codepoint hex value表示,比如这个:
hex 1D11E = 𝄞
正是U+01D11E 对应的``字符'' 打印出来的效果(glyph),哈哈。
另外,在windows下,可以安装Chinese(Taiwan)的unicode输入法进行输入,不过我发觉只支持4位的code point……
上面那个1D11E就打不出来。
此外,字体文件(ttf等)也有字符集的区别;
字体文件可能支持unicode(似乎TT必须支持),也可能不支持(如Courier, Fixed Sys)。
不同字体文件包含unicode字符集的不同内容(如中文字体就包含中文字体,韩语字体则包含棒子字而不包含中文,显然英文字体只有英文)。
windows的SimSun和heiti就有不同的字符数,不信查一下U+00CC这个字符,SimSun有而heiti没有。

事实上:
Currently (August, 2008), no single "Unicode font" includes all the characters defined in the present revision of the ISO 10646 (Unicode) standard.In fact, it would be impossible to create such a font in any common font format, as Unicode includes over 100,000 characters, while no widely-used font format supports more than 65,535 glyphs. So while one could make a set of related fonts to cover all of Unicode, a single Unicode font is not possible at this time.

也就是说,流行的字体格式最多也只支持2^16个字,远远不够unicode的字符个数。

----------附:几种常用unicode编码的对比 ---------------
先摘录一段from 这里:http://en.wikipedia.org/wiki/Code_point
A unicode text file is not necessarily merely a sequence of code points encoded into 4 byte blocks. Instead an encoding scheme is used to serialize a sequence of code points into a sequence of bytes(回忆一下huffman编码). A number of such schemes exist, and these trade between space efficiency and ease of encoding. A variable number of bytes can be used for each character.

这里主要介绍的是几种流行unicode编码的方法:
utf8, utf16/ucs2, utf32/ucs4

首先明确一下ucs(universal character set)和utf(unicode transformation format)的区别。
N年前,有两个组织在搞unicode,于是有两套不同但类似的标准。
后来他们意识到他们在折腾,于是便合并了,但是留下了N多历史遗留名词与标准。
关于ucs2与utf16的区别,这里有官方的解释:
http://unicode.org/faq/basic_q.html#25

简而言之,从数据交换的角度来说,它们的编码方法是完全一样的,都代表了相同的从unicode字符集到字节编码的映射。
但是utf16包含了更多的杂七杂八的特定的规定。
总之,使用时,使用utf开头的一族就对了。另外,ucs是以byte为单位的,而utf是bit,所以有utf16=ucs2。

utf32/ucs4是与unicode的codepoint一一对应的,使用固定长度的4字节/32bit编码。
由于常用的字符往往在低位(即高2byte用不上)因此非常浪费空间。而且,它也不能完全覆盖unicode字符集。

utf16/ucs2比utf32稍节省空间,但是它有所谓的endian问题,即必须在相应的utf16文件开头两字节指明究竟是以和顺序存储(BOM)。
因此utf16也分为 utf16le和 utf16be.(在某些编辑器上,保存编码格式为Unicode(little endian)之类的,就是指utf16了)

utf8应该是最流行的格式,因为它是面向字节的,没有大小端的问题。
并且由于它是完全与ASCII兼容的,legacy的只能处理ascii的程序能对utf8的文件处理得很好!
另外,由于utf8是可变长编码(ascii占1字节,中文等占多字节,最多占4字节)
因此比起固定长度的utf32节省很多空间。可以说,utf8是unicode编码的实施标准。

更详细的猛击http://en.wikipedia.org/wiki/UTF-8

No comments: