Python3中使用readlines()方法时出现UnicodeDecodeError错误

42

我正在尝试创建一个Twitter机器人,它可以读取行并发布它们。我使用Python3和tweepy,在我的共享服务器空间上通过virtualenv运行。这是似乎有问题的代码部分:

#!/foo/env/bin/python3

import re
import tweepy, time, sys

argfile = str(sys.argv[1])

filename=open(argfile, 'r')
f=filename.readlines()
filename.close()

我遇到的错误如下:

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

错误具体指向f = filename.readlines()作为错误的源头。有什么想法是可能出了什么问题?谢谢。

2
请查看此帖子,它有两个非常有用的回答,您应该尝试一下。 - Kevin
3
我使用了编码 encoding='iso-8859-1',它解决了我的问题。 - hsinghal
4
ISO-8859-1(也称为Latin-1)始终可用,但通常是错误的。问题在于它可以解码来自任何编码的任何字节,但如果原始文本实际上不是Latin-1,则会解码成垃圾。你需要知道真正的编码,而不仅仅是猜测;UTF-8大多数情况下都是自检的,因此不太可能解码二进制乱码,但是Latin-1会愉快地将二进制乱码解码为文本乱码,从不提出任何抱怨。 - ShadowRanger
1
@ShadowRanger,非常感谢您的解释。它对我的知识增加很大帮助。 - hsinghal
3个回答

69

我认为(在Python 3中)最好的答案是使用 errors= 参数:

with open('evil_unicode.txt', 'r', errors='replace') as f:
    lines = f.readlines()

证明:

>>> s = b'\xe5abc\nline2\nline3'
>>> with open('evil_unicode.txt','wb') as f:
...     f.write(s)
...
16
>>> with open('evil_unicode.txt', 'r') as f:
...     lines = f.readlines()
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/codecs.py", line 319, in decode
    (result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe5 in position 0: invalid continuation byte
>>> with open('evil_unicode.txt', 'r', errors='replace') as f:
...     lines = f.readlines()
...
>>> lines
['�abc\n', 'line2\n', 'line3']
>>>

请注���,errors= 可以是 replace 或者 ignore。这里是 ignore 的效果:
>>> with open('evil_unicode.txt', 'r', errors='ignore') as f:
...     lines = f.readlines()
...
>>> lines
['abc\n', 'line2\n', 'line3']

22
你的默认编码似乎是ASCII,而输入很可能是UTF-8。当输入中出现非ASCII字节时,就会抛出异常。并不是readlines本身导致了这个问题;相反,它引起了读取和解码的过程,并且解码失败。
不过这个问题很容易解决;在Python 3中,默认的open函数允许你提供已知的输入encoding,用其他任何被识别的编码替换默认的(在你的情况下是ASCII)。提供编码参数可以让你继续以str形式进行读取(而不是与之显著不同的原始二进制数据bytes对象),同时让Python完成从原始磁盘字节到真正的文本数据的转换工作。
# Using with statement closes the file for us without needing to remember to close
# explicitly, and closes even when exceptions occur
with open(argfile, encoding='utf-8') as inf:
    f = inf.readlines()

如果文件是其他编码方式,你需要将encoding='utf-8'更改为适当的参数。请注意,尽管有些人会告诉你在这里“只使用'latin-1'”如果'utf-8'不起作用:
  1. 这通常是错误的(现代文本编辑器倾向于生成UTF-8或UTF-16,而latin-1则较少见;坦率地说,你更有可能看到微软的'latin-1'变体'cp1252',它基本相同但重新映射了一些字符以支持智能引号等功能),以及
  2. 与UTF编码不同,各种每字节一个字符的ASCII超集编码(包括'latin-1''cp1252''cp437'和许多其他编码)没有自检功能;如果数据不符合指定的编码,它们仍然会愉快地解码,只是对ASCII范围之上的内容会产生乱码。
简而言之,如果您的数据不是UTF编码(或者是一种罕见的非UTF自检编码),您需要知道所使用的编码,否则您只能猜测并检查结果是否合理(对于可能是Latin-1或cp1252的源代码,除非最终包含一个特定于cp1252的字符,否则您永远无法确定)。

1
我喜欢这个解决方案的简洁性,但我刚在Python 3.6.8中尝试了一下,它失败了。 - M.H.
2
@M.H.:它将在UTF-8数据上工作。如果不是UTF-8,则需要弄清楚它是什么编码。这将在3.6.8上和任何其他3.x版本上同样有效(如果您使用“from io import open”来替换Py2的“open”为Py3版本,则也适用于Python 2.6+)。但是,如果您不知道编码,那么只能猜测。 - ShadowRanger
@r_e_cur:我拒绝了你的编辑,因为即使你的情况碰巧适用于latin-1,latin-1也是一个“陷阱”,不应该是任何人解决问题的首选(或第二个、第三个)尝试,除非他们毫无疑问地知道源数据实际上是以latin-1编码的。它会“处理”完全随机的字节、UTF-8字节和UTF-16字节;将它们全部解码为latin-1将得到一个字符串,但这个字符串将是垃圾。UTF-8是自检验的,因此如果数据不是真正的 UTF-8,任何有意义的数据量都会出错,这使得它成为更安全的选择。 - ShadowRanger
我确实添加了关于使用它的注释,但与其将其作为一个会被盲目复制粘贴的代码示例,我更倾向于写下一些关于为什么使用它以及何时可以使用它的注释。我强烈怀疑对于你来说,latin-1 是错误的选择,即使你说它能工作,因为在大多数西欧 Windows 系统中,cp1252(类似于 latin-1,但并非完全相同)是实际的默认区域编码(当数据未存储为 UTF-16 时,大多数 Windows 程序现在都使用 UTF-16),而在基本上所有非 Windows 系统(包括东亚以外的地区,甚至其中一些地区)中,默认的编码方式是 UTF-8。 - ShadowRanger
哦,嗯。看错了,不是 r_e_cur 提出的修改建议,而是一个“匿名用户”。我甚至没有意识到在 StackOverflow 上也有这样的事情。耸耸肩如果他们回来查看的话,我会保留这些评论。 - ShadowRanger

-1
最终我自己找到了一个可行的答案:
filename=open(argfile, 'rb')

这篇文章帮了我很多。


2
如果你真的在使用Python 3,这将会极大地改变你的行为;以二进制模式打开意味着你不仅不会得到行结束符转换(尽管这只是在Windows上的问题),而且你会得到bytes对象而不是str(如果你想要使用str,必须手动进行decode)。我发布了一个避免这个问题的答案(假设你知道编码,你需要知道编码才能执行decode)。 - ShadowRanger

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