一个例子搞懂编码问题

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种编码方式,如图其中默认的是ANSI,我们分别用这4种方式保存“联通”两个字并用010Editor打开查看

ANSI编码保存(中文字符其实就是用GBK编码)以Unicode编码保存以Unicode big endian编码保存以utf-8编码保存

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

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

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

如果你看过了第一部分的基础知识,那么就应该清楚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}

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

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

当记事本遇到一个未知编码的文件,如果发现其字节流符合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 colnewline = Truefor 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+FC080纮纴纻纼绖绤绬绹缊缐缞缷缹缻缼缽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的子类,用下面一张图可以很好表示它们的关系

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

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

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

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

import chardetf = 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 teststrprint utf8strprint gbkstrutf8f = 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-8Python中str和unicode在编码和解码过程中,如果将一个str直接编码成另一种编码,或者把str与一个unicode相加,会先把str解码成unicode,采用的编码为默认编码,一般默认编码是ascii,我们可以使用下面的代码来改变Python默认编码

# -*- coding=utf-8 -*-import sysreload(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中之后,哪些是数据哪些是指令真是傻傻分不清楚。

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

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

一个例子搞懂编码问题

相关文章:

你感兴趣的文章:

标签云: