无和空字符串的 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', '']]
当然,我可以使用data
和data2
来区分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.Dialect
或csv.writer
和csv.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.reader
和csv.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.Dialect
或csv.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 阅读器行为的主要内容,如果未能解决你的问题,请参考以下文章