在Bash中加速基于行内容的大文本文件的分离

Jad*_*zia 7 linux io bash awk

我有一个非常大的文本文件(大约20 GB和3亿行),其中包含由制表符分隔的三列:

word1 word2 word3
word1 word2 word3
word1 word2 word3
word1 word2 word3
Run Code Online (Sandbox Code Playgroud)

word1,word2和word3在每一行中都不同.word3指定行的类,并经常为不同的行重复(具有数千个不同的值).目标是通过行类(word3)分隔文件.即word1和word2应该存储在一个名为word3的文件中,用于所有行.例如,对于该行:

a b c
Run Code Online (Sandbox Code Playgroud)

字符串"a b"应该附加到名为c的文件中.

现在我知道如何使用while循环,逐行读取文件,并为每一行附加适当的文件:

while IFS='' read -r line || [[ -n "$line" ]]; do
    # Variables
    read -a line_array <<< ${line}
    word1=${line_array[0]}
    word2=${line_array[1]}
    word3=${line_array[2]}

    # Adding word1 and word2 to file word3
    echo "${word1} ${word2}" >> ${word3}  
done < "inputfile"
Run Code Online (Sandbox Code Playgroud)

它工作,但速度很慢(即使我有一个带SSD的快速工作站).怎么加速?我已经尝试在/ dev/shm中执行此过程,并将文件拆分为10个并且为每个文件并行运行上面的脚本.但它仍然很慢.有没有办法进一步加快速度?

daw*_*awg 4

让我们生成一个示例文件:

$ seq -f "%.0f" 3000000 | awk -F $'\t' '{print $1 FS "Col_B" FS int(2000*rand())}' >file
Run Code Online (Sandbox Code Playgroud)

这会生成一个 300 万行文件,第 3 列中有 2,000 个不同的值,类似于以下内容:

$ head -n 3 file; echo "..."; tail -n 3 file
1   Col_B   1680
2   Col_B   788
3   Col_B   1566
...
2999998 Col_B   1562
2999999 Col_B   1803
3000000 Col_B   1252
Run Code Online (Sandbox Code Playgroud)

通过简单的操作,awk您可以生成您描述的文件:

$ time awk -F $'\t' '{ print $1 " " $2 >> $3; close($3) }' file
real    3m31.011s
user    0m25.260s
sys     3m0.994s
Run Code Online (Sandbox Code Playgroud)

这样 awk 将在大约 3 分 31 秒内生成 2,000 个组文件。当然比 Bash 更快,但是通过按第三列对文件进行预排序并一次性写入每个组文件可以更快。

sort您可以在管道中使用 Unix实用程序,并将输出提供给脚本,该脚本可以将排序后的组分隔到不同的文件中。使用-s选项 with sort,第三个字段的值将是唯一会更改行顺序的字段。

由于我们可以假设sort已根据文件的第 3 列将文件分区为组,因此脚本只需要检测该值何时发生变化:

$ time sort -s -k3 file | awk -F $'\t' 'fn != ($3 "") { close(fn); fn = $3 } { print $1 " " $2 > fn }'
real    0m4.727s
user    0m5.495s
sys     0m0.541s
Run Code Online (Sandbox Code Playgroud)

由于预分选提高了效率,相同的净过程只需 5 秒即可完成。

如果您确定第 3 列中的“单词”仅是 ascii(即,您不需要处理 UTF-8),您可以设置LC_ALL=C额外的速度

$ time LC_ALL=C sort -s -k3 file | awk -F $'\t' 'fn != ($3 "") { close(fn); fn = $3 } { print $1 " " $2 > fn }'
real    0m3.801s
user    0m3.796s
sys     0m0.479s
Run Code Online (Sandbox Code Playgroud)

来自评论:

1)请添加一行来解释为什么我们需要括号中的表达式fn != ($3 "")

awk的结构是使用您认为最具可读性的结构fn != ($3 "") {action}的有效快捷方式fn != $3 || fn=="" {action}

2)不确定如果文件大于可用内存,这是否也有效,所以这可能是一个限制因素。:

我运行了第一个和最后一个 awk,包含 3 亿条记录和 20,000 个输出文件。最后一个使用 sort 的人在 12 分钟内完成了这项任务。第一个花了10个小时...

排序版本实际上可能具有更好的扩展性,因为打开、附加和关闭 20,000 个文件 3 亿次需要一段时间。将相似的数据组合起来并进行流式传输会更有效。

3)我之前考虑过排序,但后来觉得它可能不是最快的,因为我们必须用这种方法读取整个文件两次。:

这是纯随机数据的情况;如果实际数据有些排序,则需要权衡读取文件两次。第一个 awk 在随机数据较少的情况下会明显更快。但随后还需要时间来确定文件是否已排序。如果您知道文件大部分已排序,请使用第一个;如果可能有些混乱,请使用最后一个。

  • @dawg:感谢您澄清这一点。在这种情况下,我同意 oguzismail 的观点,即您的解决方案应该是公认的答案。与单独使用 awk 已经显着的加速相比,这是一个显着的额外加速。 (3认同)
  • 谢谢你的好解决方案。我也用较大的文件对其进行了测试,因为我担心排序不会随行数线性缩放。但令人惊讶的是,排序似乎与文件大小成线性比例,因此您的解决方案也应该适用于非常大的文件。我测试的最大文件为 6 GB。不确定如果文件大于可用内存,这是否也有效,所以这可能是一个限制因素。否则,这似乎是迄今为止最有效的解决方案。 (2认同)
  • “sort”将通过将输入分解为更小的临时文件来对比物理内存大得多的文件进行排序。这是 1970 年代技术的奇迹,当时 RAM 和磁盘都很昂贵。 (2认同)
  • @Jadzia我认为这应该是公认的答案,而不是我的。 (2认同)