一个真正有效的非ASCII CSV实现?

13 投票
4 回答
6750 浏览
提问于 2025-04-16 11:56

[更新] 感谢大家的回答和意见,但如果能提供一些可运行的代码就太好了。如果你能给出可以读取示例文件的代码,那你就是王者(或女王)。

[更新 2] 感谢大家的精彩回答和讨论。我需要做的是读取这些文件,解析它们,并把其中的一部分保存到Django模型实例中。我认为这意味着要把它们从原始编码转换为unicode,这样Django才能处理,对吧?

在Stackoverflow上已经有几个 关于非ASCII的Python CSV读取的问题,但那里的解决方案和Python文档中的方法在我尝试的输入文件上都不管用。

解决方案的核心似乎是对CSV读取器的输入使用encode('utf-8'),对读取器的输出使用unicode(item, 'utf-8')。但是,这样会遇到UnicodeDecodeError的问题(见上面的提问):

UnicodeDecodeError: 'utf8' codec can't decode byte 0xa3 in position 8: unexpected

输入文件不一定是utf8格式的;它可以是ISO-8859-1、cp1251,或者其他任何格式。

所以,问题是:在Python中,有什么可靠且支持多种编码的方式来读取CSV文件吗?

问题的根源似乎是CSV模块是一个C扩展;有没有纯Python的CSV读取模块?

如果没有,是否有办法准确检测输入文件的编码,以便进行处理?

基本上,我在寻找一种万无一失的方法来读取(并希望能写入)任何编码的CSV文件。

这里有两个示例文件:欧洲俄罗斯

这是推荐的解决方案失败的情况:

Python 2.6.4 (r264:75821M, Oct 27 2009, 19:48:32)
[GCC 4.0.1 (Apple Inc. build 5493)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import csv
>>> def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, **kwargs):
...     # csv.py doesn't do Unicode; encode temporarily as UTF-8:
...     csv_reader = csv.reader(utf_8_encoder(unicode_csv_data),
...                             dialect=dialect, **kwargs)
...     for row in csv_reader:
...         # decode UTF-8 back to Unicode, cell by cell:
...         yield [unicode(cell, 'utf-8') for cell in row]
...
>>> def utf_8_encoder(unicode_csv_data):
...     for line in unicode_csv_data:
...         yield line.encode('utf-8')
...
>>> r = unicode_csv_reader(file('sample-euro.csv').read().split('\n'))
>>> line = r.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in unicode_csv_reader
  File "<stdin>", line 3, in utf_8_encoder
UnicodeDecodeError: 'ascii' codec can't decode byte 0xf8 in position 14: ordinal not in range(128)
>>> r = unicode_csv_reader(file('sample-russian.csv').read().split('\n'))
>>> line = r.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in unicode_csv_reader
  File "<stdin>", line 3, in utf_8_encoder
UnicodeDecodeError: 'ascii' codec can't decode byte 0xa0 in position 28: ordinal not in range(128)

4 个回答

0

你在代码里做错了,试图用 .encode('utf-8'),其实应该是解码,而不是编码。顺便说一下,unicode(bytestr, 'utf-8')bytestr.decode('utf-8') 是一样的。

但是最重要的是,你为什么要解码这些字符串呢?

听起来有点奇怪,其实你可以直接处理这些CSV文件,而不需要在意它们是cp1251、cp1252还是utf-8。最妙的是,区域字符的编码值都是大于0x7F,而utf-8也用大于0x7F的字符序列来表示非ASCII符号。

因为CSV文件中用到的分隔符(无论是逗号、分号还是换行符)都是ASCII字符,所以它的工作不会受到编码方式的影响(只要是单字节编码或utf-8就可以!)。

需要注意的是,在Python 2.x中使用 csv 模块时,打开文件要用 binary 模式,也就是'rb'或'wb',这是因为它的实现方式比较特殊。

2

我不知道你是否已经试过这个,不过在官方Python文档的示例部分,你会找到一对类;UnicodeReaderUnicodeWriter。到目前为止,它们对我来说都很好用。

正确识别一个文件的编码方式似乎是个很难解决的问题。你可以在这个StackOverflow讨论中阅读相关内容。

16

你现在试图用一个解决方案来处理一个不同的问题。请注意这一点:

def utf_8_encoder(unicode_csv_data)

你传入的是str对象。

读取你的非ASCII CSV文件时遇到的问题是,你不知道文件的编码方式,也不知道分隔符是什么。如果你知道编码方式(并且是基于ASCII的编码,比如cp125x、任何东亚编码、UTF-8,不是UTF-16,不是UTF-32),以及分隔符,这样就可以正常工作:

for row in csv.reader("foo.csv", delimiter=known_delimiter):
   row = [item.decode(encoding) for item in row]

你的sample_euro.csv看起来是用cp1252编码,分隔符是逗号。而俄罗斯的那个文件看起来是用cp1251编码,分隔符是分号。顺便提一下,从内容来看,你还需要确定使用的日期格式,可能还要确定货币格式——俄罗斯的例子中,金额后面跟着一个空格和“卢布”的西里尔字母缩写。

请注意:要抵制所有让你相信你的文件是用ISO-8859-1编码的说法。它们实际上是用cp1252编码的。

更新 针对评论“如果我理解你的意思,我必须知道编码才能让这个工作?一般情况下我不知道编码,而根据其他答案,猜测编码是非常困难的,所以我运气不好?”

你必须知道编码,才能让任何文件读取操作正常工作。

总是正确猜测任何编码的文件的编码并不难——这几乎是不可能的。然而,如果把范围限制在从Excel或Open Office以用户本地默认编码保存的CSV文件,并且文件大小合理,那就不是一件大事。我建议你试试chardet;它能为你的欧元文件猜测出windows-1252,为你的俄罗斯文件猜测出windows-1251——考虑到文件很小,这真是个了不起的成就。

更新 2 针对“工作代码会非常受欢迎”

工作代码(Python 2.x):

from chardet.universaldetector import UniversalDetector
chardet_detector = UniversalDetector()

def charset_detect(f, chunk_size=4096):
    global chardet_detector
    chardet_detector.reset()
    while 1:
        chunk = f.read(chunk_size)
        if not chunk: break
        chardet_detector.feed(chunk)
        if chardet_detector.done: break
    chardet_detector.close()
    return chardet_detector.result

# Exercise for the reader: replace the above with a class

import csv    
import sys
from pprint import pprint

pathname = sys.argv[1]
delim = sys.argv[2] # allegedly known
print "delim=%r pathname=%r" % (delim, pathname)

with open(pathname, 'rb') as f:
    cd_result = charset_detect(f)
    encoding = cd_result['encoding']
    confidence = cd_result['confidence']
    print "chardet: encoding=%s confidence=%.3f" % (encoding, confidence)
    # insert actions contingent on encoding and confidence here
    f.seek(0)
    csv_reader = csv.reader(f, delimiter=delim)
    for bytes_row in csv_reader:
        unicode_row = [x.decode(encoding) for x in bytes_row]
        pprint(unicode_row)

输出 1:

delim=',' pathname='sample-euro.csv'
chardet: encoding=windows-1252 confidence=0.500
[u'31-01-11',
 u'Overf\xf8rsel utland',
 u'UTLBET; ID 9710032001647082',
 u'1990.00',
 u'']
[u'31-01-11',
 u'Overf\xf8ring',
 u'OVERF\xd8RING MELLOM EGNE KONTI',
 u'5750.00',
 u';']

输出 2:

delim=';' pathname='sample-russian.csv'
chardet: encoding=windows-1251 confidence=0.602
[u'-',
 u'04.02.2011 23:20',
 u'300,00\xa0\u0440\u0443\u0431.',
 u'',
 u'\u041c\u0422\u0421',
 u'']
[u'-',
 u'04.02.2011 23:15',
 u'450,00\xa0\u0440\u0443\u0431.',
 u'',
 u'\u041e\u043f\u043b\u0430\u0442\u0430 Interzet',
 u'']
[u'-',
 u'13.01.2011 02:05',
 u'100,00\xa0\u0440\u0443\u0431.',
 u'',
 u'\u041c\u0422\u0421 kolombina',
 u'']

更新 3 这些文件的来源是什么?如果它们是从Excel或OpenOffice Calc或Gnumeric中“另存为CSV”,你可以通过将它们保存为“Excel 97-2003工作簿(*.xls)”来避免整个编码的问题,然后使用xlrd来读取它们。这也可以省去检查每个CSV文件以确定分隔符(逗号还是分号)、日期格式(31-01-11还是04.02.2011)和“小数点”(5750.00还是450,00)等麻烦——所有这些差异显然都是因为保存为CSV而产生的。 [免责声明]:我是xlrd的作者。

撰写回答