Python UTF-16 CSV读取器

10

我有一个UTF-16格式的CSV文件需要读取,但是Python的csv模块好像不支持UTF-16。

我正在使用Python 2.7.2。这些CSV文件非常大,包含几GB的数据。

以下是对John Machin的问题的回答:

print repr(open('test.csv', 'rb').read(100))

使用仅包含 "abc" 内容的 test.csv 输出结果

'\xff\xfea\x00b\x00c\x00'

我认为这个csv文件是在美国的Windows机器上创建的。我正在使用Mac OSX Lion。

如果我使用phihag提供的代码和包含一条记录的test.csv进行测试。

使用的示例test.csv内容。以下是打印repr(open('test.csv','rb').read(1000))输出

'\xff\xfe1\x00,\x002\x00,\x00G\x00,\x00S\x00,\x00H\x00 \x00f\x00\xfc\x00r\x00 \x00e\x00 \x00\x96\x00 \x00m\x00 \x00\x85\x00,\x00,\x00I\x00\r\x00\n\x00'

由phihag编写的代码

import codecs
import csv
with open('test.csv','rb') as f:
      sr = codecs.StreamRecoder(f,codecs.getencoder('utf-8'),codecs.getdecoder('utf-8'),codecs.getreader('utf-16'),codecs.getwriter('utf-16'))      
      for row in csv.reader(sr):
         print row

上述代码的输出结果

['1', '2', 'G', 'S', 'H f\xc3\xbcr e \xc2\x96 m \xc2\x85']
['', '', 'I']

期望的输出是

['1', '2', 'G', 'S', 'H f\xc3\xbcr e \xc2\x96 m \xc2\x85','','I']
4个回答

34

目前,csv模块不支持UTF-16编码。

在Python 3.x中,csv期望一个文本模式的文件,您可以使用open函数的encoding参数来强制使用其他编码:

# Python 3.x only
import csv
with open('utf16.csv', 'r', encoding='utf16') as csvf:
    for line in csv.reader(csvf):
        print(line) # do something with the line

在Python 2.x中,您可以重新编码输入:

# Python 2.x only
import codecs
import csv

class Recoder(object):
    def __init__(self, stream, decoder, encoder, eol='\r\n'):
        self._stream = stream
        self._decoder = decoder if isinstance(decoder, codecs.IncrementalDecoder) else codecs.getincrementaldecoder(decoder)()
        self._encoder = encoder if isinstance(encoder, codecs.IncrementalEncoder) else codecs.getincrementalencoder(encoder)()
        self._buf = ''
        self._eol = eol
        self._reachedEof = False

    def read(self, size=None):
        r = self._stream.read(size)
        raw = self._decoder.decode(r, size is None)
        return self._encoder.encode(raw)

    def __iter__(self):
        return self

    def __next__(self):
        if self._reachedEof:
            raise StopIteration()
        while True:
            line,eol,rest = self._buf.partition(self._eol)
            if eol == self._eol:
                self._buf = rest
                return self._encoder.encode(line + eol)
            raw = self._stream.read(1024)
            if raw == '':
                self._decoder.decode(b'', True)
                self._reachedEof = True
                return self._encoder.encode(self._buf)
            self._buf += self._decoder.decode(raw)
    next = __next__

    def close(self):
        return self._stream.close()

with open('test.csv','rb') as f:
    sr = Recoder(f, 'utf-16', 'utf-8')

    for row in csv.reader(sr):
        print (row)

opencodecs.open 需要文件以 BOM 开头。如果没有(或者你使用的是 Python 2.x),你仍然可以在内存中进行转换,例如:

try:
    from io import BytesIO
except ImportError: # Python < 2.6
    from StringIO import StringIO as BytesIO
import csv
with open('utf16.csv', 'rb') as binf:
    c = binf.read().decode('utf-16').encode('utf-8')
for line in csv.reader(BytesIO(c)):
    print(line) # do something with the line

我如何知道文件是否以BOM开头?@phihag - venky
尝试使用@phihag csv reader的StreamReader选项时,有时候csv reader似乎只读取了记录的一部分。当我在vi中打开文件时,在它认为是记录结尾的行中看到<85>,但在<85>字符之后还有两个其他字段。看起来剩余的字段被视为下一条记录。 - venky
你能把演示文件上传到某个地方吗?如果没有它,我就无法重现这个问题。此外,当在演示文件上使用第二种方法时,它也失败了吗? - phihag
我可以给您发送CSV样本文件吗?@phihag - venky
第二种方法,即使用BytesIO,在我的样例文件中有效。它将文件加载到内存中,但在我的情况下我无法这样做(文件太大)。@phihag - venky
显示剩余2条评论

4

Python 2.x的csv模块文档中的示例展示了如何处理其他编码方式。


1
文档实际上是这样说的:"只要避免使用使用NULs的编码(如UTF-16),你就可以编写处理编码和解码的函数或类。" - Antony Hatchkins
@Antony,你看了最后一个例子吗?在将其传递给csv模块之前,它会重新编码为UTF-8的任何编码。 - Mark Tolonen
是的,这个问题只需要几行代码就可以解决,基本上与@phihag答案中的代码相同。不过我会明确引用示例 - 让读者的生活更轻松 :) 取消了踩的操作。 - Antony Hatchkins
这是在phihag的回答和友好的RTFM之外的补充。 - Mark Tolonen
好的补充 :) 糟糕编写的csv模块代码(utf16 不是那么可怕,它是 Excel 输出的默认之一),以及文档(最后一个示例处理 NUL 和 utf16 并不明显)都是因为 Guido 想让每个人都转移到 Python 3.x 吧。 - Antony Hatchkins

3
我强烈建议您将文件重新编码为UTF-8。在非BMP范围内没有Unicode字符的情况下,您可以利用UTF-16是一种固定长度编码的特点,从输入文件中读取固定长度块,而不必担心跨越块边界。
步骤1:确定您实际拥有的编码。检查文件的前几个字节: print repr(open('thefile.csv', 'rb').read(100))
四种可能的编码方式u'abc'
\xfe\xff\x00a\x00b\x00c -> utf_16
\xff\xfea\x00b\x00c\x00 -> utf_16
\x00a\x00b\x00c -> utf_16_be
a\x00b\x00c\x00 -> utf_16_le

如果您在此步骤中遇到任何问题,请编辑您的问题,包括上面print repr()的结果。

步骤2:以下是一个Python 2.X重新编码UTF-16 *至UTF-8的脚本:

import sys
infname, outfname, enc = sys.argv[1:4]
fi = open(infname, 'rb')
fo = open(outfname, 'wb')
BUFSIZ = 64 * 1024 * 1024
first = True
while 1:
    buf = fi.read(BUFSIZ)
    if not buf: break
    if first and enc == 'utf_16':
        bom = buf[:2]
        buf = buf[2:]
        enc = {'\xfe\xff': 'utf_16_be', '\xff\xfe': 'utf_16_le'}[bom]
        # KeyError means file doesn't start with a valid BOM
    first = False
    fo.write(buf.decode(enc).encode('utf8'))
fi.close()
fo.close()

其他事项:

您说您的文件太大,无法阅读整个文件并重新编码和重写,但是您可以在vi中打开它。请解释一下。

<85>被视为记录结束似乎有点令人担忧。看起来0x85被认为是NEL(C1控制码,NEWLINE)。很可能数据最初是在某种旧的单字节编码中编码的,其中0x85具有含义,但在错误的假设下已被转换为UTF-16,即原始编码为ISO-8859-1 aka latin1。文件的来源在哪里?IBM大型机?Windows / Unix / classic Mac?哪个国家,区域设置,语言?您显然认为<85>不是换行符;您认为它代表什么意思?

请随时将包含一些<85>内容的削减文件的副本发送到sjmachin at lexicon dot net

根据提供的1行样本数据进行更新

这证实了我的怀疑。阅读此文。以下是其中的一句话:

...... C1控制字符......很少直接使用,除了特定平台(如OpenVMS)之外。当它们出现在文件、网页、电子邮件等中,这些文件明显是在ISO-8859-n编码中时,它们的代码位置通常指代该位置上专有的、系统特定的编码(如Windows-1252或Apple Macintosh("MacRoman")字符集),使用提供给C1集表示的代码来代替单个8位字节提供额外的图形字符。

此代码:

s1 = '\xff\xfe1\x00,\x002\x00,\x00G\x00,\x00S\x00,\x00H\x00 \x00f\x00\xfc\x00r\x00 \x00e\x00 \x00\x96\x00 \x00m\x00 \x00\x85\x00,\x00,\x00I\x00\r\x00\n\x00'
s2 = s1.decode('utf16')
print 's2 repr:', repr(s2)
from unicodedata import name
from collections import Counter
non_ascii = Counter(c for c in s2 if c >= u'\x80')
print 'non_ascii:', non_ascii
for c in non_ascii:
    print "from: U+%04X %s" % (ord(c), name(c, "<no name>"))
    c2 = c.encode('latin1').decode('cp1252')
    print "to:   U+%04X %s" % (ord(c2), name(c2, "<no name>"))

s3 = u''.join(
    c.encode('latin1').decode('1252') if u'\x80' <= c < u'\xA0' else c
    for c in s2
    )
print 's3 repr:', repr(s3)
print 's3:', s3

以下是产生的结果(Python 2.7.2 IDLE,Windows 7):

s2 repr: u'1,2,G,S,H f\xfcr e \x96 m \x85,,I\r\n'
non_ascii: Counter({u'\x85': 1, u'\xfc': 1, u'\x96': 1})
from: U+0085 <no name>
to:   U+2026 HORIZONTAL ELLIPSIS
from: U+00FC LATIN SMALL LETTER U WITH DIAERESIS
to:   U+00FC LATIN SMALL LETTER U WITH DIAERESIS
from: U+0096 <no name>
to:   U+2013 EN DASH
s3 repr: u'1,2,G,S,H f\xfcr e \u2013 m \u2026,,I\r\n'
s3: 1,2,G,S,H für e – m …,,I

你认为下面哪种解释更合理:\x96是SPA i.e. 保护区域起点(用于块式终端)还是EN DASH?
看起来需要对更大的数据样本进行彻底分析。很乐意帮忙。

更新的问题更多细节 - venky

-1

只需像以下代码一样使用codecs.open打开您的文件

import codecs, csv

stream = codecs.open(<yourfile.csv>, encoding="utf-16")
reader = csv.reader(stream)

如果你正在处理文本,那么应该使用Unicode字符串来编写程序,这是你无论如何都应该做的


1
对于csv.reader(stream)中的记录:line抛出UnicodeEncodeError异常:'ascii'编解码器无法在位置77处编码字符u'\xed',该字符超出了128的范围。 - venky
这在Python 3.x中运行良好(尽管可以只写“open”而不是“codecs.open”),但在2.x中失败,因为“csv”尝试重新编码从流中读取的Unicode字符。 - phihag

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