python的编码与解码

admin 2022年1月6日01:33:57安全博客评论18 views9431字阅读31分26秒阅读模式

在python中经常会遇到乱码问题,总是折腾半天,该花点时间来学习一下啦。

详细可看此三篇文章:
字符编码详解:https://www.xjimmy.com/python-6-code.html
Python2编码: https://foofish.net/why-python-encoding-is-tricky.html
Python3编码; https://foofish.net/how-python3-handle-charset-encoding.html

0x1字符编码与乱码

0x1.1字节与字符

  • 计算机存储的一切数据,文本字符、图片、视频、音频、软件都是由一串01的字节序列构成的,一个字节等于8个比特位。
  • 而字符就是一个符号,比如一个汉字、一个英文字母、一个数字、一个标点都可以称为一个字符。
  • 字节方便存储和网络传输,而字符用于显示,方便阅读。例如字符 “p” 存储到硬盘是一串二进制数据 01110000,占用一个字节的长度

0x1.2编码与解码

  • 我们用编辑器打开的文本,看到的一个个字符,最终保存在磁盘的时候都是以二进制字节序列形式存起来的。那么从字符到字节的转换过程就叫做编码(encode),反过来叫做解码(decode),两者是一个可逆的过程。
  • 编码是为了存储传输,解码是为了方便显示阅读。

0x1.3什么是字符编码

我们知道,计算机只处理二进制的数据,所以,我们最终的代码都会编译成计算机能识别的二进制数据。比如字母A,对应二进制数1011,字母B,对应二进数1100等等,这种我们能看到的、使用到的字符和计算机能处理的二进制数字的对应关系,就可以绘制一张对应表,这就是字符编码表。

0x1.4乱码的产生

根本原因就是:对同一个字符串在读和写的时候,使用了不同的字符编码表

比如,我们用GBK字符编码来解释字符串‘你好,世界’,编译成二进制数是“1010”,然后我们在读取的时候,用了其他字符编码‘utf-8’,那么在‘utf-8’字符编码表看来,你这个二进制“1010”代表的就不是字符串“我爱你”,可能其他乱七八糟的东西,这样就产生了乱码。

0x2常用的字符编码

0x2.1ASCII编码

最早的字符编码,包含字母、数字和一些常见的符号,只有一个字节,所以最多能表示 28=256 个字符数。由于计算机是美国人发明的,所以ASCII编码表只有127个字符,因为在他们看来,已经足够用了,比如大写字母A的编码是65,小写字母z的编码是122。所以如果你的代码中只出现在127个字符中的字母、数字或者符合,用ASCII编码已经可以使你的代码在所有平台上运行都不会出现乱码问题,因为其他的编码基本都会兼容ASCII。

0x2.2ISO-8859-1

既然ASCII只能表示128个字符,显示是不能完全表示完的,所以ISO-8859-1扩展了ASCII编码,在ASCII编码之上又增加了西欧语言、希腊语、泰语、阿拉伯语、希伯来语对应的文字符号,它是向下兼容ASCII编码的。
ISO-8859-1也是单字节编码,但它是一个8位的容器,它能表示256个字符。

*0x2.3GB2312/GBK *

中国博大精深的汉字当然无法用ASCII编码来表示了,那么我们国人就自己定义了适合中国汉字的编码表——GB2312/GBK。这就是汉字的国标码,专门用来表示汉字,是双字节编码,。其中gbk编码能够用来同时表示繁体字和简体字,而gb2312只能表示简体字,gbk是兼容gb2312编码的。

0x2.4unicode

你可以想得到的是,全世界有上百种语言,类似的,日文和韩文等其他语言也有这个问题。为了统一所有文字的编码,Unicode 应运而生。Unicode 把所有语言都统一到一套编码里,这样就不会再有乱码问题了,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,规定最少2个字节(16位),即:216 = 65536,注意:此处说的的是最少2个字节,可能更多。

0x2.5UTF-16

UTF-16是UNICODE的具体实现,16即16位,UTF-16即是这个来由,定义了UNICODE字符在计算机中的存储方式,UTF-16同样使用了两个字节来表示任何字符,这样使得操作字符串非常高效,这也是java把UTF-16作为字符在内存中存储的格式的重要原因。
UTF-16适合在磁盘与内存之间使用,字符和字节的相互转换会更加简单和高效,但不适合在网络上传输,因为网络传输可能会损坏字节流。

0x2.6UTF-8

使用全部使用Unicode编码,虽然解决了乱码问题,但是随即又产生了一个新问题,资源浪费!怎么说呢,因为Unicode编码是最少2个字节的,也就是说之前用ASCII编码表示的字母A,本来一个字节就可以表示的东西,现在需要多一倍的的存储空间。
所以,本着节约的精神,又出现了把Unicode编码转化为“可变长编码”的UTF-8编码,UTF-8是Unicode的扩展之一,还有什么UTF-9,UTF-16什么的,比较少用,最常用的还是UTF-8。
UTF-8编码可以把一个Unicode字符根据实际大小编码成1-6个字节,常用的英文字母被编码成1个字节,汉字通常是3个字节,只有很生僻的字符才会被编码成4-6个字节。

字符 ASCII Unicode UTF-8
A 01000001 00000000 01000001 01000001
X 01001110 00101101 11100100 10111000 10101101

0x2.7编码总结

UTF是为unicode编码设计的一种在存储和传输时节省空间的编码方案。在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。

比如:用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件:注意下图不同状态对应不同的编码格式。

0x3python2的编码

python中字符串的缺陷:
使用 ASCII 码作为默认编码方式,对中文处理很不友好。
把字符串的牵强地分为 unicode 和 str 两种类型,误导开发者

0x3.1python 2中默认编码查看和转换

1
2
3
4
5
6
7
8
9
10
#查看默认字符编码
>>> import sys
>>> sys.getdefaultencoding()
'ascii’

#设置默认字符编码
>>> reload(sys)
>>> sys.setdefaultencoding("utf-8")
>>> sys.getdefaultencoding()
'utf-8'

0x3.2str与unicode

Python2 把字符串分为 unicode 和 str 两种类型。本质上 str 是一串二进制字节序列,下面的示例代码可以看出 str 类型的 “禅” 打印出来是十六进制的 \xec\xf8 ,对应的二进制字节序列就是 ‘11101100 11111000

1
2
3
4
5
>>> s = '禅'
>>> s
'\xec\xf8'
>>> type(s)
<type 'str'>

而 unicode 类型的 u”禅” 对应的 unicode 符号是 u’\u7985’

1
2
3
4
5
>>> u = u"禅"
>>> u
u'\u7985'
>>> type(u)
<type 'unicode'>

我们要把 unicode 符号保存到文件或者传输到网络就需要经过编码处理转换成 str 类型,于是 python 提供了 encode 方法,从 unicode 转换到 str,反之亦然。
python的编码与解码
encode

1
2
3
4
5
6
>>> u = u"禅"
>>> u
u'\u7985'
>>> s=u.encode("utf-8")
>>> s
'\xe7\xa6\x85'

decode

1
2
>>> s.decode("utf-8")
u'\u7985

不少初学者怎么也记不住 str 与 unicode 之间的转换用 encode 还是 decode,如果你记住了 str 本质上其实是一串二进制数据,而 unicode 是字符(符号),编码(encode)就是把字符(符号)转换为 二进制数据的过程,因此 unicode 到 str 的转换要用 encode 方法,反过来就是用 decode 方法。

0x3.3UnicodeEncodeError

UnicodeEncodeError 发生在 unicode 字符串转换成 str 字节序列的时候,来看一个例子,把一串 unicode 字符串保存到文件

1
2
3
text=u'hello世界'
f=open("output.txt","w")
f.write(text)

错误日志

1
UnicodeEncodeError: 'ascii' codec can't encode characters in position 5-6: ordinal not in range(128)

为什么会出现 UnicodeEncodeError?

因为调用 write 方法时,Python 会先判断字符串是什么类型,如果是 str,就直接写入文件,不需要编码,因为 str 类型的字符串本身就是一串二进制的字节序列了。

如果字符串是 unicode 类型,那么它会先调用 encode 方法把 unicode 字符串转换成二进制形式的 str 类型,才保存到文件,而 encode 方法会使用 python 默认的 ascii 码来编码

相当于:

1
u'hello世界'.encode("ascii")

但是,我们知道 ASCII 字符集中只包含了128个拉丁字母,不包括中文字符,因此 出现了 ‘ascii’ codec can’t encode characters 的错误。要正确地使用 encode ,就必须指定一个包含了中文字符的字符集,比如:UTF-8、GBK。

1
2
3
4
5
>>> u"hello世界".encode("utf-8")
'hello\xe4\xb8\x96\xe7\x95\x8c'

>>> u"hello世界".encode("gbk")
'hello\xca\xc0\xbd\xe7'

所以要把 unicode 字符串正确地写入文件,就应该预先把字符串进行 UTF-8 或 GBK 编码转换。

1
2
3
text=u'hello世界'.encode('utf-8')
f=open("output.txt","w")
f.write(text)

当然,把 unicode 字符串正确地写入文件不止一种方式,但原理是一样的,这里不再介绍,把字符串写入数据库,传输到网络都是同样的原理

0x3.4UnicodeDecodeError

UnicodeDecodeError 发生在 str 类型的字节序列解码成 unicode 类型的字符串时

1
2
3
4
5
6
7
8
9
10
>>> a = u"禅"
>>> a
u'\u7985'
>>> b = a.encode("utf-8")
>>> b
'\xe7\xa6\x85'
>>> b.decode("gbk")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'gbk' codec can't decode byte 0x85 in position 2: incomplete multibyte sequence

把一个经过 UTF-8 编码后生成的字节序列 ‘\xe7\xa6\x85’ 再用 GBK 解码转换成 unicode 字符串时,出现 UnicodeDecodeError,因为 (对于中文字符)GBK 编码只占用两个字节,而 UTF-8 占用3个字节,用 GBK 转换时,还多出一个字节,因此它没法解析。避免 UnicodeDecodeError 的关键是保持 编码和解码时用的编码类型一致。

这也回答了文章开头说的字符 “禅”,保存到文件中有可能占3个字节,有可能占2个字节,具体处决于 encode 的时候指定的编码格式是什么。
再举一个 UnicodeDecodeError 的例子

1
2
3
4
5
6
>>> x=u"hello"
>>> y="世界"
>>> x+y
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xca in position 0: ordinal not in range(128)

str 与 unicode 字符串 执行 + 操作时,Python 会把 str 类型的字节序列隐式地转换成(解码)成 和 x 一样的 unicode 类型,但Python是使用默认的 ascii 编码来转换的,而 ASCII字符集中不包含有中文,所以报错了。相当于:

1
2
3
4
>>> y.decode('ascii')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xca in position 0: ordinal not in range(128)

正确地方式应该是找到一种包含有中文字符的字符编码,比如 UTF-8或者 GBK 显示地把 y 进行解码转换成 unicode 类型

0x4python3的编码

Python3 把系统默认编码设置为 UTF-8

1
2
3
4
>>> import sys
>>> sys.getdefaultencoding()
'utf-8'
>>>

然后,文本字符和二进制数据区分得更清晰,分别用 str 和 bytes 表示。
文本字符全部用 str 类型表示,str 能表示 Unicode 字符集中所有字符,
而二进制字节数据用一种全新的数据类型,用 bytes 来表示。

0x4.1str与byte

str

1
2
3
4
5
6
7
8
9
10
11
>>> a="a"
>>> a
'a'
>>> type(a)
<class 'str'>
>>> b="是"
>>> b
'是'
>>> type(b)
<class 'str'>
>>>

byte
Python3 中,在字符引号前加‘b’,明确表示这是一个 bytes 类型的对象,实际上它就是一组二进制字节序列组成的数据,bytes 类型可以是 ASCII范围内的字符和其它十六进制形式的字符数据,但不能用中文等非ASCII字符表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> c=b'a'
>>> c
b'a'
>>> type(c)
<class 'bytes'>

>>> d = b'\xe7\xa6\x85'
>>> d
b'\xe7\xa6\x85'
>>> type(d)
<class 'bytes'>

>>> e=b'是'
File "<stdin>", line 1
SyntaxError: bytes can only contain ASCII literal characters.
>>>

bytes 类型提供的操作和 str 一样,支持分片、索引、基本数值运算等操作。但是 str 与 bytes 类型的数据不能执行 + 操作,尽管在py2中是可行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> b"a"+b"c"
b'ac'
>>> b"a"*2
b'aa'
>>> b"abcdef\xd6"[1:]
b'bcdef\xd6'
>>> b"abcdef\xd6"[-1]
214

>>> b"a" + "b"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't concat bytes to str

0x4.2encode 与 decode

str 与 bytes 之间的转换可以用 encode 和从decode 方法。
python的编码与解码
encode 负责字符到字节的编码转换。默认使用 UTF-8 编码准换。

1
2
3
4
5
6
>>> s="hello世界"
>>> s.encode()
b'hello\xe4\xb8\x96\xe7\x95\x8c'
>>> s.encode('gbk')
b'hello\xca\xc0\xbd\xe7'
>>>

decode 负责字节到字符的解码转换,通用使用 UTF-8 编码格式进行转换。

1
2
3
4
5
>>> b'hello\xe4\xb8\x96\xe7\x95\x8c'.decode()
'hello世界'

>>> b'hello\xca\xc0\xbd\xe7'.decode('gbk')
'hello世界'

0x5python2与python3的区别

0x5.1encode和decode

encode(编码):Unicode ==> utf-8或者gbk(字节流)
decode(解码):utf-8或者gbk(字节流) ==> Unicode
python的编码与解码
一个是编,一个是解,怎么样才能比较好记忆和理解呢?我是这么认为的,‘utf-8’或者‘gbk’是具体的编码格式,所以这两个要‘解(decode)’,解完之后就成了Unicode,它好比一种中间编码的状态(仅仅为了好理解),虚无定型,这个时候就可以指定一种具体的格式进行‘编(encode)’。

流程是这样的:UTF-8(解码)–> Unicode –>(编码) GBK

0x5.2python 2和3中‘str’类型的本质区别

在理解为什么python 3.x就解决了乱码这个问题之前,我们首先要知道一个事实,python 2和python 3的str是有本质区别的。

在python 2中的str是“某种具体的编码格式”,比如‘utf-8’,‘gbk’,‘ascii’,它本身存储的就是字节码(bytes),虽然在读取的时候比较方便,但是比较局限,如果要从gbk到utf-8,就得先转换成 Unicode。

1
2
3
>>> s="你好世界"
>>> s
'\xc4\xe3\xba\xc3\xca\xc0\xbd\xe7'

python 3的str格式定义变更为”Unicode类型的字符串“,在默认情况下,被引号框起来的字符串,本质是使用Unicode编码的。也就是说python3中的str就相当于python2中的unicode。一种可以理解为万金油的格式,作为一种中间编码,不管是要到gbk或者utf-8,都非常方便。

0x5.3python 2和3中‘str’类型的表现形式区别

字符串解码(转成Unicode)

  • python 2的字符串有两种解码方式:

    ① 使用decode(编码格式):可以指定字符编码

    ② 在字符串前面加‘u’:不能指定字符编码,使用默认编码方式来解码

    1
    2
    3
    4
    5
    6
    7
    8
      #① 第一种解码方式
    >>> s="你好世界"
    >>> s.decode('gbk')
    u'\u4f60\u597d\u4e16\u754c'

    #② 第二种解码方式
    >>> u"你好世界"
    u'\u4f60\u597d\u4e16\u754c'
  • Python 3的字符串不能直接解码,因为前面说过了,Python 3的str本质就是Unicode,我已经是Unicode格式了,还需要解码么?所以在Python 3中,字符串是没有decode方法的。

0x5.4encode和decode的使用场景

前面有说到,为了节约空间资源,在网络传输或者写入磁盘的时候,最终会编码为‘utf-8’或者‘gbk’格式的字节码,

  • 在Python 2中,因为Python 2的内存数据直接就是已经编码的字节码,所以不需要进行encode或者decode,可以直接读取。

  • 在Python 3中,从网络或磁盘接收到的数据是已经编码的字节码(utf-8’或‘gbk’),而Python需要根据格式进行解码(decode)成Unicode格式,相反如果想从内存、磁盘或者网络中写入数据,python 3要先进行编码(encode),一句话总结:接收解码,发送编码。

为了便于记忆,下面简易的画了个图帮助理解。

0x5.5字符编码使用

  • 在python 2中,默认使用ASCII编码,所以如果你的代码出现中文,妥妥的报SyntaxError,因为ASCII不认识中文

    所以在python 2中为了解决中文问题,都会在源文件的头部加上以下信息:

    1
    2
    #!/usr/bin/env python3
    # -*- coding: utf-8 -*- 或者 #coding=utf-8

    第一行注释:为了告诉Linux/OS X系统,这是一个Python可执行程序, Windows系统会忽略这个注释;
    第二行注释:为了告诉Python解释器,按照UTF-8编码去处理字符串。

  • python 3
    在python 3中,默认使用‘utf-8’编码,所以在编写python 3代码时,如果要支持中文字符串的处理,终于不需要手动指定编码格式。

0x5.6python2 与 python3 字节与字符的对应关系

python2 python3 表现 转换 作用
str bytes 字节 encode 存储
unicode str 字符 decode 显示

0x6实例演示

到此为止,你以为就结束了么?非也非也,你以为在Python 2中配置了‘utf-8’,或者直接使用python 3就一定不会出现乱码问题了么?答案肯定不是,如果不注意,仍然会出现乱码。

  • ① 我们下面在windows的CMD来做一个小示例:

    在python 3.7中执行脚本,脚本只有一条语句:print(‘科比’),按理来说,python 3.7肯定是支持中文的啦,为什么还是会报错?

    因为我们这里设了一个小小的坑,我的test.py源代码脚本的保存格式是‘GBK’的,所以当python解释器拿到这个字符串(GBK格式),然后用‘utf-8’去decode成Unicode这个过程当然会有问题,所以直接报错了。

    所以最终,我们的源代码脚本的编码格式和设置的编码格式要保证一致。

  • ② 到这里还没完,对,就是没完没了了。下面再看一个示例:
    python的编码与解码
    咦,不对啊,我‘utf-8’也申明了,源代码文件格式也是‘utf-8’了,为什么还给我打印乱码?
    首先,我们要知道运行这个脚本,打印中文“科比”这条语句是涉及到了两个角色的,一个自然就是我们的python解释器,一个是我们的控制台,在windows是cmd,linux是shell,python要打印字符串,会调用控制台进行显示的,所以会把要显示的字符串数据传给控制台。

    知道有这么个隐藏的动作,就好理解为什么会打印出乱码了?如果我前面的讲解你都能懂的话,你应该能猜到了,首先,我是在python 2中打印这条语句,我虽然设置了默认编码格式为‘utf-8’,然后编码成字节码(utf-8格式)传送给我们的控制台cmd,而cmd拿到这串数据,用‘gbk’去解码,注意,这里就是产生乱码的原因!

    windows的cmd是‘gbk’编码格式的,而我们的字符串是‘utf-8’格式的,所以产生了乱码。

    如果你把这个脚本文件拿到linux上去运行,正常打印,因为我们linux的shell是‘utf-8’的!当然在windows用python 3去运行这个脚本,也没有问题,为什么?因为python 3的str是Unicode类型的,cmd拿到就可以直接编码成‘gbk’格式的

参考文章:
字符编码详解:https://www.xjimmy.com/python-6-code.html
Python 编码为什么那么蛋疼? https://foofish.net/why-python-encoding-is-tricky.html
Python3 是如何解决棘手的字符编码问题的? https://foofish.net/how-python3-handle-charset-encoding.html

FROM :blog.cfyqy.com | Author:cfyqy

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月6日01:33:57
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  python的编码与解码 http://cn-sec.com/archives/721861.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: