Python中Unicode (UTF-8)文件读写

414

我在理解如何读写文件(Python 2.4)方面遇到了一些困难。

# The string, which has an a-acute in it.
ss = u'Capit\xe1n'
ss8 = ss.encode('utf8')
repr(ss), repr(ss8)
("u'Capit\xe1n'", "'Capit\xc3\xa1n'")
print ss, ss8
print >> open('f1','w'), ss8

>>> file('f1').read()
'Capit\xc3\xa1n\n'

我在我的喜爱编辑器中输入了Capit\xc3\xa1n,并保存在文件f2中。

接着:

>>> open('f1').read()
'Capit\xc3\xa1n\n'
>>> open('f2').read()
'Capit\\xc3\\xa1n\n'
>>> open('f1').read().decode('utf8')
u'Capit\xe1n\n'
>>> open('f2').read().decode('utf8')
u'Capit\\xc3\\xa1n\n'

我到底哪里理解有误?显然我缺少某些重要的魔法(或好的想法)。该如何在文本文件中输入才能得到正确的转换?

我真正无法领会的是,如果从外部获取UTF-8表示时Python不能识别它,那么UTF-8表示的意义是什么。也许我应该只将字符串转储为JSON,然后使用它,因为它具有可转换为ASCII的表示!更重要的是,是否存在这个Unicode对象的ASCII表示,Python可以在从文件中读入时识别并解码?如果有,我该如何获得它?

>>> print simplejson.dumps(ss)
'"Capit\u00e1n"'
>>> print >> file('f3','w'), simplejson.dumps(ss)
>>> simplejson.load(open('f3'))
u'Capit\xe1n'

要理解的重要事情是,u'Capit\xe1n\n'是一个正确的结果,并且该字符串已经包含了你要寻找的特殊字符。它只是用转义序列表示。这里的实质问题与如何读写文件和指定编码实际上没有任何关系,因为代码已经正确地展示了如何做到这一点。 - Karl Knechtel
14个回答

876
与其操作.encode.decode方法,更好的做法是在打开文件时指定编码方式。Python 2.6新增了io 模块,提供了io.open函数,允许指定文件的encoding编码方式。假设文件采用UTF-8编码,我们可以这样使用:
>>> import io
>>> f = io.open("test", mode="r", encoding="utf-8")

然后f.read返回一个解码后的Unicode对象:

>>> f.read()
u'Capit\xe1l\n\n'

在3.x中,io.open函数是内置open函数的别名,支持encoding参数(2.x不支持)。

我们还可以使用标准库模块codecs中的open函数

>>> import codecs
>>> f = codecs.open("test", "r", "utf-8")
>>> f.read()
u'Capit\xe1l\n\n'

需要注意的是,这在混合使用read()readline()时可能会出现问题


Note, however, that this can cause problems when mixing read() and readline().

74
写文件时,使用 codecs.open(file, 'w', 'utf-8') 代替 open(file, 'w') 可以完美地解决问题。请注意,不要更改原始含义。 - Matt Connolly
6
codecs.open(...) 方法是否也完全符合 with open(...): 的写法,其中 with 关心在所有操作完成后关闭文件?似乎它无论如何都能正常工作。 - try-catch-finally
2
@try-catch-finally 是的。我一直使用 with codecs.open(...) as f: - Tim Swast
7
我希望我能够将这个点赞一百次。由于大量混合数据导致的编码问题让我苦恼了几天,阅读有关编码的文章让我感到头晕目眩,而这个答案就像沙漠中的一滴水一样宝贵。希望我早点看到它。 - Mike Girard
太棒了!我试图清理下游代码;我直接通过 io.open(filename,'r',encoding='utf-8') as file: 找到了问题的源头。 - Pat Grady
如果你的文件可能有BOM(在Python 2.7中有效),请使用encoding="utf-8-sig" - Perry

124
在符号表示法u'Capit\xe1n\n'中(在3.x中应该只是'Capit\xe1n\n',并且必须在3.0和3.1中),\xe1代表一个字符。 \x是转义序列,表示e1是十六进制的。
在文本编辑器中将Capit\xc3\xa1n写入文件意味着它实际上包含\xc3\xa1。这些是8个字节,代码会读取它们所有。我们可以通过显示结果来看到这一点:
# Python 3.x - reading the file as bytes rather than text,
# to ensure we see the raw data
>>> open('f2', 'rb').read()
b'Capit\\xc3\\xa1n\n'

# Python 2.x
>>> open('f2').read()
'Capit\\xc3\\xa1n\n'

相反,只需在编辑器中输入像á这样的字符,然后编辑器应该会处理转换为UTF-8并保存。

在2.x版本中,实际包含这些反斜杠转义序列的字符串可以使用string_escape编解码器进行解码:

# Python 2.x
>>> print 'Capit\\xc3\\xa1n\n'.decode('string_escape')
Capitán

结果是一个使用UTF-8编码的str,其中重音字符由原始字符串中写成\\xc3\\xa1的两个字节表示。要获得unicode结果,请再次使用UTF-8进行解码。

在3.x中,string_escape编解码器被替换为unicode_escape,严格执行只能从strbytes进行encode,从bytesstr进行decodeunicode_escape需要以bytes开头才能处理转义序列(反过来,它会将它们添加进去);然后它将把生成的\xc3\xa1视为字符转义而不是字节转义。因此,我们需要做更多的工作:

# Python 3.x
>>> 'Capit\\xc3\\xa1n\n'.encode('ascii').decode('unicode_escape').encode('latin-1').decode('utf-8')
'Capitán\n'

4
“那么,重点是什么?”这个问题的答案是“Mu”(因为Python可以读取UTF-8编码的文件)。至于你的第二个问题:\xc3不是ASCII字符集的一部分。也许你的意思是“8位编码”。你对Unicode和编码感到困惑,没关系,很多人都有这种情况。 - tzot
9
请尝试将此作为入门读物:http://www.joelonsoftware.com/articles/Unicode.html - tzot
注意:u'\xe1'是一个Unicode代码点U+00e1,可以使用1个或多个字节表示(在utf-8中为2个字节)。b'\xe1'是一个字节(数字225),它所代表的字母(如果有)取决于用于解码它的字符编码,例如,在cp1251中为бU+0431,在cp866中为сU+0441等。 - jfs
13
许多英国程序员说“只需使用ASCII”,但却没有意识到£符号并不是ASCII码,这一点令人惊讶。大多数人不知道ASCII码不等于本地码页(即Latin1码)。 - Danny Staple
针对您最后的问题,我在写入文件时遇到了一个错误信息:write() argument must be str, not bytes。这个问题在Python2和Python3.7中都存在。 - vi_ral
显示剩余2条评论

83

现在在Python3中,您只需要使用open(Filename, 'r', encoding='utf-8')来打开文件。

[2016年2月10日编辑以获取请求的澄清信息]

Python3添加了encoding参数到其open函数。以下关于open函数的信息来自于这里:https://docs.python.org/3/library/functions.html#open

open(file, mode='r', buffering=-1, 
      encoding=None, errors=None, newline=None, 
      closefd=True, opener=None)

编码是用于解码或编码文件的编码名称。这仅应在文本模式下使用。默认编码取决于平台(即 locale.getpreferredencoding() 返回的编码),但 Python 支持的任何文本编码都可以使用。请参见codecs 模块以获取支持的编码列表。

因此,通过将encoding ='utf-8'添加为打开函数的参数,文件读取和写入都以 utf8 进行(这也是现在 Python 中所有操作的默认编码)。


您能否详细阐述一下您的答案,并对您提供的解决方案进行更多描述? - abarisone
3
看起来在Python 2中可以使用codecs模块进行处理 - codecs.open('somefile', encoding='utf-8')。参考链接:https://dev59.com/p3VC5IYBdhLWcg3w51ry#147756。 - Taylor D. Edmiston

20

这适用于在Python 3.2中使用UTF-8编码读取文件:

import codecs
f = codecs.open('file_name.txt', 'r', 'UTF-8')
for line in f:
    print(line)

18

所以,我已经找到了我正在寻找的解决方案,它是:

print open('f2').read().decode('string-escape').decode("utf-8")

这里有一些非常特殊的编解码器可以派上用场。这种方式可以让你将Python中的UTF-8编码转换成ASCII文件,并使其能够读取Unicode格式。使用"string-escape"方式进行解码时,反斜杠不会被双倍处理。

这种方法可以实现我所想象的往返转换。


14
# -*- encoding: utf-8 -*-

# converting a unknown formatting file in utf-8

import codecs
import commands

file_location = "jumper.sub"
file_encoding = commands.getoutput('file -b --mime-encoding %s' % file_location)

file_stream = codecs.open(file_location, 'r', file_encoding)
file_output = codecs.open(file_location+"b", 'w', 'utf-8')

for l in file_stream:
    file_output.write(l)

file_stream.close()
file_output.close()

9

除了codecs.open()之外,io.open()可用于2.x和3.x中读写文本文件。例如:

import io

text = u'á'
encoding = 'utf8'

with io.open('data.txt', 'w', encoding=encoding, newline='\n') as fout:
    fout.write(text)

with io.open('data.txt', 'r', encoding=encoding, newline='\n') as fin:
    text2 = fin.read()

assert text == text2

1
+1 io比codecs好得多。 - personal_cloud
是的,使用io更好;但我像这样编写了with语句 with io.open('data.txt', 'w', 'utf-8') as file: 却出现了错误:TypeError: an integer is required。之后我改成了 with io.open('data.txt', 'w', encoding='utf-8') as file: 就可以了。 - Evan Hu

6

你最喜欢的文本编辑器并不知道\xc3\xa1应该是字符字面量,但它会将它们解释为文本。这就是为什么你在最后一行得到双反斜杠的原因——现在在你的文件中是一个真正的反斜杠+xc3等等。

如果你想在Python中读写编码文件,最好使用codecs模块。

在终端和应用程序之间粘贴文本很困难,因为你不知道哪个程序将使用哪种编码来解释你的文本。你可以尝试以下方法:

>>> s = file("f1").read()
>>> print unicode(s, "Latin-1")
Capitán

将此字符串粘贴到您的编辑器中,并确保它使用Latin-1存储。在假设剪贴板不会破坏该字符串的情况下,往返转换应该可以正常工作。


这是用于Python 2的内容。 - Nathan B

6
你遇到了编码的通病:如何确定文件所使用的编码方式?
答案是:除非文件格式提供了相应信息,否则你无法确定。例如,XML文件的开头会包含以下内容:
<?xml encoding="utf-8"?>

这个标题被精心选择,以便无论编码如何都可以读取。在您的情况下,没有这样的提示,因此您的编辑器和Python都不知道发生了什么。因此,您必须使用codecs模块,并使用codecs.open(path,mode,encoding)来提供Python中缺失的部分。

至于您的编辑器,您必须检查它是否提供了一些设置文件编码的方法。

UTF-8的目的是能够将21位字符(Unicode)编码为8位数据流(因为这是全世界所有计算机都能处理的唯一事物)。但由于大多数操作系统都是Unicode时代之前的,它们没有适合的工具将编码信息附加到硬盘上的文件中。

接下来的问题是Python中的表示方式。这在heikogerlach的评论中解释得非常清楚。您必须理解,您的控制台只能显示ASCII。为了显示Unicode或任何>= charcode 128的内容,它必须使用某种转义方式。在您的编辑器中,您不能输入转义后的显示字符串,而是要输入字符串的含义(在本例中,您必须输入umlaut并保存文件)。

话虽如此,您可以使用Python函数eval()将转义后的字符串转换为字符串:

>>> x = eval("'Capit\\xc3\\xa1n\\n'")
>>> x
'Capit\xc3\xa1n\n'
>>> x[5]
'\xc3'
>>> len(x[5])
1

如您所见,字符串"\xc3"已被转换为单个字符。这是一个8位字符串,采用UTF-8编码。要获取Unicode:

>>> x.decode('utf-8')
u'Capit\xe1n\n'

Gregg Lind问道:我认为这里有些内容缺失了:文件f2包含:十六进制:

0000000: 4361 7069 745c 7863 335c 7861 316e  Capit\xc3\xa1n
codecs.open('f2','rb', 'utf-8'),例如,将文件读取为单独的字符(预期)。有没有办法以ASCII编写文件,使之工作?
回答:这取决于您的意思是什么。 ASCII无法表示大于127的字符。因此,您需要某种方式来表明“下几个字符具有特殊含义”,这就是"\ x"序列所做的。它表示:下两个字符是一个字符的代码。“\ u”使用四个字符对Unicode进行编码,最高可达0xFFFF(65535),也可以实现相同效果。
因此,您无法直接将Unicode写入ASCII(因为ASCII根本不包含相同的字符)。您可以将其写为字符串转义符(如f2中所示);在这种情况下,文件可以表示为ASCII。或者您可以将其写为UTF-8,在这种情况下,您需要使用8位安全流。
您提供的解决方案使用decode('string-escape')确实有效,但您必须注意使用了多少内存:使用codecs.open()的三倍。
请记住,文件只是具有8位字节序列的序列。既没有字节也没有位具有含义。是您说“65表示‘A’”。由于\xc3\xa1应变为“à”,但计算机无法知道,因此必须通过指定编写文件时使用的编码来告诉它。

我认为这里有一些缺失的部分:文件f2包含:hex: 0000000: 4361 7069 745c 7863 335c 7861 316e 0a Capit\xc3\xa1n.例如,codecs.open('f2','rb','utf-8')会将它们全部读入单独的字符(预期的结果)。有没有办法以ASCII编码写入文件并能够正常工作? - Gregg Lind

6
为了读取Unicode字符串并将其发送到HTML,我执行了以下操作:

fileline.decode("utf-8").encode('ascii', 'xmlcharrefreplace')

适用于使用Python实现的HTTP服务器。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接