将多个 csv 文件连接成一个具有相同标题的 csv

新手上路,请多包涵

我目前正在使用以下代码导入 6,000 个 csv 文件(带标题)并将它们导出到单个 csv 文件(带单个标题行)。

 #import csv files from folder
path =r'data/US/market/merged_data'
allFiles = glob.glob(path + "/*.csv")
stockstats_data = pd.DataFrame()
list_ = []

for file_ in allFiles:
    df = pd.read_csv(file_,index_col=None,)
    list_.append(df)
    stockstats_data = pd.concat(list_)
    print(file_ + " has been imported.")

这段代码工作正常,但速度很慢。最多可能需要 2 天的时间来处理。

我得到了一个用于终端命令行的单行脚本,它执行相同的操作(但没有标题)。此脚本需要 20 秒。

  for f in *.csv; do cat "`pwd`/$f" | tail -n +2 >> merged.csv; done

有谁知道如何加速第一个 Python 脚本?为了缩短时间,我考虑过不将其导入 DataFrame 并只是连接 CSV,但我无法弄清楚。

谢谢。

原文由 mattblack 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 713
2 个回答

如果您不需要内存中的 CSV,只需从输入复制到输出,那么完全避免解析和复制而不在内存中构建会便宜得多:

 import shutil
import glob

#import csv files from folder
path = r'data/US/market/merged_data'
allFiles = glob.glob(path + "/*.csv")
allFiles.sort()  # glob lacks reliable ordering, so impose your own if output order matters
with open('someoutputfile.csv', 'wb') as outfile:
    for i, fname in enumerate(allFiles):
        with open(fname, 'rb') as infile:
            if i != 0:
                infile.readline()  # Throw away header on all but first file
            # Block copy rest of file from input to output without parsing
            shutil.copyfileobj(infile, outfile)
            print(fname + " has been imported.")

就是这样; shutil.copyfileobj 有效地处理数据复制,大大减少了解析和重新序列化的 Python 级别工作。不要省略 `allFiles.sort()!†

这假设所有 CSV 文件都具有相同的格式、编码、行尾等,编码编码使得换行符显示为等效于 ASCII \n 的单个字节并且它是字符中的最后一个字节(所以ASCII 和所有 ASCII 超集编码都有效,UTF-16-BE 和 UTF-32-BE 也有效,但 UTF-16-LE 和 UTF-32-LE 无效)并且标头不包含嵌入的换行符,但如果那是案例,它比替代品快得多。

对于换行符的编码版本看起来不够像 ASCII 换行符,或者输入文件采用一种编码而输出文件应采用不同编码的情况,您可以添加编码和解码的工作不添加 CSV 解析/序列化工作,使用(添加 from io import open 如果在 Python 2 上,以获得类似 Python 3 的高效编码感知文件对象,并定义 known_input_encoding 到一些字符串表示输入文件的已知编码,例如 known_input_encoding = 'utf-16-le' ,以及可选的输出文件的不同编码):

 # Other imports and setup code prior to first with unchanged from before

# Perform encoding to chosen output encoding, disabling line-ending
# translation to avoid conflicting with CSV dialect, matching raw binary behavior
with open('someoutputfile.csv', 'w', encoding=output_encoding, newline='') as outfile:
    for i, fname in enumerate(allFiles):
        # Decode with known encoding, disabling line-ending translation
        # for same reasons as above
        with open(fname, encoding=known_input_encoding, newline='') as infile:
            if i != 0:
                infile.readline()  # Throw away header on all but first file
            # Block copy rest of file from input to output without parsing
            # just letting the file object decode from input and encode to output
            shutil.copyfileobj(infile, outfile)
            print(fname + " has been imported.")

这仍然比涉及 csv 模块 _快得多_,尤其是在现代 Python 中(其中 io 模块已经进行了越来越大的优化,以至于解码和重新编码的成本是非常小,尤其是首先执行 I/O 的成本)。即使编码不应该更改,它也是自检查编码(例如 UTF 系列)的良好有效性检查;如果数据与假定的自检编码不匹配,则极不可能有效解码,因此您将得到一个异常而不是无声的不当行为。


因为这里链接 的一些重复项 正在寻找比 copyfileobj 更快的解决方案,一些选项:

  1. The only succinct, reasonably portable option is to continue using copyfileobj and explicitly pass a non-default length parameter, eg shutil.copyfileobj(infile, outfile, 1 << 20) ( 1 << 20 是 1 MiB,这个数字 shutil 已经切换到普通 shutil.copyfile 由于卓越的性能而在 Windows 上调用)。

  2. 仍然可移植,但仅适用于二进制文件而不简洁,将复制底层代码 copyfile 在 Windows 上使用,它使用可重用的 bytearray 缓冲区,其大小大于 copyfileobj 的默认值(1 MiB,而不是 64 KiB),消除一些分配开销 copyfileobj 不能完全避免大缓冲区。 You’d replace shutil.copyfileobj(infile, outfile) with (3.8+’s walrus operator, := , used for brevity) the following code adapted from CPython 3.10’s implementation of shutil._copyfileobj_readinto (如果您不介意使用非公共 API,您可以直接使用它):

    buf_length = 1 << 20  # 1 MiB buffer; tweak to preference
   # Using a memoryview gets zero copy performance when short reads occur
   with memoryview(bytearray(buf_length)) as mv:
       while n := infile.readinto(mv):
           if n < buf_length:
               with mv[:n] as smv:
                   outfile.write(smv)
           else:
               outfile.write(mv)

  1. 不可移植,如果您可以(以任何您喜欢的方式)确定标头的精确长度,并且您知道它在任何其他文件中甚至不会改变一个字节,您可以直接编写标头,然后使用 OS-特定调用类似于 shutil.copyfile 在后台使用复制每个文件的非标头部分,使用特定于操作系统的 API 可以通过单个系统调用完成工作(无论文件大小)并避免额外的数据副本(通过将所有工作推送到内核甚至文件系统操作中,从用户空间删除副本)例如:

一种。在 Linux 内核 2.6.33 及更高版本(以及允许 sendfile(2) 系统调用在打开的文件之间工作的任何其他操作系统)上,您可以替换 .readline()copyfileobj 调用:

    filesize = os.fstat(infile.fileno()).st_size  # Get underlying file's size
   os.sendfile(outfile.fileno(), infile.fileno(), header_len_bytes, filesize - header_len_bytes)

为了使其具有弹性信号,可能需要检查 sendfile 的返回值,并跟踪发送的字节数+跳过的字节数和剩余的字节数,循环直到你将它们全部复制(这些很低级系统调用,它们可以被中断)。

b.在使用 glibc >= 2.27(或 Linux 内核 4.5+)构建的任何系统 Python 3.8+ 上,文件都在同一文件系统上,您可以将 `sendfile` 替换为 `copy_file_range` :
    filesize = os.fstat(infile.fileno()).st_size  # Get underlying file's size
   os.copy_file_range(infile.fileno(), outfile.fileno(), filesize - header_len_bytes, header_len_bytes)

关于检查复制的字节数是否少于预期并重试的类似警告。

C。在 OSX/macOS 上,您可以使用完全未记录的,因此可移植性/稳定性更差的 API `shutil.copyfile` 使用, `posix._fcopyfile` 用于类似的目的,类似的东西(完全未经测试,而且真的,不要这样做;它可能会突破甚至次要的 Python 版本):
    infile.seek(header_len_bytes)  # Skip past header
   posix._fcopyfile(infile.fileno(), outfile.fileno(), posix._COPYFILE_DATA)

它假设 fcopyfile 注意搜索位置(文档不是 100% 的),并且如前所述,它不仅是特定于 macOS 的,而且使用未记录的 CPython 内部结构,这些内部结构可能会在任何版本中发生变化。


† 关于对 glob 的结果进行排序的旁白:不应省略 allFiles.sort() 调用; glob 不对结果施加任何排序,并且为了可重现的结果,您需要施加 一些 排序(如果具有相同名称和数据的相同文件生成输出文件,那就不好了顺序不同只是因为在运行之间,文件被移出目录,然后又移回目录,并更改了本机迭代顺序)。如果没有 sort 调用,此代码(以及所有其他 Python+glob 模块答案)将无法可靠地从包含 a.csvb.csv (或字母表)的目录中读取任何其他有用的)命令;它会因操作系统、文件系统以及相关目录中文件创建/删除的整个历史而异。这在现实世界中已经破坏了一些东西,请参阅 代码故障可能导致超过 100 项已发表研究中的错误的 详细信息。

原文由 ShadowRanger 发布,翻译遵循 CC BY-SA 4.0 许可协议

您需要在 Python 中执行此操作吗?如果您愿意完全在 shell 中执行此操作,那么您需要做的就是首先 cat 将随机选择的输入 .csv 文件中的标题行放入 merged.csv 在运行你的文件之前-衬垫:

 cat a-randomly-selected-csv-file.csv | head -n1 > merged.csv
for f in *.csv; do cat "`pwd`/$f" | tail -n +2 >> merged.csv; done

原文由 Peter Leimbigler 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题