Python csv.DictReader - 如何反转输出?
我想要改变文件读取的方式。我使用DictReader是因为我想把内容放进一个字典里。我想先读取文件的第一行,把它当作字典的键,然后再从文件的底部开始往上解析,类似于Linux中的“tac”命令。有没有简单的方法可以做到这一点?下面是我用来把文件读入字典并写入文件的代码...
reader = csv.DictReader(open(file_to_parse, 'r'), delimiter=',', quotechar='"')
for line in reader:
# ...
这段代码可以正常处理文件,但是……我需要它从文件的末尾开始读取。
换句话说,我想让它读取文件:
fruit, vegetables, cars
orange, carrot, ford
apple, celery, chevy
grape, corn, chrysler
并且能够返回:
{' cars': ' chrysler', ' vegetables': ' corn', 'fruit': 'grape'}
{' cars': ' chevy', ' vegetables': ' celery', 'fruit': 'apple'}
{' cars': ' ford', ' vegetables': ' carrot', 'fruit': 'orange'}
而不是:
{' cars': ' ford', ' vegetables': ' carrot', 'fruit': 'orange'}
{' cars': ' chevy', ' vegetables': ' celery', 'fruit': 'apple'}
{' cars': ' chrysler', ' vegetables': ' corn', 'fruit': 'grape'}
3 个回答
其实你并不需要把整个文件都读到内存里。
csv.DictReader
并不一定要一个文件,它只需要一个字符串的可迭代对象。
而且你可以以反向顺序读取文本文件,平均情况下只需线性时间,且占用的空间是固定的,开销也不算太大。这并不简单,但也不是特别难:
def reverse_lines(*args, **kwargs):
with open(*args, **kwargs) as f:
buf = ''
f.seek(0, io.SEEK_END)
while f.tell():
try:
f.seek(-1024, io.SEEK_CUR)
except OSError:
bufsize = f.tell()
f.seek(0, io.SEEK_SET)
newbuf = f.read(bufsize)
f.seek(0, io.SEEK_SET)
else:
newbuf = f.read(1024)
f.seek(-1024, io.SEEK_CUR)
buf = newbuf + buf
lines = buf.split('\n')
buf = lines.pop(0)
yield from reversed(lines)
yield buf
这个方法没有经过严格测试,它会去掉换行符(对于csv.DictReader
来说这样处理是可以的,但一般情况下就不太合适了),而且它没有针对一些特殊情况进行优化(比如对于特别长的行,它的效率会变得很低),并且需要Python 3.3版本,直到你关闭或释放迭代器,文件才会消失(最好是用上下文管理器来处理这个问题)——但如果你真的需要这个,我敢打赌你可以在ActiveState或者PyPI上找到没有这些问题的解决方案。
总之,对于中等大小的文件,我怀疑在几乎所有实际的文件系统上,把整个文件顺序读入内存,然后反向遍历列表会更快。但对于非常大的文件(尤其是那些你根本无法放进内存的文件),这个方法显然要好得多。
根据我快速测试的结果(代码见http://pastebin.com/Nst6WFwV),在我的电脑上,基本情况如下:
- 对于小于1000行的文件,速度要慢得多。
- 对于1K到1M行的文件,速度大约慢10%。
- 在30M行时速度交叉。
- 在500M行时速度快50%。
- 在1.5G行时速度快1300%。
- 在2.5G行时速度几乎快到无限(反向遍历列表会让我的机器陷入交换区的地狱,我得通过ssh杀掉进程,还得等几分钟才能恢复……)。
当然,具体情况会受到你电脑很多因素的影响。500M行72个字符的ASCII文件几乎占用了我机器一半的物理内存,这可能不是巧合。但如果是机械硬盘而不是SSD,你可能会发现reverse_lines
的性能损失更大(因为随机读取会比连续读取慢得多,磁盘的性能会更重要)。你的平台的内存分配和虚拟内存行为,甚至是局部性问题(比如在读取一行后几乎立即解析,而不是在它被交换出去再换回来后解析……)也可能会有影响。等等。
总之,教训是,如果你不期待至少有几千万行(或者在资源非常有限的机器上稍微少一点),那就别考虑这个了;保持简单就好。
* 正如Martijn Pieters在评论中指出的,如果你没有使用显式的fieldnames
,DictReader
需要一个字符串的可迭代对象,其中第一行是表头。但你可以通过单独读取第一行并用csv.reader
传递它作为fieldnames
来解决这个问题,或者甚至可以在反向读取的最后一行之前,通过itertools.chain
把所有的第一行连接起来。
你需要把整个CSV文件读入内存;你可以通过对读取器对象调用 list()
来实现:
with open(file_to_parse, 'rb') as inf:
reader = csv.DictReader(inf, skipinitialspace=True)
rows = list(reader)
for row in reversed(rows):
注意,我在这里把文件当作上下文管理器使用,这样可以确保文件在使用完后会被关闭。你还需要以二进制模式打开文件(把换行符的处理留给 csv
模块)。你传给 DictReader()
的其他配置都是默认的,所以我就省略了。
我把 skipinitialspace
设置为 True,因为根据你的示例输入和输出,你的分隔符后面确实有空格;这个选项可以去掉这些空格。
csv.DictReader()
对象会自动处理第一行,把它当作键来读取。
示例:
>>> import csv
>>> sample = '''\
... fruit, vegetables, cars
... orange, carrot, ford
... apple, celery, chevy
... grape, corn, chrysler
... '''.splitlines()
>>> reader = csv.DictReader(sample, skipinitialspace=True)
>>> rows = list(reader)
>>> for row in reversed(rows):
... print row
...
{'cars': 'chrysler', 'vegetables': 'corn', 'fruit': 'grape'}
{'cars': 'chevy', 'vegetables': 'celery', 'fruit': 'apple'}
{'cars': 'ford', 'vegetables': 'carrot', 'fruit': 'orange'}
把内容读入一个列表并反转:
lines = [x for x in reader]
for line in lines[::-1]:
print line
{' cars': ' chrysler', ' vegetables': ' corn', 'fruit': 'grape'}
{' cars': ' chevy', ' vegetables': ' celery', 'fruit': 'apple'}
{' cars': ' ford', ' vegetables': ' carrot', 'fruit': 'orange'}
或者像Martijn Pieters建议的那样:
for line in reversed(list(reader)):