Python UTF-16 CSV 读取器

10 投票
4 回答
21380 浏览
提问于 2025-04-17 12:30

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

我使用的是Python 2.7.2。需要解析的CSV文件非常大,数据量达到好几个GB。

以下是对John Machin提问的回答

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

这是一个名为test.csv的文件,里面只有abc作为内容的输出

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

我觉得这个CSV文件是在美国的Windows机器上创建的,而我现在使用的是Mac OSX Lion。

如果我使用phihag提供的代码,并且test.csv文件只包含一条记录。

下面是使用的test.csv文件内容。以下是执行print 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 个回答

3

我强烈建议你把你的文件重新编码成UTF-8格式。如果你的文件里没有BMP(基本多语言平面)以外的Unicode字符,你可以利用UTF-16是一种固定长度编码的特点,从你的输入文件中读取固定长度的块,而不用担心块的边界问题。

第一步:确定你文件的实际编码。查看文件的前几个字节:

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() 的结果。

第二步:下面是一个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控制码,换行符)。很有可能这些数据最初是用某种旧的单字节编码格式编码的,其中0x85有特定含义,但在错误假设原始编码是ISO-8859-1(也叫latin1)的情况下被转码为UTF-16。这个文件是从哪里来的?是IBM大型机?Windows/Unix/经典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,即受保护区域的开始(用于块导向终端。)
还是
EN DASH(短破折号)
?

看来需要对更大样本的数据进行深入分析。我很乐意提供帮助。

4

Python 2.x的csv模块文档里有一些示例,教你怎么处理其他编码方式。

34

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

在Python 3.x中,csv模块需要处理文本文件,你可以通过在open函数中使用编码参数来强制使用其他编码方式:

# 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

撰写回答