Python csv.DictReader - 如何反转输出?

3 投票
3 回答
3435 浏览
提问于 2025-04-18 15:13

我想要改变文件读取的方式。我使用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 个回答

0

其实你并不需要把整个文件都读到内存里。

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在评论中指出的,如果你没有使用显式的fieldnamesDictReader需要一个字符串的可迭代对象,其中第一行是表头。但你可以通过单独读取第一行并用csv.reader传递它作为fieldnames来解决这个问题,或者甚至可以在反向读取的最后一行之前,通过itertools.chain把所有的第一行连接起来。

3

你需要把整个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'}
1

把内容读入一个列表并反转:

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)):

撰写回答