我有一个200 MB的制表符分隔文本文件,包含数百万行。在这个文件中,我有一个包含多个位置的列,如美国,英国,澳大利亚等。
现在我想在此列的基础上打破此文件。虽然这段代码对我来说很好,但面临性能问题,因为根据位置将文件拆分成多个文件需要1个多小时。这是代码:
#!/bin/bash
read -p "Please enter the file to split " file
read -p "Enter the Col No. to split " col_no
#set -x
header=`head -1 $file`
cnt=1
while IFS= read -r line
do
if [ $((cnt++)) -eq 1 ]
then
echo "$line" >> /dev/null
else
loc=`echo "$line" | cut -f "$col_no"`
f_name=`echo "file_"$loc".txt"`
if [ -f "$f_name" ]
then
echo "$line" >> "$f_name";
else
touch "$f_name";
echo "file $f_name created.."
echo "$line" >> "$f_name";
sed -i '1i '"$header"'' "$f_name"
fi
fi
done < $file
这里应用的逻辑是我们只读取整个文件一次,并且根据位置,我们正在创建数据并将其附加到它。
请建议对代码进行必要的改进以提高其性能。
以下是样本数据,用冒号而不是制表符分隔。国家/地区代码位于第4列:
ID1:ID2:ID3:ID4:ID5
100:abcd:TEST1:ZA:CCD
200:abcd:TEST2:US:CCD
300:abcd:TEST3:AR:CCD
400:abcd:TEST4:BE:CCD
500:abcd:TEST5:CA:CCD
600:abcd:TEST6:DK:CCD
312:abcd:TEST65:ZA:CCD
1300:abcd:TEST4153:CA:CCD
答案 0 :(得分:2)
要记住以下几点:
while read
阅读文件很慢这是文本处理工具的工作,例如awk。
我建议你使用这样的东西:
# save first line
NR == 1 {
header = $0
next
}
{
filename = "file_" $col ".txt"
# if country code has changed
if (filename != prev) {
# close the previous file
close(prev)
# if we haven't seen this file yet
if (!(filename in seen)) {
print header > filename
}
seen[filename]
}
# print whole line to file
print >> filename
prev = filename
}
使用以下行中的内容运行脚本:
awk -v col="$col_no" -f script.awk file
其中$col_no
是一个shell变量,包含带有国家/地区代码的列号。
如果您没有太多不同的国家/地区代码,则可以将所有文件保持打开状态,在这种情况下,您可以取消对close(filename)
的调用。
您可以在问题中提供的示例中测试脚本,如下所示:
awk -F: -v col=4 -f script.awk file
请注意,我已添加-F:
以将输入字段分隔符更改为:
。
答案 1 :(得分:1)
我认为汤姆走在正确的轨道上,但我会稍微简化一下。
Awk在某些方面是神奇的。其中一种方法是,除非您明确关闭它们,否则它将保持所有输入和输出文件句柄处于打开状态。因此,如果您创建一个包含输出文件名的变量,您可以简单地重定向到您的变量并相信awk会将数据发送到您指定的位置,并在输出用完时最终关闭输出文件
(N.B。这个魔术的延伸是除了重定向之外,你可以保持多个PIPES。想象一下,如果你去cmd="gzip -9 > file_"$4".txt.gz"; print | cmd
)
以下内容拆分您的文件而不向每个输出文件添加标题。
awk -F: 'NR>1 {out="file_"$4".txt"; print > out}' inp.txt
如果添加标题很重要,则需要更多代码。但并不多。
awk -F: 'NR==1{h=$0;next} {out="file_"$4".txt"} !(out in files){print h > out; files[out]} {print > out}' inp.txt
或者,因为这个单线程现在有点长,我们可以将其拆分出来进行解释:
awk -F: '
NR==1 {h=$0;next} # Capture the header
{out="file_"$4".txt"} # Capture the output file
!(out in files){ # If we haven't seen this output file before,
print h > out; # print the header to it,
files[out] # and record the fact that we've seen it.
}
{print > out} # Finally, print our line of input.
' inp.txt
我根据您在问题中提供的输入数据成功测试了这两个脚本。使用这种类型的解决方案,无需对输入数据进行排序 - 每个文件中的输出将按照输出数据中出现该子集记录的顺序。
注意:awk
的不同版本将允许您打开不同数量的打开文件。 GNU awk(gawk
)有数千个限制 - 远远超过您可能需要处理的国家数量。 BSD awk版本20121220(在FreeBSD中)似乎在21117文件后用完。 BSD awk版本20070501(在OS X El Capitan中)限制为17个文件。
如果您对可能的打开文件数量没有信心,可以尝试使用以下类似的awk版本:
mkdir -p /tmp/i
awk '{o="/tmp/i/file_"NR".txt"; print "hello" > o; printf "\r%d ",NR > "/dev/stderr"}' /dev/random
您还可以测试打开的管道数量:
awk '{o="cat >/dev/null; #"NR; print "hello" | o; printf "\r%d ",NR > "/dev/stderr"}' /dev/random
(如果你有一个/dev/yes
或只是吐出文字行的东西,这会比使用/ dev / random进行输入更好。)
我以前在我自己的awk编程中遇到过这个限制,因为当我需要创建许多输出文件时,我总是使用gawk。 :-P