Python 文件较大时出现CSV文件问题

Python 文件较大时出现CSV文件问题,python,file,csv,sorting,Python,File,Csv,Sorting,我有一个大的tsv文件(~2.5Gb)。我迭代每一行,其中该行有6个选项卡。我获取每行的第一个选项卡,并将该行附加到基于第一个选项卡的csv文件中。目标是以基于主tsv行的csv文件排序的文件结束 这适用于小规模文件,但当我在大文件上运行时,IPython控制台永远不会结束。我保存到的文件看起来好像正在填充,但当我打开它时,没有显示任何内容 import csv file_path = ".../master.tsv" with open(file_path, 'r') as masterf

我有一个大的tsv文件(~2.5Gb)。我迭代每一行,其中该行有6个选项卡。我获取每行的第一个选项卡,并将该行附加到基于第一个选项卡的csv文件中。目标是以基于主tsv行的csv文件排序的文件结束


这适用于小规模文件,但当我在大文件上运行时,IPython控制台永远不会结束。我保存到的文件看起来好像正在填充,但当我打开它时,没有显示任何内容

import csv

file_path = ".../master.tsv"

with open(file_path, 'r') as masterfile:
    for line in masterfile:
        line_split = line.split("|")
        cik = line_split[0].zfill(10)

        save_path = ".../data-sorted/"
        save_path += cik + ".csv"

        with open(save_path, 'a') as savefile:
            wr = csv.writer(savefile, quoting=csv.QUOTE_ALL)
            wr.writerow(line_split)

您的代码非常低效,因为它会为它处理的输入文件的每一行打开并附加数据,如果输入文件如此庞大,这将需要大量的时间(因为这样做所需的操作系统调用相对较慢)

另外,我注意到您的代码中至少有一个bug,即行:

save_path += cik + ".csv"
这使得
save_path
越来越长……这不是需要的

不管怎么说,这里有一些东西应该工作得更快,尽管处理这么大的文件可能仍然需要相当长的时间。它通过缓存中间结果来加快进程。它仅通过打开不同的输出csv文件并尽可能不频繁地创建相应的
csv.writer
对象来实现这一点,第一次需要这些对象,并且仅在缓存达到其最大长度而关闭它们时才再次创建

请注意,缓存本身可能会消耗大量内存,具体取决于有多少个唯一的csv输出文件以及可以同时打开多少个文件,但使用大量内存是使其运行更快的原因。您需要反复调整并手动调整
MAX_OPEN
值,以找到速度和内存使用之间的最佳平衡,同时保持在操作系统允许一次打开多少文件的限制之下

还请注意,通过更智能地选择要关闭的现有文件条目,而不是随机选择(打开的)条目,可能会使它的工作更加高效。然而,这样做是否真的有用取决于输入文件中的数据是否有任何有利的分组或其他顺序

import csv
import os
import random

class CSVWriterCache(dict):
    """ Dict subclass to cache pairs of csv files and associated
        csv.writers. When a specified maximum number of them already
        exist, a random one closed, but an entry for it is retained
        and marked "closed" so it can be re-opened in append mode
        later if it's ever referenced again. This limits the number of
        files open at any given time.
    """
    _CLOSED = None  # Marker to indicate that file has seen before.

    def __init__(self, max_open, **kwargs):
        self.max_open = max_open
        self.cur_open = 0  # Number of currently opened csv files.
        self.csv_kwargs = kwargs  # keyword args for csv.writer.

    # Adding the next two non-dict special methods makes the class a
    # context manager which allows it to be used in "with" statements
    # to do automatic clean-up.
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

    def __getitem__(self, k):
        if k not in self:
            return self.__missing__(k)
        else:
            try:
                csv_writer, csv_file = self.get(k)
            except TypeError:  # Needs to be re-opened in append mode.
                csv_file = open(k, 'a', newline='')
                csv_writer = csv.writer(csv_file, **self.csv_kwargs)

            return csv_writer, csv_file

    def __missing__(self, csv_file_path):
        """ Create a csv.writer corresponding to the file path and add it
            and the file to the cache.
        """
        if self.cur_open == self.max_open:  # Limit?
            # Randomly choose a cached entry with a previously seen
            # file path that is still open (not _CLOSED). The associated
            # file is then closed, but the entry for the file path is
            # left in the dictionary so it can be recognized as having
            # been seen before and be re-opened in append mode.
            while True:
                rand_entry = random.choice(tuple(self.keys()))
                if self[rand_entry] is not self._CLOSED:
                    break
            csv_writer, csv_file = self[rand_entry]
            csv_file.close()
            self.cur_open -= 1
            self[rand_entry] = self._CLOSED  # Mark as previous seen but closed.

        csv_file = open(csv_file_path, 'w', newline='')
        csv_writer = csv.writer(csv_file, **self.csv_kwargs)
        self.cur_open += 1

        # Add pair to cache.
        super().__setitem__(csv_file_path, (csv_writer, csv_file))
        return csv_writer, csv_file

    # Added, non-standard dict method.
    def close(self):
        """ Close all the opened files in the cache and clear it out. """
        for key, entry in self.items():
            if entry is not self._CLOSED:
                entry[1].close()
                self[key] = self._CLOSED  # Not strictly necessary.
                self.cur_open -= 1  # For sanity check at end.
        self.clear()
        assert(self.cur_open == 0)  # Sanity check.

if __name__ == '__main__':
    file_path = "./master.tsv"
    save_path = "./data-sorted"
    MAX_OPEN  = 1000  # Number of opened files allowed (max is OS-dependent).
#    MAX_OPEN  = 2  # Use small value for testing.

    # Create output directory if it does not exist.
    if os.path.exists(save_path):
        if not os.path.isdir(save_path):
            raise RuntimeError("Path {!r} exists, but isn't a directory")
    else:
        print('Creating directory: {!r}'.format(save_path))
        os.makedirs(save_path)

    # Process the input file using a cache of csv.writers.
    with open(file_path, 'r') as masterfile, \
         CSVWriterCache(MAX_OPEN, quoting=csv.QUOTE_ALL) as csv_writer_cache:
        for line in masterfile:
            line_split = line.rstrip().split("|")
            cik = line_split[0].zfill(10)

            save_file_path = os.path.join(save_path, cik + ".csv")
            writer = csv_writer_cache[save_file_path][0]
            writer.writerow(line_split)

    print('{!r} file processing completed'.format(os.path.basename(file_path)))

假设您有足够的RAM,您最好在内存中对文件进行排序,例如将其放入字典,然后一次性写入磁盘。如果I/O确实是您的瓶颈,那么您只需打开一次输出文件就可以获得很多好处

from collections import defaultdict
from os.path import join

file_path = ".../master.tsv"

data = collections.defaultdict(list)
with open(file_path, 'r') as masterfile:
    for line in masterfile:
        cik = line.split("|", 1)[0].zfill(10)
        data[cik].append(line)

for cik, lines in data.items():
    save_path = join(".../data-sorted", cik + ".csv")

    with open(save_path, 'w') as savefile:
        wr = csv.writer(savefile, quoting=csv.QUOTE_ALL)
        for line in lines:
            wr.writerow(line.split("|"))
您可能没有足够的内存来加载整个文件。在这种情况下,您可以将其转储为块,如果块足够大,最终仍会为您节省大量I/O。下面的块处理方法非常快速和肮脏

from collections import defaultdict
from itertools import groupby
from os.path import join

chunk_size = 10000  # units of lines

file_path = ".../master.tsv"

with open(file_path, 'r') as masterfile:
    for _, chunk in groupby(enumerate(masterfile),
                            key=lambda item: item[0] // chunk_size):
        data = defaultdict(list)
        for line in chunk:
            cik = line.split("|", 1)[0].zfill(10)
            data[cik].append(line)
        for cik, lines in data.items():
            save_path = join(".../data-sorted", cik + ".csv")

            with open(save_path, 'a') as savefile:
                wr = csv.writer(savefile, quoting=csv.QUOTE_ALL)
                for line in lines:
                    wr.writerow(line.split("|"))

@他们不应该犯错误。表示文件iterable使用缓冲I/O,因此不会将所有内容加载到内存中。这段代码可能非常慢,因为它在每次迭代时都必须打开和关闭输出文件。@InCorrigible1 IPython是一个库,而不是Python的不同实现。“我保存的文件看起来好像正在填充”这意味着什么?看起来每次迭代都要保存到不同的文件中,因为您使用行分割的第一个元素来确定保存路径;然而,我得到:“[Errno 23]系统中打开的文件太多了:”我尝试了,但即使在ulimit-n 10000时,我也出现了相同的错误。user2544427:我认为链接解决方案在这种情况下并不适用,因为您没有使用
子进程
。这个答案中使用的缓存方法是在处理完整个输入文件之前保持所有文件的打开状态,这显然超出了操作系统的处理能力。我认为有可能使缓存“更智能”,并将其实际存储的数量限制在某个最大值。如果有机会的话,我会继续努力,并相应地更新我的答案。您也可以自己尝试这样做……user2544427:好的,我已经更新了答案,所以cache类现在有一个长度限制参数,可以控制一次允许打开多少文件。我还使它在
语句中可用,这使清理自动进行。user2544427:我的上一次更新无法正常工作。我一直在用我手动创建的一个相对较小的输入文件测试它,但现在一个更大的文件导致“打开的文件太多”异常发生,并且当前发布的版本无法正确处理它。我会继续努力,并尝试解决这个问题。嗯,除了代码中的一些小的排版错误外,我一直在
key=lambda item:item[0]//chunk\u size)
调用的
groupby()
部分上遇到一个
SyntaxError:invalid syntax
,我不知道如何解决。我不完全确定,因为在我看来这没问题,但我认为这可能是由于Python解释器本身的一个bug(或者可能只是因为我自己对语言解析的理解)。我会调查的。如果你愿意的话,请随意更正错误。我目前在手机上,所以我的代码未经测试。@martineau。for循环末尾缺少一个冒号。现在修好了。啊,修好了那部分。至于修复其他打字错误,我将把它留给您,因为在我尝试修复它们之后,会出现一些不相关的问题。顺便说一句,这两种方法看起来都很有希望。谢谢我已经修复了一些缺失的部分,明天睡觉后将测试其余部分。