我在处理巨大的 .gz 文件(大于 500G)时遇到问题。我的目标是将这些文件中的每个字段按第四个字段分开。我之前用过一个漂亮的 awk 单行语句来做到这一点:
zcat file.txt.gz | awk 'NR>1{print > $4}'
但不幸的是,这需要很长时间才能处理大文件,因此我尝试首先按大小拆分它们,然后在按字段拆分后连接每个文件。我可以使用以下方法拆分它们:
i=file.txt.gz
dir=$i
mkdir -p $dir
cd $dir
split -b 200M ../$i $i
for file in `ls *`; do zcat $file | awk 'NR>1{print > $4}'; done
但是我如何通过第四个字段连接所有正确的文件呢?另外,真的没有更好的办法吗?当我使用 gz 文件分割时,我也遇到错误,就像这样说“文件意外结束”,所以我想我的分割也是错误的,但我不确定我是否朝着正确的方向前进,如果你有建议将会非常有帮助。
非常感谢你的帮助!弗拉
答案1
Satō Katsura 的文件描述符注释是正确的,假设有超过 1021 个(通常用户 FD 限制为 1024,stdin/stdout/stderr 为 -3)$4 的不同值和您正在使用的gawk
.
>
当您使用或打印到文件时>>
,文件将保持打开状态,直到出现显式close()
,因此您的脚本正在累积 FD。自 Gawk v3.0 之前开始,FD 耗尽 ( ulimit -n
) 的处理是透明的:遍历打开文件的链表,并且“暂时”关闭 LRU(最近最少使用)(从操作系统角度关闭以释放 FD) ,gawk
在内部对其进行跟踪,以便稍后在需要时透明地重新打开)。您可以通过-W lint
在调用时添加来看到这种情况的发生(从 v3.1 开始)。
我们可以像这样模拟问题(在bash
):
printf "%s\n" {0..999}\ 2\ 3\ 0{0..9}{0..9}{0..9} | time gawk -f a.awk
这会生成 1,000,000 行输出,其中包含 1000 个唯一值 4 美元,在我的笔记本电脑上大约需要 17 秒。我的限制是 1024 个 FD。
printf "%s\n" {0..499}\ 2\ 3\ {0..1}{0..9}{0..9}{0..9} | time gawk -f a.awk
这也会生成 1,000,000 行输出,但有 2000 个 $4 的唯一值,运行时间约为 110 秒(时间长了六倍多,并且有 100 万个额外的小页面错误)。
从跟踪 $4 的角度来看,上面是“最悲观”的输入,输出文件每一行都会更改(并保证每次都需要(重新)打开所需的输出文件)。
有两种方法可以帮助解决这个问题:减少文件名使用的混乱(即按 $4 进行预排序),或者使用 GNU 对输入进行分块split
。
预分选:
printf "%s\n" {0..499}\ 2\ 3\ {0..1}{0..9}{0..9}{0..9} |
sort -k 4 | time gawk -f a.awk
(您可能需要调整sort
选项以同意awk
的字段编号)
在大约 4.0 秒时,这甚至比第一种情况更快,因为文件处理被最小化。 (请注意,对大文件进行排序可能会使用$TMPDIR
或中的磁盘临时文件/tmp
。)
与split
:
printf "%s\n" {0..499}\ 2\ 3\ {0..1}{0..9}{0..9}{0..9} |
time split -l 1000 --filter "gawk -f a.awk"
这大约需要 38 秒(因此您可以得出结论,即使启动 1000 个进程的开销也gawk
小于低效的内部 FD 处理)。在这种情况下你必须使用>>
而不是>
在 awk 脚本中,否则每个新进程都会破坏之前的输出。 (如果您重新调整代码来调用,同样的警告也适用close()
。)
您当然可以结合使用这两种方法:
printf "%s\n" {0..499}\ 2\ 3\ {0..1}{0..9}{0..9}{0..9} |
time split -l 50000 --filter "sort -k 4 | gawk -f a.awk"
对我来说,这大约需要 4 秒,调整分块 (50000) 可以让您在进程/文件处理开销与sort
磁盘使用要求之间进行权衡。 YMMV。
如果您提前知道输出文件的数量(并且不是太大),您可以使用 root 来增加(例如ulimit -n 8192
,然后su
自己),或者您也可以一般调整限制,请参阅如何增加所有进程的打开文件限制?。该限制将由您的操作系统及其配置(如果不幸的话,可能还有 libc)决定。