从程序员的角度看ASCII, GB2312, UNICODE, UTF-8

字符编码是怎么回事 


0. 概念
字节是计算机的最基本存储单位,一个字节包括8个位.
字符是一种文字的基本单位,比如'A' 是一个字符,'汉' 也是一个字符.

1. 计算机被发明之后,程序员们编写了很多复杂的计算让计算机运行.
但是一个问题是,计算机如何把辛苦计算的结果告知程序员? 假设计算机把计算结果放在某个寄存器,内容是 1010010
总不能让程序员去检测每个引脚的电位吧? 还是得有个显示器.
显示器是依靠点阵来显示图像的. CPU必须告诉显示器,当CPU 把一个字节的数据比如 00101010 放入显示器寄存器的时候,显示器要显示怎样的一个点阵(图像),这个图像就是我们人类可以看得懂的字符. 这样问题就解决了,比如当CPU把00110001放入显示器寄存器时,显示器就显示(控制点阵画出一个图像-字体)字符 "1", 这是一个查表的过程,内存中的值(内码)和字符是一一对应的. 问题是这个对应关系是可以自由确定的,我可以指定显示器把 00110001(内码) 显示为字符 "1",也可以指定显示为字符"2". 这样当然会引起混乱,同一个内码被映射为不同的字符,不利于人们的交流. 

美国国家标准学会(ANSI)决定着手解决这个问题, 英语有一个很小的字符集,26个字母再加上一些控制字符和标点符号,7位2进制值就足以表示所有的变化.于是ANSI公布了一个标准的对应关系,以字节为单位. 当内码为 0110001 时,大家都公认它代表字符 "1",在所有显示器都显示为同一个字符. 这样大家就可以按照同一个标准相互交换数据而不会引起误解. 这个表就是一个包含了128项的对应关系, 叫做 "ASCII", 美国信息交换标准代码.

2.对于中国这样不使用ABC字符的国家来说,如何显示自己的文字是一个大问题. 我们可以制定一个内码表,指定一个内码对应一个汉字. (由于中文的字符非常多,所以一个字节是不够的,至少也要有2个字节存储一个内码.) 这是很容易的,只要国家公布一个标准的内码字符对应表,大家都遵照这个表就可以了.但是还是有一些问题要注意:

(1). 即使在中国,计算机还是得能显示英文吧? 而英文的内码已经有 ASCII 标准在先,并且已经有无数的程序已经在这个标准上运行了很多年,成为不可或缺的部分. 所以我们新制定的内码表必须和 ASCII 兼容.

(2) 很多C语言的库函数是以内码0作为字符串结束标志的,为了兼容那些以前就已经编写好,并且运行良好的程序,我们指定的内码中不能含有值为0的字节.

巧合的是,所有的ASCII内码的最高位都为0. 那么我们只要让第一个和第二个字节(一个汉字占用2个字节)的最高位都为1,这样既和ASCII内码区分开来,又不会出现0.符合这个规则的内码(2字节长)理论上一共可以标识 127 * 127 = 16129个字符(实际上只用了6000多个码位,保留了一些,不过也已经够用了,常用的中文字符只有4000多个).

我们国家公布的这个内码标准表就是GB2312.

原有的英文软件可以很好的运行,C的库函数也不用做修改, 比如 strlen("ABC") 在GB2312表示的内码中, 由于GB2312对英文字符的编码是和ASCII完全一样的,所以返回3.对于 strlen("A汉字"), 由于strlen()是以内码为0作为边界的,而所有中文字符的GB2312内码高位都为1,不会出现0,并且每个汉字占用2个字节,所以 strlen 返回5. 对于程序来说只要检查一个字节的最高位,就可以很容易的判断这个字符是中文还是英文字符,非常方便.


"一个字母一个字节,一个汉字2个字节" 的观念深入人心.

有了GB2312之后,汉字显示/存储/交换就基本上没什么问题了.
几乎所有的非英语国家都制定了和GB2312类似兼容ASCII的内码字符对应表.
(BIG5 由于有几个字符的内码和ASCII相同但表示不同的字符,不符合2.(1)条件.所以被认为是有"瑕疵"的.)

3. 很明显,GB2312的码位是不够的, 一个例子就是有很多人的人名电脑里打不出来.(只有6000多个码位,而<<康熙字典>>就收录了4万多个汉字).所以后来有出现了诸如GBK, GB18030以及同期流行于台湾香港的BIG5编码. 虽然编码有些不同, 但是设计思想是一致的: 兼容ASCII,并确保不会有某个字节值为0的内码出现.有一个共同的特点是: 它们都是局部的标准,只流行于某个地区/国家内.

4.由于内码表都是各个国家独自制定的,同一个内码,在不同的国家表示的可能是不同的字符.(除了ASCII字符, ASCII字符在所有国家指定的内码表中都有同样的值.)不利于国家间的信息交换. 于是 UNICODE 应运而生.

UNICODE 采用一种很简单的办法来解决这个问题. 就是采用2个 - UCS-2 (或者4个字节 - UCS-4)字节标识一个字符. 2个字节总共可以表示65535个字符,足够表示世界上的所有语言的所有字符.(汉字不就有4万多个吗,65535怎么够. 我估计只是常用的汉字几千个被编在UCS-2中吧. 目前被正式编码到UNICODE码位的只有不超过65534个, 所以就目前的情况来说,用2个字节是可以的.) 注意 UCS-4, UCS-2 和 ASCII是向下兼容的,只要前面补0就可以了.这点很重要,可以一直扩展下去包含全宇宙的字符.

现在地球上每个字符在所有采用UNICODE字符编码的计算机内都有一个唯一的内码了.
要注意, 除了ASCII字符外,其他国家文字的字符的内码是重新分配过的,不一定和各国原有的编码相同.比如大部分汉字的GB2312内码和UNICODE 内码都是不同的.

5. 很显然,对于英语国家来说,UNICODE内码非常浪费空间,对于UCS-2 浪费了50%的存储空间,对于 UCS-4 则浪费了70%的存储空间. 而且还有一个更大的问题, UNICODE的内码中含有很多 '\0', 原有的C标准库函数没办法处理这些字符串.于是有人发明了一种针对UNICODE的变换规则,把UNICODE字符串中的0去除. 注意这个变换规则不是通过查表实现的,而只要用一些位移操作就可以实现. 这就是UTF8. UTF8 只是 UNICODE内码在存储/传输时的状态. 而从GB2312编码转换到UNICODE编码需要查表. UTF8 和 UNICODE 的关系 与 GB2312 和 UNICODE的关系有本质的不同. UTF8 和 UNICODE 是一个人的两个面孔, GB2312 和 UNICODE 是两个人. 所以,要实现UTF8编码到GB2312编码的转换必须先把 UTF8编码还原为UNICODE编码,再通过查表的方式,把UNICODE编码转化为GB2312编码.

以上,虽然说得不是很严谨(比如GB2312其实是区位码,真正的内码还要给每个字节加上A0, 这些我都没提,免得分散注意力),但是文字编码的原理大致就是那么回事,理解就好了. 要想

详细了解细节Google一下能找到很多资料.

字符编码的编程相关问题. 


1. Windows从NT开始,内核使用UNICODE内码. 为了向前兼容,前端使用的还是GB2312内码(中文环境).  所以用 Visual Studio 编写代码时, 如果在CPP文件中写这样一句 const char* pszText = "中文", 编译器让 pszText 指向"中文"的GB2312内码值的内存空间. 当调用 printf(pszText)时, WINAPI 把这个GB2312字符串转化为UNICODE字符串再输出.(WIndows自然知道你的编码是GB2312,因为你在Windows系统中设置的语言区域是中国, CodePage 936. 如果改成其它语言,就会显示为乱码.)
微软非常鼓励Windows程序员用Unicode编写程序,很明显,由于Windows内核就是原生的Unicode环境,调用API时,省却了编码转换的操作,效率更高. 而且一个额外的好处就是不会有乱码. 注意,MS的C/C++编译器把sizeof(wchar_t)设置为2个字节. 由于目前所有的UNICODE字符只有65534个码位(BMP),所以用2个字节是没问题的.

2. Linux系统(比如Ubuntu)现在一般都用UTF8编码了. 我们在Linux下创建CPP文件并添加同样的: const char* pszText = "中文" 编译器会让 pszText 指向"中文"UTF8的内码值的内存空间.Linux的终端可以理解为一个只接收UTF8字符串的显示器. 任何被写到终端的字符流都被认为是是一个UTF8字符流.所以,编程的时候,从外部(文件或者控制台)读入UTF8字符流,转换为wchar_t,然后程序在内部使用宽字符处理,最后再把要输出的宽字符流转换为UTF8字符流并输出到控制台/文件中. 用户程序可以通过环境变量LANG的值得知当前的系统环境所使用的字符编码.由此可见,C库函数的 mbstowcs()/wcstombs()主要是为应付这种情况设计的. 如果要处理XML, HTML 等等有明确指明字符编码的字符流,用专门的字符转换库更为方便.
为什么很多Windows下的C源文件的注释在Linux编辑器下会显示为乱码就很好理解了.

3. 字符编码转换相关的函数和库
Windows 的字符转换函数: MultiByteToWideChar() / WideCharToMultiByte()
Linux 的字符转换库: GLIBC iconv函数组.
C标准库使用的 mbstowcs()和wcstombs()和 locale 相关,用起来很不方便,而且功能有限.
(注意不要假设 wchar_t 的大小, 它可能是4字节也可能是2字节,取决于编译器. 比如 MS VC9.0 (2008) 里, sizeof(wchar_t) = 2, 而在GCC中, sizeof(wchar_t) = 4.)

4. 给定一个ANSI兼容的字符串(包括GB2312,GBK,UTF8等),无法确定它的编码类型,只能猜测.所以不要指望会有一个万能的转换函数.

5. BOM (Byte Order Mark)UNICODE: FF FE / FE FF 和 UTF8: EF BB BF 是不完全靠谱的,仅供参考.

最后说明一点,对于不是专门处理字符编码的程序来说,所有字符编码相关的问题只是显示的问题,并不会影响到程序的内在逻辑.
开始用 Unicode 来编写我们的代码吧.
=============================================================
 上我自己写的判断(猜测)一个文本是否是UTF8文本的函数:
// 返回值说明: 
// 0 -> 输入字符串符合UTF-8编码规则,有可能是UTF8串
// -1 -> 检测到非法的UTF-8编码首字节
// -2 -> 检测到非法的UTF-8字节编码的后续字节.

int IsTextMaybeUTF8 (const char* pszSrc) 

    const unsigned char* puszSrc = (const unsigned char*)pszSrc; // 一定要无符号的,有符号的比较就不正确了. 
    // 看看有没有BOM表示 EF BB BF 
    if( puszSrc[0] != 0 && puszSrc[0] == 0xEF && 
        puszSrc[1] != 0 && puszSrc[1] == 0xBB && 
        puszSrc[2] != 0 && puszSrc[2] == 0xBF) 
    { 
        return 0; 
    } 
    // 如果没有 BOM标识 
    BOOL bIsNextByte = FALSE; 
    int nBytes = 0; // 记录一个字符的UTF8编码占用几个字节. 
    const unsigned char* pCur = (const unsigned char*)pszSrc; 
    while( pCur[0] != 0 ) 
    { 
        if(!bIsNextByte) 
        { 
            bIsNextByte = TRUE; 
            if ( (pCur[0] >> 7) == 0) { bIsNextByte = FALSE; nBytes = 1; bIsNextByte = FALSE; } // 最高位为0, ANSI 兼容的. 
            else if ((pCur[0] >> 5) == 0x06) { nBytes = 2; } // 右移5位后是 110 -> 2字节编码的UTF8字符的首字节 
            else if ((pCur[0] >> 4) == 0x0E) { nBytes = 3; } // 右移4位后是 1110 -> 3字节编码的UTF8字符的首字节 
            else if ((pCur[0] >> 3) == 0x1E) { nBytes = 4; } // 右移3位后是 11110 -> 4字节编码的UTF8字符的首字节 
            else if ((pCur[0] >> 2) == 0x3E) { nBytes = 5; } // 右移2位后是 111110 -> 5字节编码的UTF8字符的首字节 
            else if ((pCur[0] >> 1) == 0x7E) { nBytes = 6; } // 右移1位后是 1111110 -> 6字节编码的UTF8字符的首字节 
            else 
            { 
                nBytes = -1; // 非法的UTF8字符编码的首字节 
                break; 
            } 
        } 
        else 
        { 
            if ((pCur[0] >> 6) == 0x02) // 首先,后续字节必须以 10xxx 开头 
            { 
                nBytes--; 
                if (nBytes == 1) bIsNextByte = FALSE; // 当 nBytes = 1时, 说明下一个字节应该是首字节. 
            } 
            else 
            { 
                nBytes = -2; 
                break; 
            } 
        } 
        // 下跳一个字符 
        pCur++; 
    } 
    if( nBytes == 1) return 0; 
    else return nBytes; 
}



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 可以使用 Python 的内置函数 `open()` 打开二进制文件。然后,可以使用内置的 `read()` 函数读取文件的内容。 为了能够正确地解码文件中的中文汉字,需要指定文件的编码格式。如果不确定文件的编码格式,可以使用 Python 的 `chardet` 库来检测编码格式。 下面是一个示例代码,假设文件名为 `file.bin`: ```python import chardet # 检测文件的编码格式 with open('file.bin', 'rb') as f: data = f.read() result = chardet.detect(data) encoding = result['encoding'] # 使用检测到的编码格式打开文件 with open('file.bin', 'r', encoding=encoding) as f: content = f.read() print(content) ``` 上面的代码首先使用 `chardet` 库检测文件的编码格式,然后使用检测到的编码格式来打开文件。最后,使用 `read()` 函数读取文件内容并打印出来。 ### 回答2: 使用Python打开二进制文件并打印不同编码格式的中文汉字可以通过以下步骤实现: 1. 首先,使用Python的内置open()函数以二进制模式打开文件,指定文件路径和打开模式。 2. 读取文件内容,使用read()函数将文件内容读取为字节流,存储在一个变量中。 3. 通过不同编码格式对字节流进行解码,并打印出中文汉字。 下面是一个示例代码: ```python # 打开二进制文件 with open('file.bin', 'rb') as file: # 读取文件内容 data = file.read() # 打印GB2312编码中文汉字 print(data.decode('gb2312')) # 打印GB18030编码中文汉字 print(data.decode('gb18030')) # 打印GBK编码中文汉字 print(data.decode('gbk')) # 打印BIG5编码中文汉字 print(data.decode('big5')) # 打印Unicode编码中文汉字 print(data.decode('unicode_escape')) # 打印UTF-8编码中文汉字 print(data.decode('utf-8')) # 打印UTF-16 BE(大端序)编码中文汉字 print(data.decode('utf-16-be')) # 打印UTF-16 LE(小端序)编码中文汉字 print(data.decode('utf-16-le')) ``` 请确保将代码中的`file.bin`替换为实际的二进制文件路径。这样,代码会使用给定的编码格式打印出文件中相应格式的中文汉字。 ### 回答3: 首先,要使用Python打开一个二进制文件,可以通过以下步骤实现: 1. 使用`open()`函数以二进制模式打开文件,指定参数`'rb'`。 2. 使用`read()`函数读取文件的内容,并将其保存在一个变量中。 接下来,我们需要将这些二进制数据解码为不同的编码格式来打印中文汉字。下面是针对不同编码格式的解码方法: 1. 对于GB2312编码,可以使用`decode()`函数将二进制数据解码为字符串,并指定参数`'gb2312'`。 2. 对于GB18030编码,可以使用`decode()`函数将二进制数据解码为字符串,并指定参数`'gb18030'`。 3. 对于GBK编码,可以使用`decode()`函数将二进制数据解码为字符串,并指定参数`'gbk'`。 4. 对于BIG5编码,可以使用`decode()`函数将二进制数据解码为字符串,并指定参数`'big5'`。 5. 对于unicode编码,可以使用`decode()`函数将二进制数据解码为字符串,并指定参数`'unicode'`。 6. 对于UTF-8编码,可以使用`decode()`函数将二进制数据解码为字符串,并指定参数`'utf-8'`。 7. 对于UTF-16 BE编码,可以使用`decode()`函数将二进制数据解码为字符串,并指定参数`'utf-16 be'`。 8. 对于UTF-16 LE编码,可以使用`decode()`函数将二进制数据解码为字符串,并指定参数`'utf-16le'`。 最后,我们可以使用`print()`函数将解码后的字符串打印出来。 以下是一个示例代码,实现了打开一个二进制文件,并以不同的编码格式打印出中文汉字: ```python # 打开二进制文件 with open('binary_file.bin', 'rb') as file: content = file.read() # 解码并打印中文汉字 print(content.decode('gb2312')) print(content.decode('gb18030')) print(content.decode('gbk')) print(content.decode('big5')) print(content.decode('unicode')) print(content.decode('utf-8')) print(content.decode('utf-16 be')) print(content.decode('utf-16le')) ``` 请注意,以上代码中的`binary_file.bin`是一个示例二进制文件名,你需要将其替换为你要打开的实际二进制文件的路径或文件名。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值