一个真正有效的非ASCII CSV实现?
[更新] 感谢大家的回答和意见,但如果能提供一些可运行的代码就太好了。如果你能给出可以读取示例文件的代码,那你就是王者(或女王)。
[更新 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 个回答
你在代码里做错了,试图用 .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',这是因为它的实现方式比较特殊。
我不知道你是否已经试过这个,不过在官方Python文档的示例部分,你会找到一对类;UnicodeReader
和UnicodeWriter
。到目前为止,它们对我来说都很好用。
正确识别一个文件的编码方式似乎是个很难解决的问题。你可以在这个StackOverflow讨论中阅读相关内容。
你现在试图用一个解决方案来处理一个不同的问题。请注意这一点:
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
的作者。