在工作中遇到了有关字符编码的一个问题,在解决之后记录一下。
在一个深度学习的目标检测任务中,需要将检测出的结果输出到一个 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 | C:\Users\sean\Pictures\01.嶥杫巗彫栰杫.png |
可以看出,第一个文件的文件名是乱码,这是因为它是 Shift-JIS 编码的,但是却尝试在 GBK 的 cmd 上打印出来,因此会报错。
总结: python2 中,如果在文件开头声明了某种文件编码格式,那么此文件中定义的字符串就是跟文件同样的编码格式。如果想要将此字符串打印到控制台上,则其编码必须跟控制台的编码一致。否则就需要手动进行 decode 成 unicode 进行打印。对于从系统中读进来的文件名,其编码格式是根据所在系统的编码格式决定的,与源码文件头部声明的编码格式无关。例如,以下代码中:
1 | # -*- coding:utf-8 -*- |
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 | UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position |
此问题的解决方案有以下两种:
一是明确的指示出 s 的解码方式
1
2
3
4 > # -*- coding: utf-8 -*-
s = '中文'
s.decode('utf-8').encode('gb18030')
二是更改
sys.defaultencoding
为文件的编码方式
1 | # -*- coding: utf-8 -*- |
注意: 在 pycharm 等 IDE 中,一般新建一个文件都是默认的 UTF-8 编码,而在文件中显式地声明一个字符串时(前面没加 u),此字符串采用的就是与文件同样的编码方式,即 UTF-8. 也就是说,当你在一个 python 脚本里直接定义一个字符串常量的时候,此字符串的编码方式和环境编码方式相同。注意,如果一个 str 变量是从外部传过来的,如 glob 函数返回的,则此变量的编码方式可能会和源文件编码方式不一样。
python 中的 str 和 unicode
简体中文版 Windows 系统,编码方式为 GBK
1 | '你好' a= |
由以上代码段可以看出,直接定义一个字符串常量的时候,即 str 的时候,其编码方式为 GBK,是环境的编码方式,
在一个系统编码为UTF-8的Linux环境下
1 | '你好' a = |
可以看出,在此 Linux 中,str 的编码方式为 utf-8,也是环境的编码方式。
len(string)返回string的字节数,len(unicode)返回的是字符数
print(string) 的时候,如果 string 是按当前环境编码方式编码的,可以正常输出,不会乱码;如果 string 不是当前环境编码的,就会乱码。而 print(unicode) 是不会乱码的。why?因为 print(unicode) 的时候,会把 unicode 先转成当前编码,然后再输出。我没看过 print 的源码,不过估计是这样的。
关于 如果 string 不是当前环境编码的,就会乱码。
这句,可由一下代码证实:
coding.py
1 | # -*- coding:utf-8 -*- |
在使用 GBK 的 terminal 中,输出为:
1 | C:\Users\sean\PycharmProjects\working>python coding.py |
这是因为,s 使用文件编码方式 utf-8 进行编码,而 terminal 是 gbk 编码的,因此会出现乱码。若在声明中使用 gbk,则不会出现乱码(已亲测)。
python2 还是 python3
以上所说的都是针对 python2,python3 针对编码部分进行了改进。
1 | # -*- coding:utf-8 -*- |
以上这段代码,在 python2 中执行结果为
1 | C:\Users\sean\PycharmProjects\working>python coding.py |
在 python3 中为:
1 | 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 代码页方法