字符和编码

前言

作为爬虫工程师, 经常会遇到网页编码的问题, 所以自己找资料总结起来, 方便以后学习

小例子

记事本-联通

什么是字符?

一个文字就是一个字符, 在计算机中,字符用数字表示, 不同的文字用不用的数字表示

什么是字符集?

集,就是集合的意思,字符集就是一定量的字符的集合
一个字符集包含固定数量的字符比如:ASCII, GB2312, BIG5, unicode等等

为什么会有这么多字符集?

待会再说

什么是字符编码?

计算机要处理各种字符, 就需要将字符和二进制对应起来,这种关系就是字符编码,指定编码首先需要字符集, 还要根据字符集的多少来确定用几个字节来编码

简单总结就是:
字符集是字符的集合,字符编码是把字符集的实现方式

什么是字节?

一个字节由八位组成, 位(bit)表示一个二进制0或者1

讲故事编码

计算机由来

很久以前一群人决定用8个开合的晶体管来组合成不同的状态, 在我们现在看来,它就是一个字节(8位)

ASCII

计算机开始是在美国, 八位的字节可以组合256(2的8次方)中不同的状态, 他们把其中的编号从0开始的32种状态分别规定了特殊的用途一但终端、打印机遇上约定好的这些字节被传过来时,就要做一些约定的动作。遇上0×10, 终端就换行,遇上0×07, 终端就向人们嘟嘟叫,他们看到这样很好,于是就把这些0×20以下的字节状态称为”控制码”,他们又把所有的空 格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第127号,这样计算机就可以用不同字节来存储英语的文字了,它们觉得很好,就取了名字叫: American Standard Code for Information Interchange,美国信息互换标准代码(ASCII编码)

后来

其它国家也要使用计算机, 但是我用的不是英文,怎么办?不是美国的ASCII只排到127号么,它们决定把从127之后的空位来表示它们的新字符和字母.则128-255就是扩展字符.

GB2312

等到中国开始用计算机的时候, 发现没有可以利用的字节来表示汉子了.汉字可是有6000多个常用汉字,但是这难不倒伟大的中国人民. 我们毫不客气的把那些127号之后的奇异字符取消掉,规定:一个小于127的字符的意义与原来的相同,但是大于127的字符连在一起时就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了.在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的”全角”字符,而原来在127以下的那些就叫做”半角”字符了.这就是GB2312

GBK

很快发现,中国汉字太多了, 很多人的人民没办法在这里打印出来,那干脆不再要求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容,结果扩展之后的编码方案被称为 GBK 标准,GBK包括了GB2312 的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号. 后来少数民族也要用电脑了,于是我们再扩展,又加了几千个新的少数民族的字,GBK扩成了 GB18030。从此之后,中华民族的文化就可以在计算机时代中传承了。 中国的程序员们看到这一系列汉字编码的标准是好的,于是通称他们叫做 “DBCS“(Double Byte Charecter Set 双字节字符集)

混乱

当时各个国家都像中国一样搞出来一套自己的编码标准, 这样谁也不认识谁.就连台湾也搞出来一套自己的编码标准来支持他们的繁体字-BIG5(大五码)

unicode

就在这个时候, iso(国际标准化组织)出现了.他们开始着手解决这个问题.简单粗暴,废除所有的地区性编码方案,重新打造一个支持地球上所有字符和字母:Universal Multiple-Octet Coded Character Set(超级字符集UCS),俗称unicode

unicode规定必须用16位来统一表示所有的字符,对于ASCII里的那些“半角”字符,unicode包持其原编码不变,只是将其长度由原来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码,由于”半角”英文符号只需要用到低8位,所以其高8位永远是0

unicode的缺点?

  1. 从unicode的介绍可以看到,所有的字符都是两个字节, 对于那些英文字符就是浪费
  2. 还有一个问题就是, 如何区分unicode和ascii. 计算机怎么知道三个字节表示一个符号还是分别表示三个符号

unicode的机遇 - 互联网

unicode在很长一段时间无法推广, 直到互联网的出现, 互联网出现前都是本地单机, 互联网出现之后, 数据要从一个地方传输到另一个遥远的地方, 这两个地方可能使用完全不同的字符集,现在他们不得不考虑unicode

UTF-16

使用固定2个字节16位来表示一个字符

UTF-8

1
2
3
**unicode与utf-8的区别**
unicode: 它只是字符集, 定义了世界上所有文字的对应哪一个数字, 每个符号对应一个数字序号
utf-8: 它是字符编码, 有了数字序号, 要编码成几个字节, 字节顺序如何, 都有utf-8来定义, 所以后面出现了utf-16, utf-32,它们都是使用unicode字符集, 但是却有不同的编码方式

UTF: UCS Transfer Format
UTF-8: 顾名思义, 使用的是每次8位传输数据
UTF-8特点: 使用变成的编码方式.它可以使用1-4个字节来表示一个字符.根据不同的符号来变化字节的长度

1. ASCII码范围: 1个字节
2. 中文: 大部分是3个字节

UTF-8的编码规则

  1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
  2. 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码
    1
    2
    3
    4
    5
    6
    7
    Unicode符号范围 | UTF-8编码方式
    (十六进制) | (二进制)
    ——————–+———————————————
    0000 0000-0000 007F | 0xxxxxxx
    0000 0080-0000 07FF | 110xxxxx 10xxxxxx
    0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
    0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

举例:
已知“严”的unicode是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此“严”的UTF-8编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是“11100100 10111000 10100101”,转换成十六进制就是E4B8A5。

UTF-8和Unicode的转换

从前面介绍可以知道, unicode和utf-8不是字节对应的, 需要一些算法和规则来转换

大端模式和小段模式

一个故事

英国作家斯威夫特的《格列佛游记》中描述: 小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。

正文

以汉字”严“为例,Unicode码是4E25,需要用两个字节存储,一个字节是4E,另一个字节是25。存储的时候,4E在前,25在后,就是Big endian方式;25在前,4E在后,就是Little endian方式。

问题出现了

计算机怎么知道某一个文件到底采用哪一种方式编码?

UTF-16的字节序

前面说到UTF-16是2个字节表示一个字符
例如收到一个“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”?
这就是UTF-16存在的字节序问题, 那要怎么处理了?
Unicode规范中推荐的标记字节顺序的方法是BOM: Byte Order Mark
Unicode规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格“(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且FF比FE大1。
如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式

UTF-8的字节序

读入第一个字节,该字节中包含了该Unicode字符总共用几个字节编码的信息(例如3个字节),然后根据上述信息再读入接下来的字节(如2个字节),由此完成一个字符的解码,以此类推。因此整个Unicode文件对解码器来说只是一个字节(8bit)流,所以不涉及字节序的问题。

一个有趣的问题

当你在 windows 的记事本里新建一个文件,输入”联通”两个字之后,保存,关闭,然后再次打开,你会发现这两个字已经消失了,代之的是几个乱码!

ANSI编码

泛指最早每种国家语言各自实现的编码方式,各个编码互相之间不兼容.和我们最开始讨论的各个国家的编如:GB2312、GBK、Big5、Shift_JIS

推测编码

当一个软件打开一个文本时,它要做的第一件事是决定这个文本究竟是使用哪种字符集的哪种编码保存的。软件一般采用三种方式来决定文本的字符集和编码:
检测文件头标识,提示用户选择,根据一定的规则猜测
最标准的途径是检测文本最开头的几个字节,开头字节 Charset/encoding,如下表:

1
2
3
4
5
EF BB BF UTF-8
FF FE UTF-16/UCS-2, little endian
FE FF UTF-16/UCS-2, big endian
FF FE 00 00 UTF-32/UCS-4, little endian.
00 00 FE FF UTF-32/UCS-4, big-endian.

记事本

新建一个文本文件的时候, 记事本默认用ANSI编码
前面也说了, ANSI代表的是系统的默认编码, 我们的系统肯定就是GB系列的编码,这个时候”联通”的内码是:

1
2
3
4
c1 1100 0001
aa 1010 1010
cd 1100 1101
a8 1010 1000

当再次打开这个文件的时候, 记事本开始猜测了, 由字节还原成字符,究竟要用哪种方式还原
我们回到utf-8的编码格式, 看到4个字节

1
2
3
4
110
10
110
10

是不是很符合UTF-8的编码风格, 这个时候记事本推测这个是一个UTF-8编码的文件, 然后用UTF-8解码.
不好意思, 在UTF-8里面可不是联通两个字, 具体是啥要查表了,反正就是乱码
提示: 这个时候只是读取出来的时候是按照utf-8读取, 但是硬盘上的数据并没有改变,如果你用别的编辑器按照gb2312打开还能正确显示 “联通”两个字

识别网页编码

爬取网页的过程中, 涉及到网页编码的问题.如何识别网页的编码:

  1. 通过服务器返回的header里的charset变量获取

    1
    Content-Type:text/html;charset=utf-8
  2. 通过页面里的meta 信息获取

    1
    <meta charset="UTF-8" />

进阶识别网页编码

抓取网页时,经常会出现以下几种情况:

  1. 这两个参数缺失了
  2. 这两个参数虽然都提供了,但是不一致
  3. 这两个参数提供了,但是与网页实际的编码不一致

universalchardet和chardet

前者是mozilla在很多年前就做了一个非常优秀的编码检测工具,叫chardet,后来有发布了算法更加优秀的universalchardet
后来出现很多的版本, 支持各个语言

1
2
https://pypi.python.org/pypi/chardet
pip install chardet

#判断网页编码

1
2
3
4
5
>>> import urllib
>>> rawdata = urllib.urlopen('http://www.google.cn/').read()
>>> import chardet
>>> chardet.detect(rawdata)
{'confidence': 0.98999999999999999, 'encoding': 'GB2312'}

判断文件编码

1
2
3
4
5
6
7
import chardet
tt=open('c:\\111.txt','rb')
ff=tt.readline()
#这里试着换成read(5)也可以,但是换成readlines()后报错
enc=chardet.detect(ff)
print enc['encoding']
tt.close()