提问人:Caterina 提问时间:10/24/2023 更新时间:10/25/2023 访问量:147
连接目录中所有文件的第 n 列的有效方法?
Efficient way to join the nth column of all files in a directory?
问:
for 循环是减速的方式。这些文件有 500k 行。我想专门加入所有文件的第 4 列。将一列又一列地追加到右侧。
每个文件中的列由制表符分隔。
col1 col2 col3 col4 col5
a 0 0 -1 0.001
b 1 0 2 0.004
c 2 0 3 0
col1 col2 col3 col4 col5
c 2 0 -9 0.004
s 1 0 5 0.002
d 3 0 3 0.4
col1 col2 col3 col4 col5
r 2 1 0 0.4
j 1 1 1 0.2
r 3 1 2 0.1
我想要:
file1 file2 file3
-1 -9 0
2 5 1
3 3 2
我尝试首先转换为 .csv:
for file in $(ls) do awk '{$1=$1}1' OFS=',' ${file} > ${file}.csv done
然后这样做:
eval paste -d, $(printf "<(cut -d, -f4 %s) " *.csv)
但是我收到此错误:paste: /dev/fd/19: Too many open files
我必须加入 400 个文件,每个文件有 500k 行。
答:
您的操作系统不允许您一次处理那么多文件。您必须将它们分解成更小的批次。以下是一次只做一个的方法。paste
for file in *.csv; do
if [ -e tempfile ]; then
paste -d, tempfile <(cut -d, -f4 "$file") >tempfile2
mv tempfile2 tempfile
else
cut -d, -f4 "$file" >tempfile
fi
done
mv tempfile result.csv
顺便说一句,不要在脚本中使用 ls
。你只想
awk '{$1=$1}1' OFS=',' * > ${file}.csv
...但是没有理由将每个文件单独转换为CSV。您可以将两个操作合二为一;
rm tempfile
for file in *; do
case $file in tempfile | tempfile2 | result.csv) continue;; esac
if [ -e tempfile ]; then
paste -d, tempfile <(awk '{print $4}' "$file") >tempfile2
mv tempfile2 tempfile
else
awk '{ print $4 }' "$file" >tempfile
fi
done
mv tempfile result.csv
评论
以下是如何在单个 中执行此操作的方法,这将比 shell 循环和循环中的所有额外命令更有效:awk
awk -F '\t' '
FNR == 1 {
fn = FILENAME
sub(/\.[^.]+$/, "", fn)
rec[FNR] = (FNR in rec ? rec[FNR] FS : "") fn
next
}
{
rec[FNR] = (FNR in rec ? rec[FNR] FS : "") $4
m = FNR
}
END {
for (i=1; i<=m; ++i)
print rec[i]
}' file{1..3}.csv
file1 file2 file3
-1 -9 0
2 5 1
3 3 2
使用任何 awk 并假设您的所有文件都有相同数量的行,没有一个是空的,您不仅在字段之间的空格中具有制表符(根据您所做的注释),您没有任何空字段,并且您实际上想要 CSV 输出:
$ cat tst.awk
BEGIN { OFS="," }
FNR == 1 { val = FILENAME }
FNR > 1 { val = $4 }
{ vals[FNR] = ( FNR in vals ? vals[FNR] OFS : "" ) val }
END {
for ( i=1; i<=FNR; i++ ) {
print vals[i]
}
}
$ awk -f tst.awk file{1..3}
file1,file2,file3
-1,-9,0
2,5,1
3,3,2
如果您提到的“奇怪的空白”可以是控制字符,并且您有一个 POSIX awk,那么请更改为适当设置或使用等效的,以您喜欢的为准。如果您没有 POSIX awk,那么可能适合您。BEGIN { OFS="," }
BEGIN { FS="[[:space:][:cntrl:]]+"; OFS="," }
FS
FS="[^[:graph:]]+"
FS="[^a-zA-Z_0-9.-]+"
但是我收到此错误:
paste: /dev/fd/19: Too many open files
我必须加入 400 个文件,每个文件有 500k 行。
根据修复 Linux 中的“打开文件过多”错误 |Baeldung 在 Linux 上有两个与该错误相关的限制,称为软和硬。你可以通过这样做来揭示他们当前的价值观
ulimit -Sn
和
ulimit -Hn
分别。如果后者大于 400,您可以通过将 Soft 设置为足够高的值来消除错误,我建议在您的情况下
ulimit -n 512
由于此解决方案依赖于机器,因此我无法对其进行测试,请这样做
ulimit -n 512 && eval paste -d, $(printf "<(cut -d, -f4 %s) " *.csv)
并写下效果。
我创建了以下 40 个测试文件:
$ head -3 file_*
==> file_01 <==
Col 1 Col 2 Col 3 Col 4 Col 5
0.56 0.90 0.75 0.25 0.95
0.40 0.26 0.99 0.05 0.06
==> file_02 <==
Col 1 Col 2 Col 3 Col 4 Col 5
0.62 0.18 0.01 0.85 0.29
0.82 0.53 0.99 0.78 0.91
==> file_03 <==
Col 1 Col 2 Col 3 Col 4 Col 5
0.20 0.80 0.97 0.17 0.23
0.87 0.03 0.61 0.88 0.03
...
==> file_40 <==
Col 1 Col 2 Col 3 Col 4 Col 5
0.98 0.12 0.02 0.84 0.36
0.57 0.31 0.65 0.92 0.95
每个有 500,000 行。
我用 Bash 的测试测试了这篇文章中每个条目的时间性能(不是最准确但相关的。time
我还添加了两个我编写的条目,并编辑了 tripleee 的解决方案,以便它产生相同的制表符分隔结果(并更正了导致它无法完成的 glob 问题)。
红宝石:
ruby -e '
BEGIN{files=Hash.new {|h,k| h[k] = []} }
ARGV.each{|fn| fh=File.open(fn)
fh.each_line.with_index{|line,i| files[fn]<<line.split[3] if i>0}
}
END{
puts files.keys.join("\t")
files.values.transpose.each{|row| puts row.join("\t")}
}' file_* >tst_1
此管道带有 GNU awk(用于 ENDFILE 模式)和 GNU datamash
gawk 'BEGIN{FS=OFS="\t"}
FNR==1 {printf "%s",FILENAME; next}
{printf "%s%s", OFS, $4}
ENDFILE{print ""}' file_* | datamash transpose >tst_5
我编辑了 tripleee 的解决方案,使其在我的计算机上运行并产生相同的结果:
for file in file_*; do
if [ -e tempfile ]; then
paste -d$'\t' tempfile <(awk 'BEGIN{FS="\t"} FNR==1{print FILENAME; next}{ print $4 }' "$file") >tempfile2
mv tempfile2 tempfile
else
awk 'BEGIN{FS="\t"} FNR==1{print FILENAME; next}{ print $4 }' "$file" >tempfile
fi
done
mv tempfile tst_4
据我所知,这些中的每一个都产生了“正确”的输出:
$ head file_{1,4,5}
==> tst_1 <==
file_01 file_02 file_03 file_04 file_05 file_06 file_07 file_08 file_09 file_10 file_11 file_12 file_13 file_14 file_15 file_16 file_17 file_18 file_19 file_20 file_21 file_22 file_23 file_24 file_25 file_26 file_27 file_28 file_29 file_30 file_31 file_32 file_33 file_34 file_35 file_36 file_37 file_38 file_39 file_40
0.25 0.85 0.17 0.01 0.89 0.91 0.27 0.27 0.42 0.71 0.59 0.42 0.57 0.13 0.13 0.45 0.31 0.87 0.54 0.55 0.14 0.06 0.06 0.38 0.14 0.11 0.15 0.72 0.07 1.00 1.00 0.28 0.62 0.71 0.09 0.78 0.90 0.90 0.10 0.84
==> tst_4 <==
file_01 file_02 file_03 file_04 file_05 file_06 file_07 file_08 file_09 file_10 file_11 file_12 file_13 file_14 file_15 file_16 file_17 file_18 file_19 file_20 file_21 file_22 file_23 file_24 file_25 file_26 file_27 file_28 file_29 file_30 file_31 file_32 file_33 file_34 file_35 file_36 file_37 file_38 file_39 file_40
0.25 0.85 0.17 0.01 0.89 0.91 0.27 0.27 0.42 0.71 0.59 0.42 0.57 0.13 0.13 0.45 0.31 0.87 0.54 0.55 0.14 0.06 0.06 0.38 0.14 0.11 0.15 0.72 0.07 1.00 1.00 0.28 0.62 0.71 0.09 0.78 0.90 0.90 0.10 0.84
==> tst_5 <==
file_01 file_02 file_03 file_04 file_05 file_06 file_07 file_08 file_09 file_10 file_11 file_12 file_13 file_14 file_15 file_16 file_17 file_18 file_19 file_20 file_21 file_22 file_23 file_24 file_25 file_26 file_27 file_28 file_29 file_30 file_31 file_32 file_33 file_34 file_35 file_36 file_37 file_38 file_39 file_40
0.25 0.85 0.17 0.01 0.89 0.91 0.27 0.27 0.42 0.71 0.59 0.42 0.57 0.13 0.13 0.45 0.31 0.87 0.54 0.55 0.14 0.06 0.06 0.38 0.14 0.11 0.15 0.72 0.07 1.00 1.00 0.28 0.62 0.71 0.09 0.78 0.90 0.90 0.10 0.84
以下是每个人的时间:
dawg gawk pipe: real 0m5.697s
dawg Ruby: real 0m17.668s
anubhava awk: real 0m24.094s
Ed Morton awk: real 0m24.345s
tripleee paste: real 1m21.150s
如果性能是您的目标,请使用 datamash。在我的测试中,它比 awk 解决方案快 4 倍以上,比使用 Bash 循环快 14 倍。Ruby 比 awks 快一些。paste
如果要生成测试文件,可以使用以下脚本:
#!/bin/bash
cd /tmp
cnt=499999
for x in {01..40}; do
fn="file_$x"
echo "$fn"
gawk -v cnt="$cnt" 'BEGIN{
srand()
OFS="\t"; col_cnt=5
for(col=1; col<=col_cnt; col++)
printf "%s%s%s", "Col ",col, (col==col_cnt ? ORS : OFS)
for(row=1;row<=cnt;row++)
for(col=1; col<=col_cnt; col++)
printf "%.2f%s", rand(), (col==col_cnt ? ORS : OFS)
}' >"$fn"
done
评论