根据列值拆分大型 csv 文本文件

Posted

技术标签:

【中文标题】根据列值拆分大型 csv 文本文件【英文标题】:split large csv text file based on column value 【发布时间】:2012-04-14 15:15:12 【问题描述】:

我的 CSV 文件有多个已排序的列。例如,我可能有这样的行:

19980102,,PLXS,10032,Q,A,,,15.12500,15.00000,15.12500,2
19980105,,PLXS,10032,Q,A,,,14.93750,14.75000,14.93750,2
19980106,,PLXS,10032,Q,A,,,14.56250,14.56250,14.87500,2
20111222,,PCP,63830,N,A,,,164.07001,164.09000,164.12000,1
20111223,,PCP,63830,N,A,,,164.53000,164.53000,164.55000,1
20111227,,PCP,63830,N,A,,,165.69000,165.61000,165.64000,1

我想根据第 3 列划分文件,例如将 PLXS 和 PCP 条目放入它们自己的名为 PLXS.csv 和 PCP.csv 的文件中。因为文件恰好是预先排序的,所以所有 PLXS 条目都在 PCP 条目之前,依此类推。

我通常最终会在 C++ 中做这样的事情,因为这是我最了解的语言,但在这种情况下,我的输入 CSV 文件有几个 GB 并且太大而无法在 C++ 中加载到内存中。

有人能说明如何做到这一点吗? Perl/Python/php/bash 解决方案都可以,它们只需要能够处理巨大的文件而不会占用过多的内存。

【问题讨论】:

你浏览过吗?本网站上所有上述语言的几个相关问题等等。您可以搜索:site:***.com csv split by value 或一些此类变体。祝你好运 【参考方案1】:

这是一个适合您的老式单行代码(只需将 >> 替换为 > 以在每次运行时截断输出文件):

awk -F, 'print >> ($3".csv")' input.csv

由于大众的需求(以及我刚刚的痒),我还编写了一个版本,可以将标题行复制到所有文件:

awk -F, 'NR==1 h=$0; next f=$3".csv" !($3 in p) p[$3]; print h > f print >> f' input.csv

但你可以从这个开始,然后以第一个 awk 结束:

HDR=$(head -1 input.csv); for fn in $(tail -n+2 input.csv | cut -f3 -d, | sort -u); do echo $HDR > $fn.csv; done

大多数现代系统都包含 awk 二进制文件,但如果您没有,您可以在 Gawk for Windows 找到一个 exe 文件

【讨论】:

这太棒了 :) 如果我们能保留标题就更好了 原始文件中没有标题。也许你可以问一个不同的问题? 这对我有帮助 - 但要改进 - 可以使用 NR==1 hdr=$0; next 简化标题示例,然后您无需检查后续模式的 NR,因为您无法访问这些模式并且不是由于 Awk 的排序规则,NR>1。来自手册页:Each pattern in the program then shall be evaluated in the order of occurrence, and the action associated with each pattern that matches the current record executed. 还有一个小的改进 - 文件名模式 fn=$3".csv" 应该出现在 NR==1 模式之后,因为它仅由遵循此模式的模式使用。 感谢您的反馈,@Phil。我不相信探查器会在您提出建议后显示出任何更改或改进,并且我相信会混淆标准 awk 语言结构支持的明确意图(您很有帮助地包括在内,我觉得也很清楚)。 1.添加next不改变逻辑。删除有意定义代码块用途的显式 awk 指令将是您提供的报价的反模式。 2. 对程序中引用的模式重新排序不会更改代码执行计划,因此请随意编辑您的副本!【参考方案2】:

如果输入文件中没有标题行

awk -F, '
fn = $3".csv"
 print > fn' bigfile.csv

如果有一个标题行应该传递给拆分的文件

awk -F, '
NR==1 hdr=$0; next
fn = $3".csv"
!seen[$3]++print hdr > fn
print > fn' bigfile.csv

【讨论】:

【参考方案3】:
perl -F, -ane '`echo $_ >> $F[2].csv`' < file

使用这些命令行选项:

-n 循环输入文件的每一行 -l 在处理之前删除换行符,然后将它们添加回 -a 自动拆分模式 - 将输入行拆分为 @F 数组。默认为空格分割。 -e执行perl代码 -F 自动拆分修饰符,在这种情况下拆分 ,

@F 是每行中的单词数组,索引以$F[0] 开头


如果要保留标头,则需要更复杂的方法。

perl splitintofiles.pl file

splitintofiles.pl 的内容:

open $fh, '<', $ARGV[0];
while ($line = <$fh>) 
    print $line;
    if ($. == 1) 
        $header = $line;
     else 
        # $fields[2] is the 3rd column
        @fields = split /,/, $line;
        # save line into hash %c
        $c"$fields[2].csv" .= $line;
    

close $fh;
for $file (keys %c) 
    print "$file\n";
    open $fh, '>', $file;
    print $fh $header;
    print $fh $c$file;
    close $fh;

输入:

a,b,c,d,e,f,g,h,i,j,k,l
19980102,,PLXS,10032,Q,A,,,15.12500,15.00000,15.12500,2
19980105,,PLXS,10032,Q,A,,,14.93750,14.75000,14.93750,2
19980106,,PLXS,10032,Q,A,,,14.56250,14.56250,14.87500,2
20111222,,PCP,63830,N,A,,,164.07001,164.09000,164.12000,1
20111223,,PCP,63830,N,A,,,164.53000,164.53000,164.55000,1
20111227,,PCP,63830,N,A,,,165.69000,165.61000,165.64000,1

输出 PCP.csv

a,b,c,d,e,f,g,h,i,j,k,l
20111222,,PCP,63830,N,A,,,164.07001,164.09000,164.12000,1
20111223,,PCP,63830,N,A,,,164.53000,164.53000,164.55000,1
20111227,,PCP,63830,N,A,,,165.69000,165.61000,165.64000,1

输出 PLXS.csv

a,b,c,d,e,f,g,h,i,j,k,l
19980102,,PLXS,10032,Q,A,,,15.12500,15.00000,15.12500,2
19980105,,PLXS,10032,Q,A,,,14.93750,14.75000,14.93750,2
19980106,,PLXS,10032,Q,A,,,14.56250,14.56250,14.87500,2

【讨论】:

【参考方案4】:

如果文件的前三列没有引号,则简单的单行是:

cat file | perl -e 'while(<>)@a=split(/,/,$_,4);$key=$a[2];open($f$key,">$key.csv") unless $f$key;print $f$key $_; for $key (keys %f) close $f$key'

它不会消耗太多内存(仅存储关联 distinct(3rd_column) --> 文件句柄)并且行可以按任何顺序排列。

如果列更复杂(例如包含引号括起来的逗号),则使用Text::CSV

【讨论】:

实际上,我只是注意到这与下面 Sean Summers 的答案基本相同。【参考方案5】:

另一种解决方案是将 CSV 加载到 Solr 索引中,然后根据您的自定义搜索条件生成 CSV 文件。

这是一个基本的 HOWTO:

Create report and upload to server for download

【讨论】:

【参考方案6】:

如果你最了解 C++,它就很好。你为什么要尝试将整个文件加载到内存中?

由于输出取决于正在读取的列,您可以轻松地为输出文件存储缓冲区,并在处理时将记录填充到适当的文件中,同时清理以保持内存占用相对较小。

当需要从数据库中提取大量数据时,我会这样做(尽管是在 java 中)。记录被推送到文件缓冲区流中,并且内存中的任何内容都被清理,因此程序的占用空间永远不会超过它最初开始时的大小。

坐以待毙伪代码:

    创建一个列表来保存您的输出文件缓冲区 在文件中打开流并开始一次读取一行内容 我们是否遇到过具有针对其内容类型的打开文件流的记录? 是的 - 获取存储的文件流 将记录存储到该文件中 刷新流 没有 - 创建一个流并将其保存到我们的流列表中 将记录存储在流中 刷新流 冲洗重复...

基本上会继续此处理,直到我们到达文件末尾。

由于我们存储的不仅仅是指向流的指针,而且我们一写入流就会刷新,因此除了输入文件中的一条记录之外,我们不会在应用程序的内存中保存任何驻留的内容。因此,足迹保持可管理。

【讨论】:

+1:C++ 不是问题。将整个文件加载到内存中是个问题。

以上是关于根据列值拆分大型 csv 文本文件的主要内容,如果未能解决你的问题,请参考以下文章

在 Powershell 中,按记录类型拆分大型文本文件的最有效方法是啥?

根据列将大型 csv 文件拆分为多个文件

根据父文件头的大小将大型 CSV 文件拆分为多个文件

根据行值python将大型csv文件拆分为多个文件

如何根据日期列在不同的文本/csv文件中转储一个巨大的mysql表?

javascript 将文本文件或csv拆分为指定行数的较小文件