【问题标题】:Efficient way of splitting large file into smaller ones based on column value根据列值将大文件拆分为较小文件的有效方法
【发布时间】:2019-04-30 17:33:46
【问题描述】:

我有一个非常大的 csv 文件,我想将其拆分为较小的文件,以便大文件中 ID 列(csv 中的第二列)具有相同值的所有条目最终都在同一个文件中。但是,我还需要在每个较小的文件中有 50 个不同的 ID。

我有执行此操作的代码,但对于 1 gig 文件,大约需要 15 到 20 分钟。有没有有效的方法来做到这一点?

这就是我现在拥有的:

awk -F, '{if(NR > 1) {print >> $2"_backfill_tmp.csv"; close($2"_backfill_tmp.csv")}}' $input_file
counter=0
for file in *"_backfill_tmp.csv"
do
  file_name=${input_file%.*}"_backfill2_part_"$PART_NUMBER".csv"
  cat "$file" >> "$file_name"
  rm "$file"
  (( counter++ ))
  if (( $counter % 50 == 0 )) ; then
    (( PART_NUMBER++ ))
  fi
done

awk 命令根据第 2 列的值将每一行写入单独的文件(忽略作为标题的第一行),以便具有相同 ID 值的每一行最终在同一个文件中。我每次都关闭文件,因为我遇到了Too many files open error,我无法在机器上设置ulimit。不过,这个过程只需要大约 15 秒,所以不用担心。

然后我遍历每个临时创建的文件,并将它们写入单独的文件,直到 $counter 达到 50(即组合 50 个文件)。现在这是需要大量时间的地方。我猜是因为有很多单独的ID文件,一个一个打开并合并它们需要很长时间。

我是 awk 初学者,所以我很确定我的代码效率不高。无论如何我可以更快地完成整个过程吗?

【问题讨论】:

  • awk 对于 bash 比这更有效,因为它缓存文件描述符。 (嗯,GNU 可以;其他实现可能要慢得多)。
  • 并不是说你不能让 bash 的执行速度至少比你当前的 bash 方法快一个数量级(尽管它仍然永远不会像写得好的 awk 一样快)——诀窍是停止在每一行上使用>>"$file_name",而是维护一个打开文件描述符的缓存。在 bash 4.1 中,如果您运行 declare -A fds=( ) 创建关联数组,则打开一个新的 FD 并存储其 FD 编号看起来像 exec {fd_num}>"$file_name";然后是fds[$file_name]=$fd_num——当然,只有在fds 数组中没有file_name 条目时才运行该代码。
  • 类似地,在编写性能很重要的 shell 脚本时,不要使用内部循环等外部命令。对catrm 的任何调用都是fork()关闭子进程和exec从外部到外壳的软件。

标签: bash awk


【解决方案1】:

未排序输入脚本

您可以使用以下脚本。我没有使用close,因为现在打开的文件数只有#uniqueIDs / 50 而不是#uniqueIDs。

awk -F, 'NR > 1 {
  if (!($2 in mapIdToPart)) {
    if (uniqueIds % 50 == 0) {
      maxPart++;
    }
    mapIdToPart[$2] = maxPart;
    uniqueIds++;
  }
  print >> "part"mapIdToPart[$2]".csv";
}' input.csv

这将创建文件part#.csv,其中# 是当前部分的编号。输入文件不必排序。具有相同 ID 的行将转到相同的部分。每个部分中的行顺序对应于输入文件中的行顺序。每个部分都有 50 个(或更少,最后一部分)唯一 ID。

排序输入脚本

当您的输入文件按 ID 排序时,您可以加快脚本速度,因为这样您就不需要映射 mapIdToPart 并且每个生成的部分都是一次性编写的。

顺序可以是字母,数字,...,没关系。在这里,我假设排序后的文件不再有标题。如果还有标头,则在awk脚本的开头添加NR > 1

awk -F, '{
  if ($2 != lastId) {
    lastId = $2;
    if (uniqueIds % 50 == 0) {
      close("part"maxPart".csv");
      maxPart++;
    }
    uniqueIds++;
  }
  print >> "part"maxPart".csv";
}' sorted.csv

基准测试

为了测试脚本,我使用

生成了示例数据
n=98""000""000; paste -d,
    <(shuf -i 10""000-99""000 -r -n "$n") \
    <(shuf -i 0-9""999 -r -n "$n") \
| cat <(echo data,id) - > input.csv

样本数据有两列和 9800 万行,其中包含数字。那里有 10,000 个唯一 ID。测量时间

  • 3m 54s 在未排序的输入上运行第一个脚本。
  • 1m 19s 使用tail -n +2 input.csv | LC_ALL=C sort -t, -k2 &gt; sorted.csv 对输入进行排序。 tail 部分删除了标头。
  • 1m 48s 在排序后的输入上运行第二个脚本。
  • 3m 07s 用于排序和运行第二个脚本。

结论:即使您的输入没有排序,排序然后运行第二个脚本可能会更快。

【讨论】:

  • 谢谢!排序后聚合工作完美。但是有没有办法将零件文件命名为输入文件名后跟零件号?我在常规 bash 脚本中有输入文件的名称,然后通过将其传递为 -v op="$input" 在 awk 中使用它。当我在 awk 命令之外打印它时,它看起来很好。但是,当我尝试使用 print &gt;&gt; $op"_part_"maxPart".csv"; 将聚合内容写入 awk 中的文件时,awk 将 $op 替换为文件的实际内容,而不是文件名。为什么会这样?
  • @drunkenfist 在awk 中使用op"_part_"maxPart".csv"awk 中的常规变量前面没有 $。请注意,我在脚本中做了一些小改动。昨天我忘记在排序之前删除标题。
  • print &gt;&gt; "part"maxPart".csv" 是未定义的行为,请改用print &gt;&gt; ("part"maxPart".csv") 但当然您应该真正使用输出文件名的变量来保证您 close() 的文件与您的文件相同正在写信给。
  • @Socowi 是的,它可以在没有 $ 的情况下工作。我以为我已经尝试过了。而且我已经注意到在排序之前忽略了标题行。虽然我只是用了(tail -n +2 $input_file | sort -t, -k2) &gt; sorted.csvLC_ALL=C在你的排序语句中的目的是什么?
  • @drunkenfist tl;dr:它使sort 更快。 --- LC_ALL=... 更改语言环境(此处仅适用于 sort 命令)。 sort 的排序顺序取决于语言环境。对于不同的语言,某些符号可能以不同的顺序排序。尊重这些命令是昂贵的。 C 是您计算机的语言,即每个字符都根据其代码点进行排序,通常会加快 sort
【解决方案2】:

您应该对文件进行排序以获得最佳性能

$ sort -t, -k2,2 file | awk '!($2 in a){c++; a[$1]; 
                                        if(c==50) {ix++; c=0}} 
                                       {print > "file_"(ix+1)}'

计算唯一键并在 50 后增加文件计数器;将每一行打印到索引文件。

如果仍高于系统阈值,您可能仍需要关闭文件。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-11-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-06
    • 1970-01-01
    相关资源
    最近更新 更多