帮我理解为什么Unicode在Python中有时只工作不顺利

12 投票
5 回答
7278 浏览
提问于 2025-04-16 15:55

这里有一个小程序:

#!/usr/bin/env python
# -*- encoding: utf-8 -*-

print('abcd kΩ ☠ °C √Hz µF ü ☃ ♥')  
print(u'abcd kΩ ☠ °C √Hz µF ü ☃ ♥')

在Ubuntu的Gnome终端中,IPython的表现和我预期的一样:

In [6]: run Unicodetest.py
abcd kΩ ☠ °C √Hz µF ü ☃ ♥
abcd kΩ ☠ °C √Hz µF ü ☃ ♥

如果我在trypython.org上输入这些命令,得到的结果也是一样的。

但是在codepad.org上,第二个命令却出错了:

abcd kΩ ☠ °C √Hz µF ü ☃ ♥
Traceback (most recent call last):
  Line 6, in <module>
    print(u'abcd kΩ ☠ °C √Hz µF ü ☃ ♥')
UnicodeEncodeError: 'ascii' codec can't encode character u'\u03a9' in position 6: ordinal not in range(128)

相反,在Windows的IDLE中,第一个命令的输出被搞乱了,但第二个命令没有报错:

>>>
abcd kΩ ☠ °C √Hz µF ü ☃ ♥
abcd kΩ ☠ °C √Hz µF ü ☃ ♥

在Windows的命令提示符下运行IPython,或者通过Python(x,y)的Console2版本,都会把第一个输出搞乱,并且对第二个命令报错:

In [9]: run Unicodetest.py
abcd kΩ ☠ °C √Hz µF ü ☃ ♥
ERROR: An unexpected error occurred while tokenizing input
The following traceback may be corrupted or invalid
The error message is: ('EOF in multi-line statement', (15, 0))

---------------------------------------------------------------------------
UnicodeEncodeError                        Traceback (most recent call last)

Desktop\Unicodetest.py in <module>()
      4 print('abcd kΩ ☠ °C √Hz µF ü ☃ ♥')
      5
----> 6 print(u'abcd kΩ ☠ °C √Hz µF ü ☃ ♥')
      7
      8

C:\Python27\lib\encodings\cp437.pyc in encode(self, input, errors)
     10
     11     def encode(self,input,errors='strict'):
---> 12         return codecs.charmap_encode(input,errors,encoding_map)
     13
     14     def decode(self,input,errors='strict'):

UnicodeEncodeError: 'charmap' codec can't encode character u'\u2620' in position 8: character maps to <undefined>
WARNING: Failure executing file: <Unicodetest.py>

在Python(x,y)的Spyder中运行IPython也是这样,不过情况有点不同:

In [8]: run Unicodetest.py
abcd kΩ ☠ °C √Hz µF ü ☃ ♥
------------------------------------------------------------
Traceback (most recent call last):
  File "Unicodetest.py", line 6, in <module>
    print(u'abcd kΩ ☠ °C √Hz µF ü ☃ ♥')
  File "C:\Python26\lib\encodings\cp1252.py", line 12, in encode
    return codecs.charmap_encode(input,errors,encoding_table)
UnicodeEncodeError: 'charmap' codec can't encode character u'\u03a9' in position 6: character maps to <undefined>

WARNING: Failure executing file: <Unicodetest.py>

(在sitecustomize.py中,Spyder根据本地模块的编码设置自己的SPYDER_ENCODING,在Windows 7上是cp1252。)

这是怎么回事?我的命令有错吗?为什么在某些平台上一个命令能工作,而在其他平台上另一个命令能工作?我该如何稳定地打印Unicode字符,而不出现崩溃或混乱的情况?

有没有一种Windows的终端,能像Ubuntu中的那样工作?看起来TCC-LE、Console2、Git Bash、PyCmd等都只是cmd.exe的包装,而不是替代品。有没有办法在IDLE使用的界面中运行IPython?

5 个回答

0

从Python输出Unicode到Windows控制台就是不太好使。Python无法被说服去使用Windows本身的编码方式,而Windows的编码是需要宽字符和UCS2格式的。

2

可能有两个原因:

  • Unicode的编码问题。你不能直接输出原始的Unicode,所以print需要想办法把它转换成控制台所需要的字节流(据我所知,它使用的是sys.stdout.encoding),这就引出了第二个原因。
  • 控制台的支持问题。Python并不能控制你的终端,所以如果终端输出的是UTF-8格式,而你的终端又期待其他格式,就会出现乱码。
12

在Python(以及大多数其他编程语言)中,输入输出是基于字节的。当你把一个字节字符串(在2.x中是str,在3.x中是bytes)写入文件时,这些字节会直接写入文件。而当你写入一个Unicode字符串(在2.x中是unicode,在3.x中是str)时,数据需要被编码成字节序列。

如果想更深入了解这个区别,可以查看《深入Python 3》中关于字符串的章节

print('abcd kΩ ☠ °C √Hz µF ü ☃ ♥')

在这里,字符串是一个字节字符串。因为你的源文件编码是UTF-8,所以这些字节是

'abcd k\xce\xa9 \xe2\x98\xa0 \xc2\xb0C \xe2\x88\x9aHz \xc2\xb5F \xc3\xbc \xe2\x98\x83 \xe2\x99\xa5'

print语句会把这些字节原封不动地写到控制台上。但是Windows控制台会把字节字符串当作“OEM”编码来解释,在美国,这种编码是437。所以你在屏幕上看到的字符串是

abcd kΩ ☠ °C √Hz µF ü ☃ ♥

在你的Ubuntu系统上,这不会造成问题,因为那里的默认控制台编码是UTF-8,所以源文件编码和控制台编码之间没有差异。

print(u'abcd kΩ ☠ °C √Hz µF ü ☃ ♥')

当打印一个Unicode字符串时,这个字符串必须被编码成字节。但这只有在你有支持这些字符的编码时才有效。而你并没有。

  • 默认的IBM437编码缺少字符☠☃♥
  • Spyder使用的windows-1252编码缺少字符Ω☠√☃♥

所以在这两种情况下,你在打印字符串时都会遇到UnicodeEncodeError的错误。

这是怎么回事呢?

Windows和Linux在支持Unicode方面采取了截然不同的方法。

最初,它们的工作方式几乎是一样的:每个地区都有自己特定语言的char编码(在Windows中称为“ANSI代码页”)。西方语言使用ISO-8859-1或windows-1252,俄语使用KOI8-R或windows-1251,等等。

当Windows NT在早期添加对Unicode的支持时(当时认为Unicode会使用16位字符),它是通过创建一个并行版本的API来实现的,这个版本使用wchar_t而不是char。例如,MessageBox函数被分成了两个函数:

int MessageBoxA(HWND hWnd, const char* lpText, const char* lpCaption, unsigned int uType);
int MessageBoxW(HWND hWnd, const wchar_t* lpText, const wchar_t* lpCaption, unsigned int uType);

以"W"开头的函数是真正的函数。以"A"开头的函数是为了与基于DOS的Windows保持向后兼容,主要是将它们的字符串参数转换为UTF-16,然后调用相应的"W"函数。

在Unix世界(特别是Plan 9)中,编写一个全新的POSIX API版本被认为不切实际,因此对Unicode的支持采取了不同的方法。现有的对CJK地区多字节编码的支持被用来实现现在称为UTF-8的新编码。

在类Unix系统上偏向使用UTF-8,而在Windows上偏向使用UTF-16,这在编写支持Unicode的跨平台代码时是个大麻烦。Python试图将这一点隐藏起来,但打印到控制台是Joel所说的“泄漏抽象”的一个例子。

撰写回答