Python 编码错误在使用 UTF-8 字符串写入文件时发生

5
我正在开发一款Python 3 Tkinter应用程序(操作系统为Windows 10),其功能概述如下:
1. 读取多个文本文件,这些文件可能包含ascii、cp1252、utf-8或其他任何编码的数据。
2. 在“预览窗口”(Tkinter标签小部件)中显示任何一个文件的内容。
3. 将文件内容写入单个输出文件(每次打开以追加方式)。
对于第1点:我通过在二进制模式下打开和读取文件使文件阅读编码无关。要将数据转换为字符串,我使用一个循环,遍历一个“可能性”编码列表,并依次尝试每个编码(使用error = 'strict'),直到它找到一个不会抛出异常的编码。这一步骤已经实现。
对于第2点:一旦我有了解码后的字符串,我只需调用Tkinter Label的textvariableset()方法即可。这一步也已经实现。
对于第3点:我以通常的方式打开一个输出文件,并使用write()方法来写入已解码的字符串。当字符串解码为utf-8时,这种方法可以正常工作,但当它被解码为ascii或cp1252时,它会抛出一个异常:
'charmap' codec can't encode characters in position 0-3: character maps to <undefined>

我搜索了一下,发现了一些相似的问题,但似乎没有解决我的问题。有一些进一步的限制使得一些解决方案对我无法起作用:

A. 我可以通过将读入的数据保留为字节并将输出文件打开/写入为二进制来规避这个问题,但这会导致一些输入文件内容无法读取。

B. 虽然这个应用程序主要是为Python 3设计的,但我正在尝试使其与Python 2兼容——我们有一些使用较慢/较晚版本的人会使用它。(顺便说一下,当我在Python 2上运行应用程序时,它也会抛出异常,但是对cp1252数据和utf-8数据都是如此。)


为了说明问题,我创建了这个简化的测试脚本。(我的真实应用程序是一个更大的项目,也是我的公司的专有项目,因此不会公开发布!)

import tkinter as tk
import codecs

#Root window
root = tk.Tk()

#Widgets
ctrlViewFile1 = tk.StringVar()
ctrlViewFile2 = tk.StringVar()
ctrlViewFile3 = tk.StringVar()
lblViewFile1 = tk.Label(root, relief=tk.SUNKEN,
                        justify=tk.LEFT, anchor=tk.NW,
                        width=10, height=3,
                        textvariable=ctrlViewFile1)
lblViewFile2 = tk.Label(root, relief=tk.SUNKEN,
                        justify=tk.LEFT, anchor=tk.NW,
                        width=10, height=3,
                        textvariable=ctrlViewFile2)
lblViewFile3  = tk.Label(root, relief=tk.SUNKEN,
                         justify=tk.LEFT, anchor=tk.NW,
                         width=10, height=3,
                         textvariable=ctrlViewFile3)

#Layout
lblViewFile1.grid(row=0,column=0,padx=5,pady=5)
lblViewFile2.grid(row=1,column=0,padx=5,pady=5)
lblViewFile3.grid(row=2,column=0,padx=5,pady=5)

#Bytes read from "files" (ascii Az5, cp1252 European letters/punctuation, utf-8 Mandarin characters)
inBytes1 = b'\x41\x7a\x35'
inBytes2 = b'\xe0\xbf\xf6'
inBytes3 = b'\xef\xbb\xbf\xe6\x9c\xa8\xe5\x85\xb0\xe8\xbe\x9e'

#Decode
outString1 = codecs.decode(inBytes1,'ascii','strict')
outString2 = codecs.decode(inBytes2,'cp1252','strict')
outString3 = codecs.decode(inBytes3,'utf_8','strict')

#Assign stringvars
ctrlViewFile1.set(outString1)
ctrlViewFile2.set(outString2)
ctrlViewFile3.set(outString3)

#Write output files
try:
    with open('out1.txt','w') as outFile:
        outFile.write(outString1)
except Exception as e:
    print(inBytes1)
    print(str(e))

try:
    with open('out2.txt','w') as outFile:
        outFile.write(outString2)
except Exception as e:
    print(inBytes2)
    print(str(e))

try:
    with open('out3.txt','w') as outFile:
        outFile.write(outString3)
except Exception as e:
    print(inBytes3)
    print(str(e))

#Start GUI
tk.mainloop()

1
如果你读取字节并写入字节,那么你应该有一个原始文件的完全副本。当输出看起来损坏时,输入也不是看起来损坏了吗?在Windows上,你的编辑器可能无法识别UTF-8,并尝试将字节解释为CP-1252字符。 - lenz
编写Py2/3跨平台兼容代码,请查看http://python-future.org/。注意:您应该将`open(..., 'w')调用替换为io.open(..., 'w', encoding=...)`以实现Py2/3和跨平台兼容性。 - lenz
@lenz,输出文件不仅包含各个文件的内容,还包括应用程序插入的“固定”文本行。但你说得很对,可能是编辑器(记事本)在同一文件中使用多种编码时无法正常工作和协调。我会进一步检查的。 - JDM
@lenz,看起来io模块就是解决这个问题的方法。正如下面Mark Tolonen所提到的,显式编码为UTF-8可解决write()问题,而io模块支持2和3的编码。可以将其作为“官方”答案,我会接受它的。 - JDM
2个回答

10
  • 要写入任意Unicode字符到文件中
  • 需要兼容Python 2/3。
  • 使用open('out1.txt','w')存在以下问题:

    • 输出文本流使用默认编码打开,这个编码在你的平台上(显然是Windows)是CP-1252。这种编解码器仅支持Unicode的一个子集,例如缺少所有表情符号。
    • open函数在Python版本之间有很大差异。在Python 3中,它是io.open函数,提供了很多灵活性,如指定文本编码。在Python 2中,返回的文件句柄处理的是8位字符串而不是Unicode字符串(文本)。
    • 还存在一个可能您没有意识到的可移植性问题:IO的默认编码是依赖于平台的,即运行您的代码的人可能会根据操作系统和本地化看到不同的默认值。

    您可以通过使用io.open('out1.txt', 'w', encoding='utf8')来避免这些问题:

    • 使用支持所需字符的编码。使用检测到的输入编码应该可以工作,除非处理过程中出现了超出支持范围的字符。使用其中一种UTF编解码器始终可行,其中UTF-8是文本文件中最广泛使用的。请注意,某些Windows应用程序(例如记事本)倾向于不理解UTF-8。有一个支持写入带BOM的UTF-8的编解码器,使得Windows应用程序能够识别以UTF-8编码的文件,它就是utf-8-sig编解码器。当用于读取时,如果存在,则该编解码器还会从输入流中删除UTF-8 BOM签名。
    • io模块已被回溯到Python 2.7。这通常合格为Py2/3兼容,因为对于<= 2.6版本的支持已经结束相当长时间了。
    • 在打开文本文件时始终明确指定使用的编码。可能有某些情况下基于平台的默认编码是有意义的,但通常您需要掌控。

    附注: 您提到了一种简单的启发式方法来检测输入编解码器。 如果真的无法获取此信息,则应考虑使用chardet


    刚刚运行了一个测试,它完美地工作了。我可以获取已解码为ascii、cp1252或utf_8的字符串,然后将这些字符串成功地“写入”到使用io.open(<file name>,'a',encoding='utf_8')打开的文件中。谢谢! - JDM

    1

    要明确。您使用默认编码打开了写入文件。无论它是什么,它都不支持所有Unicode代码点。使用UTF-8编码打开文件,它支持所有Unicode代码点:

    import io
    with io.open('out3.txt','w',encoding='utf8') as outFile:
    

    不兼容Python 2:TypeError:'encoding'是此函数的无效关键字参数 - JDM
    1
    @JDM 你在问题中标记了 python-3.x。为了实现Python 2/3的兼容性,请使用 io.open。它与Python 3的 open 相同,但在Python 2.7中也可用。 - Mark Tolonen
    缺失的标记已添加。抱歉,我认为B项中的段落已经足够清晰。 - JDM

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