python 中关于字符的编码方式

在工作中遇到了有关字符编码的一个问题,在解决之后记录一下。

在一个深度学习的目标检测任务中,需要将检测出的结果输出到一个 xml 文件中,或者以 xml 的形式打印到控制台上。当向 xml 文件中传递的字符都是英文和数字时,不会有问题。但是当 xml 文件中出现中文或日文字符时,会出现一系列的编码问题。

问题

  当设置 xml 某个元素的值时,若传递进去的为中文或日文,就会报错

1
ValueError: All strings must be XML compatible: Unicode or ASCII, no NULL bytes or control characters [8444] Failed to execute script runcaffe_fpn

这个错是由

1
resource.text = image_name

这句话引发的,其中 image_name 是用 glob 函数从 Windows 系统中的某个路径下读进来的文件名。


分析和解决

经过搜索发现,lxml 模块中,当对一个树的某个节点进行赋值时,其传入值必须是 Unicode 或 ASCII 码,而使用 glob 函数读进来的文件名并不是,因而造成了错误。

经过研究发现,简体中文版的 Windows 系统使用 GBK 字符集,而不是 Unicode 字符集,因而传入的文件名不能直接赋值给 lxml 的节点对象。因此,需要对其进行转换,转换成 Unicode 码,再赋值给节点对象,也就是 name = image_name.decode('gbk')name = image_name.decode('Shift-JIS')

这样之后不会报错了,但是在生成的 xml 文件中,中日字符都是以 unicode 码显示的,此时,只要将 etree.tostring() 函数的 encoding= 参数设置为 utf-8 即可。

传入的文件名除了不能直接赋给 lmxl 的节点对象,直接进行 print 也是会乱码的(当 cmd 的 code page 与此字符串编码不一致时)。这是因为,传入的文件名的编码方式是由其代表文件所在的系统决定的,简体中文版 Windows 是 GBK,日文版是 Shift-JIS。当传入的字符串为 Shift-JIS 编码,但是尝试在 GBK 的 cmd 上打印出来时,就会报错。如下所示

1
2
3
4
5
C:\Users\sean\Pictures\01.嶥杫巗彫栰杫.png
C:\Users\sean\Pictures\fc_barcelona___wallpaper_by_ccrt.png
C:\Users\sean\Pictures\vs添加lib文件.PNG
C:\Users\sean\Pictures\户口簿首页.png
C:\Users\sean\Pictures\捕获.PNG

可以看出,第一个文件的文件名是乱码,这是因为它是 Shift-JIS 编码的,但是却尝试在 GBK 的 cmd 上打印出来,因此会报错。

总结: python2 中,如果在文件开头声明了某种文件编码格式,那么此文件中定义的字符串就是跟文件同样的编码格式。如果想要将此字符串打印到控制台上,则其编码必须跟控制台的编码一致。否则就需要手动进行 decode 成 unicode 进行打印。对于从系统中读进来的文件名,其编码格式是根据所在系统的编码格式决定的,与源码文件头部声明的编码格式无关。例如,以下代码中:

1
2
3
4
5
6
7
8
9
10
# -*- coding:utf-8 -*-
import glob
import os.path as osp

names = glob.glob(osp.join('C:\Users\sean\Pictures', '*.png'))
for name in names:
print name

s = '路飞学院'
print s

s 变量的编码就是头部声明的 utf-8,而 names 中每个元素的编码则是根据路径下每个文件的编码格式而异的。

值得注意的时,在 python2 中,字符串一共有两种类型,str 或 unicode。

1
2
3
# -*- coding: utf-8 -*-
s = '中文' # 注意这里的 str 是 str 类型的,而不是 unicode
s.encode('gb18030')

这句代码将 s 重新编码为 gb18030 的格式,即进行 unicode -> str 的转换。因为 s 本身就是 str 类型的,因此 Python 会自动的先将 s 解码为 unicode ,然后再编码成 gb18030。因为解码是 python 自动进行的,我们没有指明解码方式,python 就会使用 sys.defaultencoding 指明的方式来解码。很多情况下 sys.defaultencoding 是 ASCII,如果 s 不是这个类型就会出错。拿上面的情况来说,我的 sys.defaultencoding 是 ASCII,而 s 的编码方式和文件的编码方式一致,是 utf8 的,所以出错了:

1
2
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position
0: ordinal not in range(128)

此问题的解决方案有以下两种:
一是明确的指示出 s 的解码方式

1
2
3
4
> # -*- coding: utf-8 -*-

s = '中文'
s.decode('utf-8').encode('gb18030')

二是更改 sys.defaultencoding 为文件的编码方式

1
2
3
4
5
6
7
8
# -*- coding: utf-8 -*-

import sys
reload(sys) # Python2.5 初始化后会删除 sys.setdefaultencoding 这个方法,我们需要重新载入
sys.setdefaultencoding('utf-8')

str = '中文'
str.encode('gb18030')

注意: 在 pycharm 等 IDE 中,一般新建一个文件都是默认的 UTF-8 编码,而在文件中显式地声明一个字符串时(前面没加 u),此字符串采用的就是与文件同样的编码方式,即 UTF-8. 也就是说,当你在一个 python 脚本里直接定义一个字符串常量的时候,此字符串的编码方式和环境编码方式相同。注意,如果一个 str 变量是从外部传过来的,如 glob 函数返回的,则此变量的编码方式可能会和源文件编码方式不一样。


python 中的 str 和 unicode

简体中文版 Windows 系统,编码方式为 GBK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> a='你好'
>>> a
'\xc4\xe3\xba\xc3'
>>> b=u'你好'
>>> b
u'\u4f60\u597d'
>>> print a
你好
>>> print b
你好
>>> a.__class__
<type 'str'>
>>> b.__class__
<type 'unicode'>
>>> len(a)
4
>>> len(b)
2

由以上代码段可以看出,直接定义一个字符串常量的时候,即 str 的时候,其编码方式为 GBK,是环境的编码方式,

在一个系统编码为UTF-8的Linux环境下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> a = '你好'
>>> a
'/xe4/xbd/xa0/xe5/xa5/xbd'
>>> b = u'你好'
>>> b
u'/u4f60/u597d'
>>> print a
你好
>>> print b
你好
>>> a.__class__
<type 'str'>
>>> b.__class__
<type 'unicode'>
>>> len(a)
6
>>> len(b)
2

可以看出,在此 Linux 中,str 的编码方式为 utf-8,也是环境的编码方式。

len(string)返回string的字节数,len(unicode)返回的是字符数

print(string) 的时候,如果 string 是按当前环境编码方式编码的,可以正常输出,不会乱码;如果 string 不是当前环境编码的,就会乱码。而 print(unicode) 是不会乱码的。why?因为 print(unicode) 的时候,会把 unicode 先转成当前编码,然后再输出。我没看过 print 的源码,不过估计是这样的。

关于 如果 string 不是当前环境编码的,就会乱码。 这句,可由一下代码证实:
coding.py

1
2
3
# -*- coding:utf-8 -*-
s = '路飞学院'
print s

在使用 GBK 的 terminal 中,输出为:

1
2
C:\Users\sean\PycharmProjects\working>python coding.py
璺瀛﹂櫌

这是因为,s 使用文件编码方式 utf-8 进行编码,而 terminal 是 gbk 编码的,因此会出现乱码。若在声明中使用 gbk,则不会出现乱码(已亲测)。


python2 还是 python3

以上所说的都是针对 python2,python3 针对编码部分进行了改进。

1
2
3
# -*- coding:utf-8 -*-
s = '路飞学院'
print(s)

以上这段代码,在 python2 中执行结果为

1
2
C:\Users\sean\PycharmProjects\working>python coding.py
璺瀛﹂櫌

在 python3 中为:

1
2
C:\Users\sean\PycharmProjects\working>D:\Develop\Anaconda2\setup\envs\tensorflow\python coding.py
路飞学院

原因如下:

  utf-8 编码之所以能在 windows gbk 的终端下显示正常,是因为到了内存里 python 解释器把 utf-8 转成了 unicode, 但是这只是 python3, 并不是所有的编程语言在内存里默认编码都是 unicode。比如,万恶的 python2 就不是,它的默认编码是 ASCII,想写中文,就必须声明文件头的 coding 为 gbk 或 utf-8, 声明之后,python2 解释器仅以文件头声明的编码去解释你的代码,加载到内存后,并不会主动帮你转为 unicode,也就是说,你的文件编码是 utf-8,加载到内存里,你的变量字符串就也是 utf-8 编码的。如果需要显示在 gbk 的终端下,就只能使用 decode 或 encode 函数转换成 unicode 或者再使用 gbk 进行编码。


PY3 除了把字符串的编码改成了 unicode, 还把str 和 bytes 做了明确区分, str 就是 unicode 格式的字符, bytes 就是单纯二进制


python2 的字符串其实更应该称为字节串。通过存储方式就能看出来,但 python2 里还有一个类型是 bytes ,难道又叫 bytes 又叫字符串?是的,在 python2 里,bytes == str , 其实就是一回事。除此之外,python2 里还有个单独的类型 unicode , 把字符串解码后,就会变成 unicode。


总之,Python只要出现各种编码问题,无非是哪里的编码设置出错了
常见编码错误的原因有:

  • Python解释器的默认编码
  • Python源文件文件编码
  • Terminal使用的编码
  • 操作系统的语言设置

掌握了编码之前的关系后,挨个排错就好了。

这篇文章非常不错,可以参考了解。

参考文章

https://blog.csdn.net/ktb2007/article/details/3876436
https://blog.csdn.net/ktb2007/article/details/3876429
https://www.zhihu.com/question/31833164/answer/381137073
https://blog.csdn.net/abyjun/article/details/50190243
永久修改 cmd 代码页方法


文章作者: taosean
文章链接: https://taosean.github.io/2018/07/24/encoding-problem/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 taosean's 学习之旅