无和空字符串的 CSV 阅读器行为

Posted

技术标签:

【中文标题】无和空字符串的 CSV 阅读器行为【英文标题】:CSV reader behavior with None and empty string 【发布时间】:2012-07-07 22:07:18 【问题描述】:

在使用 Python 的 csv 模块在 Python 数据结构和 csv 表示之间来回切换时,我想区分 None 和空字符串 ('')。

我的问题是当我跑步时:

import csv, cStringIO

data = [['NULL/None value',None],
        ['empty string','']]

f = cStringIO.StringIO()
csv.writer(f).writerows(data)

f = cStringIO.StringIO(f.getvalue())
data2 = [e for e in csv.reader(f)]

print "input : ", data
print "output: ", data2

我得到以下输出:

input :  [['NULL/None value', None], ['empty string', '']]
output:  [['NULL/None value', ''], ['empty string', '']]

当然,我可以使用datadata2 来区分None 和空字符串,例如:

data = [d if d!=None else 'None' for d in data]
data2 = [d if d!='None' else None for d in data2]

但这会部分削弱我对 csv 模块的兴趣(用 C 实现的快速反序列化/序列化,特别是在处理大型列表时)。

是否有csv.Dialectcsv.writercsv.reader 的参数可以让他们在这个用例中区分''None

如果没有,是否有兴趣对csv.writer 实施补丁以启用这种来回操作? (可能是 Dialect.None_translate_to 参数默认为 '' 以确保向后兼容。)

【问题讨论】:

【参考方案1】:

您至少可以通过创建您自己版本的单例 None 类/值来回避 csv 模块所做的事情:

from __future__ import print_function
import csv


class NONE(object):
    ''' None-like class. '''
    def __repr__(self): # Method csv.writer class uses to write values.
        return 'NONE'   # Unique string value to represent None.
    def __len__(self):  # Method called to determine length and truthiness.
        return 0

NONE = NONE()  # Singleton instance of the class.


if __name__ == '__main__':

    try:
        from cStringIO import StringIO  # Python 2.
    except ModuleNotFoundError:
        from io import StringIO  # Python 3.

    data = [['None value', None], ['NONE value', NONE], ['empty string', '']]
    f = StringIO()
    csv.writer(f).writerows(data)

    f = StringIO(f.getvalue())
    print(" input:", data)
    print("output:", [e for e in csv.reader(f)])

结果:

 input: [['None value', None], ['NONE value', NONE],   ['empty string', '']]
output: [['None value', ''],   ['NONE value', 'NONE'], ['empty string', '']]

使用NONE 而不是None 将保留足够的信息,以便您能够区分它和任何实际的空字符串数据值。

更好的选择……

您可以使用相同的方法来实现一对相对轻量级的csv.readercsv.writer“代理”类——这是必要的,因为您实际上不能子类化用C 编写的内置csv 类——不会引入大量开销(因为大部分处理仍将由底层内置执行)。这将使发生的事情完全透明,因为它全部封装在代理中。

from __future__ import print_function
import csv


class csvProxyBase(object): _NONE = '<None>'  # Unique value representing None.


class csvWriter(csvProxyBase):
    def __init__(self, csvfile, *args, **kwrags):
        self.writer = csv.writer(csvfile, *args, **kwrags)
    def writerow(self, row):
        self.writer.writerow([self._NONE if val is None else val for val in row])
    def writerows(self, rows):
        list(map(self.writerow, rows))


class csvReader(csvProxyBase):
    def __init__(self, csvfile, *args, **kwrags):
        self.reader = csv.reader(csvfile, *args, **kwrags)
    def __iter__(self):
        return self
    def __next__(self):
        return [None if val == self._NONE else val for val in next(self.reader)]
    next = __next__  # Python2.x compatibility.


if __name__ == '__main__':

    try:
        from cStringIO import StringIO  # Python 2.
    except ModuleNotFoundError:
        from io import StringIO  # Python 3.

    data = [['None value', None], ['empty string', '']]
    f = StringIO()
    csvWriter(f).writerows(data)

    f = StringIO(f.getvalue())
    print("input : ", data)
    print("ouput : ", [e for e in csvReader(f)])

结果:

 input: [['None value', None], ['empty string', '']]
output: [['None value', None], ['empty string', '']]

【讨论】:

第一个解决方案的变体为我解决了写入问题。使用返回空字符串的 repr 创建了一个类 NONE(int)。用 NONE 替换了所有 None 值(无论如何我都必须格式化我的数据,所以没有额外的工作)。然后使用 QUOTE_NONNUMERIC 创建 csv 写入器。这有点 hacky,但这意味着在输出文件中,您知道带引号的字段始终是字符串,而未引用的空字段始终是 None。 @trelltron:很聪明,但一个潜在的缺点是它需要使用您可能不想要的QUOTE_NONNUMERIC,并且有可能使文件更大.我的解决方案不需要使用任何特定的 csv 选项即可工作。另请注意,我的第二种选择不需要用任何东西替换所有 None 值。【参考方案2】:

The documentation 暗示你想要的东西是不可能的:

为了尽可能轻松地与实现 DB API 的模块进行交互,值 None 被写为空字符串。

这在writer 类的文档中,表明它适用于所有方言,并且是 csv 模块的固有限制。

我支持改变这一点(以及 csv 模块的各种其他限制),但人们可能希望将此类工作卸载到不同的库中,并保持 CSV 模块简单(或在至少尽可能简单)。

如果您需要更强大的文件读取功能,您可能希望查看 numpy、scipy 和 pandas 中的 CSV 读取功能,我记得它们有更多选择。

【讨论】:

是的,已确认:查看 Modules/_csv.c 中的 csv_writerow ( if (field == Py_None) ... )。没有办法区分 '' 和 None。真可惜,考虑到方言抽象,您本来希望有更多的灵活性。您提到了 csv 模块的其他限制,您介意详细说明吗(如果还有其他问题我真的应该开始查看其他 csv-reading 写作)? 我发现有时令人讨厌的一个限制是分隔符必须是单个字符。因此,您无法解析列由两个选项卡分隔的文件。就像您遇到的 None 一样,这很容易解决,但仍然很烦人。 另一个是模块内的硬编码ascii限制。【参考方案3】:

我认为仅仅用方言是不可能做你想做的事的,但你可以编写自己的 csv.reader/write 子类。另一方面,我仍然认为这对于这个用例来说太过分了。即使您想捕获的不仅仅是None,您也可能只想要str()

>>> data = [['NULL/None value',None],['empty string','']]
>>> i = cStringIO.StringIO()
>>> csv.writer(i).writerows(map(str,row) for row in data)
>>> print i.getvalue()
NULL/None value,None
empty string,

【讨论】:

【参考方案4】:

由于您可以控制序列化数据的使用者和创建者,因此请考虑使用支持这种区别的格式。

例子:

>>> import json
>>> json.dumps(['foo', '', None, 666])
'["foo", "", null, 666]'
>>>

【讨论】:

【参考方案5】:

正如其他人指出的那样,您不能通过csv.Dialectcsv.writer 和/或csv.reader 的参数真正做到这一点。然而,正如我在一条评论中所说,您可以通过有效继承后两者来实现它(您显然不能真正做到,因为它们是内置的)。 “子类”在写入时所做的只是拦截 None 值并将它们更改为唯一的字符串,并在读回它们时反转过程。这是一个完整的示例:

import csv, cStringIO
NULL = '<NULL>'  # something unlikely to ever appear as a regular value in your csv files

class MyCsvWriter(object):
    def __init__(self, *args, **kwrds):
        self.csv_writer = csv.writer(*args, **kwrds)

    def __getattr__(self, name):
        return getattr(self.csv_writer, name)

    def writerow(self, row):
        self.csv_writer.writerow([item if item is not None else NULL
                                      for item in row])
    def writerows(self, rows):
        for row in rows:
            self.writerow(row)

class MyCsvReader(object):
    def __init__(self, *args, **kwrds):
        self.csv_reader = csv.reader(*args, **kwrds)

    def __getattr__(self, name):
        return getattr(self.csv_reader, name)

    def __iter__(self):
        rows = iter(self.csv_reader)
        for row in rows:
            yield [item if item != NULL else None for item in row]

data = [['NULL/None value', None],
        ['empty string', '']]

f = cStringIO.StringIO()
MyCsvWriter(f).writerows(data)  # instead of csv.writer(f).writerows(data)

f = cStringIO.StringIO(f.getvalue())
data2 = [e for e in MyCsvReader(f)]  # instead of [e for e in csv.reader(f)]

print "input : ", data
print "ouput : ", data2

输出:

input :  [['NULL/None value', None], ['empty string', '']]
ouput :  [['NULL/None value', None], ['empty string', '']]

这有点冗长,可能会稍微减慢 csv 文件的读取和写入速度(因为它们是用 C/C++ 编写的),但这可能没什么区别,因为无论如何该进程都可能是低级 I/O 绑定的。

【讨论】:

【参考方案6】:

我也遇到这个问题,找到这个https://bugs.python.org/issue23041。

问题的解决方案:

子类 csv.DictWriter,使用字典作为元素类型,并让其 writerow 方法执行特定于应用程序的工作。 定义一个 writerow() 函数,该函数执行类似的操作(本质上是包装 csv.writerow())。

【讨论】:

【参考方案7】:

如上所述,这是csv 模块的限制。一个解决方案就是用简单的字典理解重写循环内的行,如下所示:

reader = csv.DictReader(csvfile)
for row in reader:
    # Interpret empty values as None (instead of '')
    row = k: v if v else None for k, v in row.items()
    :

【讨论】:

以上是关于无和空字符串的 CSV 阅读器行为的主要内容,如果未能解决你的问题,请参考以下文章

python pandas:过滤掉给定字段的空字符串或空字符串的记录

用于测试非空字符串和非空字符串的兼容 SQL

字符串子串的查找

PB中取字符串子串的函数是啥

字符串

PB中取字符串子串的函数是啥