bindog study Machine Learning & Security

一个例子搞懂编码问题

2014-12-16
宾狗

0x00 前言

相信中文字符编码问题每个程序员都或多或少遇到过,文件读写、网页抓取、数据库等等,凡是有中文的地方总会出现各种的编码问题,但是很少有人愿意花时间去彻底弄清这个问题(至少我以前是这样),每次出现乱码问题的时候上网一搜,不能解决的一愁莫展,能够解决的也不知其所以然。

最近在学习Python的过程中再次遇到了这个问题,决定认认真真把编码问题搞清楚,同时也把经验和心得分享给大家。如有谬误,欢迎大家批评指正~

0x01 基础知识

首先声明,本文不是科普,如果你对Unicodeutf-8gb2312gbk这样的概念非常陌生的话,强烈建议你先看下字符编码的奥秘utf-8, Unicode速解UTF-8中文字符,方法和原理这两篇文章,图文并茂~

有几点这里还是要再次强调一下:

  • 字符的编码与字符在计算机中的存储是并非完全一样
  • Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储
  • utf-8Unicode的一种实现,其他还有utf-16utf-32,只不过用的很少罢了
  • 虽然都支持中文,但是utf-8gb系列的编码是完全不兼容的,要想互相转换必须要通过Unicode作为媒介

0x02 一个关于联通的经典笑话

新建一个txt文件,输入“移动”两个字(不带引号)然后保存,再用记事本打开,显示正常。新建一个txt文件,输入“联通”两个字(不带引号)然后保存,再用记事本打开,出现乱码。因此有人说联通不如移动强是有原因的……也有人说是联通得罪了微软……

笑话归笑话,不过这的确是一个很经典的字符编码问题

我们先来看看“联通”这两个字的编码

# -*- coding: utf-8 -*-
teststr = u'联通'
print repr(teststr)
print repr(teststr.encode("utf-8"))
print repr(teststr.encode("gbk"))
# 输出
# u'\u8054\u901a'
# '\xe8\x81\x94\xe9\x80\x9a'
# '\xc1\xaa\xcd\xa8'

然后再来看看记事本是怎么存储这两个字的,Windows下记事本支持4种编码方式,如图

4-coding

其中默认的是ANSI,我们分别用这4种方式保存“联通”两个字并用010Editor打开查看

ANSI编码保存(中文字符其实就是用GBK编码)

ansi-coding

Unicode编码保存

unicode-coding

Unicode big endian编码保存

unicode-big-endian-coding

utf-8编码保存

utf8-coding

你看,字符在计算机中的存储形式的确与它们的编码表示略有不同,这里不再赘述,看完第一部分中推荐的文章你自然就明白了。

这里再啰嗦一句,最后一张图中开始的三个字节EF BB BF就是BOM了,全称Byte Order Mark,这玩意也是很多乱码问题的来源,Linux下很多程序就不认BOM。因此,强烈不建议使用BOM,使用Notepad++之类的软件保存文本时,尽量选择“以UTF-8无BOM格式编码”。我们在010Editor中手动把这前三个字节删掉后保存,然后再次用记事本打开,依然可以正常显示“联通”两个字,也就是说记事本是可以识别UTF-8无BOM编码的。

我们继续回到这个问题,为什么再次打开以ANSI形式保存的“联通”两个字会出现乱码呢?这里就涉及到Windows记事本的处理策略了,当你打开一个文本文件时,记事本并不知道这个文件采用的是什么编码。此时可以采用两种策略,一种是询问用户以什么编码打开,当然这种方式是以降低用户体验为代价的,另一种方式就是猜了,也就是记事本所采用的方式。

如果你看过了第一部分的基础知识,那么就应该清楚utf-8的编码规则 utf-8

可以看出来还是有一定规律的,我们可以写的一个正则表达式来匹配这种模式

[\x01-\x7f]|[\xc0-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3}|[\xf8-\xfb][\x80-\xbf]{4}|[\xfc-\xfd][\x80-\xbf]{5}

相信你已经看晕了,没关系,我们用一个正则表达式可视化工具来解析一下

regexper

根据这个图,再结合上面那张表,是不是一目了然呢?

当记事本遇到一个未知编码的文件,如果发现其字节流符合utf-8的编码标准,就认为这个文件是以utf-8编码的。我们来看“联通”这两个字的gbk编码,联C1 AA,通 CD A8,与上面的正则表达式对比后就可以发现,这两个字的gbk编码恰好是符合utf-8编码规范的(落在[\xC0-\xDF][\x80-\xBF]这个范围中),所以记事本就猜这个文件是以utf-8编码的,自然会出现乱码。

那么,还有哪些中文字符存在这些问题呢?我们用一个程序把它们全找出来

col = "     "
for i in range(0,16):
    col = col + "+" + hex(i)[2:].upper() + " "
print col
newline = True
for i in range(192,224):
    line = u""
    linenum1 = hex(i)[2:].upper()
    count = 0
    for j in range(128,192):
        if newline:
            linenum2 = hex(j - j % 16)[2:].upper()
            line = linenum1 + linenum2 + " "
            newline = False
        c = chr(i) + chr(j)
        line = line + c.decode("gbk") + " "
        count = count + 1
        if count % 16 == 0:
            print line.strip()
            count = 0
            newline = True
# 输出
	+0	+1	+2	+3	+4	+5	+6	+7	+8	+9	+A	+B	+C	+D	+E	+F
C080																
C090		缿														
C0A0																
C0B0																
C180																
C190																
C1A0																
C1B0																
C280																
C290																
C2A0																
C2B0										鹿						
C380																
C390																
C3A0																
C3B0																
C480																
C490																
C4A0																
C4B0																
C580																
C590																
C5A0																
C5B0																
C680																
C690																
C6A0																
C6B0																
C780																
C790																
C7A0																
C7B0																
C880																
C890																
C8A0																
C8B0																
C980																
C990																
C9A0																
C9B0																
CA80																
CA90																
CAA0											湿					
CAB0										使						
CB80																
CB90																
CBA0																
CBB0																
CC80																
CC90																
CCA0																
CCB0																
CD80																
CD90																
CDA0																
CDB0																
CE80																
CE90																
CEA0																
CEB0																
CF80																
CF90																
CFA0																
CFB0																
D080																
D090																
D0A0																
D0B0																
D180																
D190																
D1A0																
D1B0																
D280																
D290																
D2A0												耀				
D2B0																
D380					觿											
D390																
D3A0																
D3B0																
D480																
D490																詿
D4A0																
D4B0																
D580																
D590																
D5A0																
D5B0																
D680																
D690											謿					
D6A0																
D6B0																
D780																
D790																
D7A0																
D7B0																
D880																
D890																
D8A0						廿										丿
D8B0																
D980																
D990			賿													
D9A0																
D9B0																
DA80																
DA90																
DAA0																
DAB0																
DB80	踿															
DB90																
DBA0																
DBB0																
DC80																
DC90																
DCA0																
DCB0																
DD80																
DD90												輿				
DDA0																
DDB0																
DE80																
DE90										迿						
DEA0																
DEB0																
DF80																
DF90																
DFA0																
DFB0																

凡是仅由这张表里面的字构成的文本输入到记事本里用ANSI保存后,再次打开都会变成乱码。比如我们输入“昆莱山”三个字(你们懂的),保存后再次打开,果然是乱码。

如果你能看明白这个例子,相信你对字符串编码会有一个更加深入的认识。

0x03 Python中乱码处理的一般方法

(这里所说的方法适用Python 2.X,在Python 3中字符串已经不是老大难的问题了)

Python中乱码处理的关键在于理解strunicode的关系,它们都是basestring的子类,用下面一张图可以很好表示它们的关系 python-str-unicode

一般情况下,如果你得到的数据在没有被加密或者压缩的情况下出现了乱码,那多半是没有被正确的编码解析罢了,剩下的事无非就是编码转换的问题了。

比如,我抓取的一个网页是用utf-8编码的,而我的数据库的编码是gbk,直接存肯定是不行的,怎么办呢,很简单

unicodestr = webstr.decode("utf-8")
databasestr = unicodestr.encode("gbk")
# 然后把databasestr写进数据库就可以了

在某些情况下,不知道数据来源的编码是什么,那该怎么办呢?Python下有一个chardet能非常方便的解决这个问题

import chardet
f = open("unknown.txt","r")
fstr = f.read()
print chardet.detect(fstr)
# 输出
# {'confidence': XXX, 'encoding': 'XXX'}

输出有两个值,后一个是chardet认为可能的编码,前一个表示可能性的大小。只要我们提供的字符串没有什么问题,一般chardet都可以给出一个比较准确的答案。在知道目标采用了什么编码后,就可以使用前面的方法进行编码转换了。

要注意的一点是,当你使用print显示乱码时并不一定真的是乱码。比如下面这段程序

# -*- coding: utf-8 -*-
teststr = u'测试'
utf8str = teststr.encode("utf-8")
gbkstr = teststr.encode("gbk")
print teststr
print utf8str
print gbkstr

utf8f = open("utf8str.txt","w")
utf8f.write(utf8str)
utf8f.close()

gbkf = open("gbkstr.txt","w")
gbkf.write(gbkstr)
gbkf.close()

分别在EclipseWindows命令行中执行,发现Eclipseprint gbkstr出现了乱码,而在Windows命令行中print utf8str出现了乱码,但却并不影响两个文件的正常显示(编码不同罢了,记事本都可以识别)。这与Pythonprint的实现有关,print直接把字符串传递给当前运行环境,只有当该字符串与运行环境默认的编码一致时才能正常显示。

最后,再总结几点Python 2.X中常见的字符串问题

  • Python默认脚本文件都是ASCII编码的,当文件中有非ASCII编码范围内的字符的时候就要使用“编码指示”来修正,也就是在文件第一行或第二行指定编码声明:# -*- coding=utf-8 -*-或者#coding=utf-8
  • Python中str和unicode在编码和解码过程中,如果将一个str直接编码成另一种编码,或者把str与一个unicode相加,会先把str解码成unicode,采用的编码为默认编码,一般默认编码是ascii,我们可以使用下面的代码来改变Python默认编码
# -*- coding=utf-8 -*-
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
s = '测试'
s.encode('gb2312')
  • 有些时候字符串中大部分字符编码都是正确的,但是偶尔出现了一两个非法字符,这时候使用decode会抛出异常而无法正常解码,不过我们可以使用decode的第二个参数来解决这个问题,如s.decode('gbk', 'ignore'),因为decode的函数原型是decode([encoding], [errors='strict']),可以用第二个参数控制错误处理的策略,默认的参数就是strict,代表遇到非法字符时抛出异常,如果设置为ignore,则会忽略非法字符,如果设置为replace,则会用?取代非法字符。

0x04 建议

先看看下面的代码

# -*- coding: utf-8 -*-
teststr = u'测试'
utf8str = teststr.encode("utf-8")
gbkstr = teststr.encode("gbk")
print len(teststr)
print len(utf8str)
print len(gbkstr)
# 输出
# 2
# 6
# 4

注意,Python中对str进行len()操作,计算的可是字节的长度,而并非我们逻辑上的一个字。所以下面提一些建议供大家参考~

  • 使用字符编码声明,并且同一工程中的所有源代码文件使用相同的字符编码声明。
  • 工程开发中尽量使用utf-8编码,如果为了节省流量或者空间gbk编码也是可以的。
  • 字符在Python内部处理时尽量使用unicode,输入时进行decode,输出时再进行encode,就像第三部分的第一张图那样,这样就避免了刚才的那个问题。

0x05 体会

冯·诺伊曼结构(英语:von Neumann architecture),也称冯·诺伊曼模型(Von Neumann model)或普林斯顿结构(Princeton architecture),是一种将程序指令存储器和数据存储器合并在一起的电脑设计概念结构。

这是维基百科中对冯·诺伊曼结构的定义,做逆向的人应该深有体会,经过加壳或者改过入口点的二进制文件扔到OD中之后,哪些是数据哪些是指令真是傻傻分不清楚。

在编码问题上也有相似之处,之所以有乱码那就是因为同一个字节流在不同的编码中都有相应的码点对应(当然也有一些没有对应的)。

但是一旦我们找到了程序真正的入口点(找到了正确的编码方式),所有的问题自然迎刃而解~

如果你觉得本文对你有帮助,欢迎打赏我一杯咖啡钱,支持我写出更多好文章~


Comments