【问题标题】:Handling non-utf8 characters in csv in Python 3 vs Python 2在 Python 3 与 Python 2 中处理 csv 中的非 utf8 字符
【发布时间】:2016-02-14 03:21:36
【问题描述】:

我有以下代码读取 csv 文件(一些包含非 UTF8 字符)。它在 Python 2.7.x 中运行良好:

    encodings = {'ukprocessed.csv': 'utf8',
                 'usprocessed.csv': 'utf8',
                 'uyprocessed.csv': 'latin1',
                 'arprocessed.csv': 'latin1'}

    with codecs.open(filepath, 'r') as csvfile:
        reader = csv.reader(csvfile)
        for row in reader:
            row = [x.decode(encodings[filename]).encode('utf8') for x in row]

但是,在 Python 3.4.x 中,测试失败并出现各种错误:

  • AttributeError: 'str' 对象没有属性 'decode'
  • UnicodeDecodeError:“ascii”编解码器无法解码位置 1078 中的字节 0xf1:序数不在范围内 (128) 等等……

我尝试在打开的文件中指定 'encoding=',使用 'rb' 和其他一些东西作为字节打开,但我找不到适用于 Python 2 和 3 的解决方案。

有人对我如何解决这个问题有任何想法吗?

谢谢

【问题讨论】:

    标签: python python-2.7 csv python-3.x unicode


    【解决方案1】:

    狭隘地解决你的错误原因:

    在 Py3 中,每个 row 中的 x 值是 str(类似于 Py2 unicode)。在 Py2 中,strunicode 过于灵活,因为 str 既是文本数据类型又是二进制数据类型;它支持encode,有点像,假设str是ASCII,解码它,然后重新编码为选择的编码(这对于ASCII兼容的编解码器是没有意义的,因为遇到非ASCII时会出错)。并且为了对称性,unicode 类型中的 decode 也同样容易出错且毫无意义;它将encode 转换为ASCII(如果unicode 包含非ASCII 则会出错),然后在请求的编解码器中decode。这是各种误解、错误等的根源。

    在 Python 3 中,他们更好地拆分了类型:

    1. str 文本类型,并且只有 encode 方法(将逻辑字符转换为所述字符的特定二进制编码)
    2. bytes(和其他bytes-like 类型)表示二进制数据,并且只有decode 方法(从特定的二进制编码转换为逻辑字符)

    以便携的方式解决您的问题:

    您的代码要求“纯文本”类型支持decode(二进制->文本),正如我所指出的,Py2 在有限的意义上允许这样做,即使它通常是愚蠢的。 Py3 没有; decode-ing 逻辑文本到逻辑文本是无意义的,为了避免沉默的不当行为,Py3 不提供无效方法(Py2 将根据 unicode 对象的内容工作,然后在错误时失败;你'会认为你的代码是非英语友好的,那么如果你真的将它与非英语文本一起使用,它就会中断)。

    如果您需要完全的可移植性,编写必须处理非 ASCII 类型的csv 代码并非易事。问题来了:

    1. 在 Python 2 中,您必须将str(面向字节)编码为不包括嵌入的NULs,而不是unicode 的编码。 注意: 如果unicode 仅包含sys.getdefaultencoding() 返回的编码中的文本,则unicode 碰巧起作用,因为csv 使用启动时配置的值(通常是ascii)对其进行静默编码由site 模块使用sys.setdefaultencodingsite 调用后删除sys.setdefaultencoding,你不应该自己调整它;在csvunicode 强制转换回str 期间,当输入有任何不符合语言环境编码的内容时,它会中断。这也不仅仅是您的系统区域设置的问题。在我的系统上,使用LANG=en_US.latin-1LANG=en_US.utf-8,Python 仍然返回'ascii' 作为我的sys.getdefaultencoding()
    2. 在 Python 3 中,必须使用 str(面向文本,相当于 Py2 的 unicode

    通常,对于非csv 相关的情况,我建议使用io.open 来获得Py2.7 和Py3.x 之间的完全兼容性(以及比codecs.open 更好的性能/兼容性)纯文本-基于类型。但是io.open(以及codecs.open)在文本模式下在Py2上返回unicode(不能与csv一起使用,除非它可以在默认编码中表示,所以你会认为它可以工作,直到你喂它是默认编码无法处理的),以及 Py3 中的 str(很好);在二进制模式下,它在 Py2 上返回 str(如果没有嵌入 NULs 则很好,尽管它不是为你解码,所以你需要从 str 解码到 unicode,然后从 @ 编码回来Py3 中的 987654377@ 到 utf-8 str) 和 bytes(需要解码为 str)。太丑了。

    我能给出的最佳解决方案是使用io.open,但在特定步骤生成的迭代器周围添加一个依赖于版本的包装器,以确保迭代器的输出采用给定 Python 版本的适当形式 (utf-8在 Py2 中编码 str,在 Py3 中编码 str),为您提供一致的行为(并将版本检查限制为每个文件执行固定次数,而不是每行一次):

    import io
    import sys
    
    encodings = {'ukprocessed.csv': 'utf8',
                 'usprocessed.csv': 'utf8',
                 'uyprocessed.csv': 'latin1',
                 'arprocessed.csv': 'latin1'}
    
    # io.open in text mode will return unicode on Py2, str on Py3, decoded appropriately
    # newline='' prevents it from doing line ending conversions (which are csv's
    # responsibility)
    with io.open(filepath, encoding=encodings[filepath], newline='') as csvdata:
        if sys.version_info[0] == 2:
            # Lazily convert lines from unicode to utf-8 encoded str
            csvdata = (line.encode('utf-8') for line in csvdata)
        reader = csv.reader(csvdata)
        if sys.version_info[0] == 2:
            # Decode row values to unicode on Py2; they're already str in Py3
            reader = ([x.decode('utf-8') for x in row] for row in reader)
        for row in reader:
            # operate on row containing native text types as values that can
            # represent whole Unicode range (unicode on Py2, str on Py3)
            ...
    

    【讨论】:

    • 很棒的答案 - 非常有用的描述并且在 Python 2 和 3 中都有效。谢谢!
    • @Mike91:在你的代码中到处都是.encode()/.decode() 很容易出错。如果您需要单一来源的 Python 2/3 兼容代码,请使用 Unicode 并将差异隐藏在诸如 unicodecsv 之类的接口后面
    • @jfs:这就是为什么我使用生成器表达式将encodedecode 的使用限制为恰好两行与处理行的代码分开的预处理;它避免了持续的版本检查(版本检查执行固定次数,而不是每行或每个单元格一次),并且它在主循环之外完成(因此主循环可以无条件地使用已知的 Unicode 友好文本类型) .虽然unicodecsv 确实有效,但并不总是可以安装第 3 方库;一个只需要固定 4 行(可以分解出来)来兼容的解决方案还不错。
    • 毕竟,您可以将其分解为 if sys.version_info[0] == 2: unicodecsvreader = csv.readerelse: def unicodecsvreader(f, *args, **kwargs): return ([x.decode('utf-8') for x in row] for row in csv.reader((line.encode('utf-8') for line in f), *args, **kwargs)) 以供重复使用(理想情况下,在不受 SO 注释格式约束的实际代码中将其分解为几行),现在你对所有版本的使用都是一样的,而且很明显。
    【解决方案2】:

    csv 在 Python 2 和 3 之间不是很便携,但 unicodecsv 是:

    from __future__ import print_function
    import io
    import unicodecsv
    
    encodings = {'ukprocessed.csv': 'utf8',
                 'usprocessed.csv': 'utf8',
                 'uyprocessed.csv': 'latin1',
                 'arprocessed.csv': 'latin1'}
    
    for filepath,enc in encodings.items():
        with open(filepath, 'rb') as csvfile:
            reader = unicodecsv.reader(csvfile,encoding=enc)
            for row in reader:
                print(row)
    

    这是在 Python 2.7、3.3 和 3.5 上测试的。以rb 模式打开很重要,因为unicodecsv 使用类似于Python 2.7 的csv 模块的字节字符串。 Python 3.X 的 csv 模块直接使用 Unicode 字符串。

    【讨论】:

      【解决方案3】:

      codecs.open 包装器已经为您提供了解码功能。试试这个:

      with codecs.open(filepath, 'r', encoding = encodings[filename]) as csvfile:
              reader = csv.reader(csvfile)
              for row in reader:
                  #do what you need
      

      【讨论】:

      • 这将破坏输入文件包含无法用sys.getdefaultencoding()返回的编码表示的Unicode的瞬间; Py2 csv 正在默默地将 unicode 输入(codecs.open 产生的类型)转换为 str 使用该编码,并且该编码通常是 ascii (即使它是别的东西,这只是意味着你默默地接受输入可以用该编码表示,然后在输入包含不可能的值时中断)。如果您正在编写针对 Py2 和 Py3 的代码,这是错误的;如果它仅针对 Py3,则使用 codecs 是不好的形式。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-03-22
      • 2018-07-22
      • 1970-01-01
      • 2010-12-11
      • 2014-01-13
      相关资源
      最近更新 更多