我想从具有 2000 列的 Linux 系统上的大文件中获取多个特定列。我怎样才能做到这一点?
文件 file1.gz 如下所示:
0 0 0 0 0 0 0 0 0 0 ...
0 0 0 0 0 0 0 0 0 0 ...
0 0 0 0 0 0 0 0 0 0 ...
0 0 0 0 0 0 0 0 0 0 ...
0 0 0 0 0 0 0 0 0 0 ...
0 0 0 0 0 0 0 0 0 0 ...
0 0 0 0 0 0 0 0 0 0 ...
0 0 0 0 0 0 0 0 0 0 ...
0 0 0 0 0 0 0 0 0 0 ...
我需要在 file2 中获取的列如下所示:
186
187
188
189
190
191
192
193
194
195
(about 1000 column)
答案1
在 中awk
,您可以按编号引用特定列。例如,第 12 列为$12
,第 1345 列为$1345
。此外,默认的列分隔符是空格,因此在使用空格分隔的文件的示例中,您所需要做的就是解压缩并传递一个awk
打印您感兴趣的列的脚本。
zcat file.gz | awk '{print $1,$12,$195} > newFile
这里的复杂之处在于您有太多所需的列,以至于无法将它们打印出来。在这里,您需要先读取列,然后打印:
awk '{
if (NR==FNR){ wantedColumns[NR]=$1 }
else{
for(i=1;i<=length(wantedColumns)-1;i++){
printf "%s ", $(wantedColumns[i])
}
print $(wantedColumns[length(wantedColumns)])
}
}' file2 <(zcat file1.gz)
例如:
$ zcat file1.gz
line1_field1 line1_field2 line1_field3 line1_field4 line1_field5 line1_field6
line2_field1 line2_field2 line2_field3 line2_field4 line2_field5 line2_field6
line3_field1 line3_field2 line3_field3 line3_field4 line3_field5 line3_field6
line4_field1 line4_field2 line4_field3 line4_field4 line4_field5 line4_field6
line5_field1 line5_field2 line5_field3 line5_field4 line5_field5 line5_field6
line6_field1 line6_field2 line6_field3 line6_field4 line6_field5 line6_field6
line7_field1 line7_field2 line7_field3 line7_field4 line7_field5 line7_field6
line8_field1 line8_field2 line8_field3 line8_field4 line8_field5 line8_field6
line9_field1 line9_field2 line9_field3 line9_field4 line9_field5 line9_field6
$ cat file2
2
4
5
如果我在这些文件上运行上面的脚本,我会得到:
$ awk '{
> if (NR==FNR){ wantedColumns[NR]=$1 }
> else{
> for(i=1;i<=length(wantedColumns)-1;i++){
> printf "%s ", $(wantedColumns[i])
> }
> print $(wantedColumns[length(wantedColumns)])
> }
> }' file2 <(zcat file1.gz)
line1_field2 line1_field4 line1_field5
line2_field2 line2_field4 line2_field5
line3_field2 line3_field4 line3_field5
line4_field2 line4_field4 line4_field5
line5_field2 line5_field4 line5_field5
line6_field2 line6_field4 line6_field5
line7_field2 line7_field4 line7_field5
line8_field2 line8_field4 line8_field5
line9_field2 line9_field4 line9_field5
line10_field2 line10_field4 line10_field5
解释
if (NR==FNR){ wantedColumns[NR]=$1 }
:NR
为输入行号,FNR
为行号当前文件的。仅在读取第一个文件时两者才相等。因此,如果NR
等于FNR
,如果我们正在读取第一个文件,则将该文件的第一个字段保存在数组中,wantedColumns
该数组的索引是行号,值是字段。else { ... }
: 如果我们是不是正在读取第一个文件(如果我们现在位于第二个文件)。for(i=1;i<=length(wantedColumns)-1;i++){
:迭代所需列的数组,从第一个索引(该索引为 1,因为我们NR
在上面的循环中使用 的值)直到倒数第二个,并打印每个列,后跟一个空格。重要警告:不一定会保留原始文件的列顺序。这些列将按照它们在 中找到的顺序打印file2
。如果这与原始文件中的顺序不同,file2
例如,如果有1 3 2
而不是1 2 3
,那么这就是它们将被打印的顺序。print $(wantedColumns[length(wantedColumns)])
:打印最后一个字段,后跟换行符。<(zcat file1.gz)
:这是(以及其他一些 shell)的一项功能bash
,称为 [“进程替换”][1],它允许您将命令的输出视为文件。在这里,我们使用 解压缩文件zcat
并将其作为第二个输入“文件”传递给awk
.
请注意,此方法将在每行末尾添加额外的尾随空格。如果这是一个问题,您可以通过sed
在最后进行管道传输来避免它:
awk '...' | sed 's/ $//'
或者,您可以使用cut
.只需更改file2
为逗号分隔的字段列表并将其传递给cut
:
$ zcat file1.gz | cut -d' ' -f $(tr '\n' ',' < file2 | sed 's/,$//')
line1_field2 line1_field4 line1_field5
line2_field2 line2_field4 line2_field5
line3_field2 line3_field4 line3_field5
line4_field2 line4_field4 line4_field5
line5_field2 line5_field4 line5_field5
line6_field2 line6_field4 line6_field5
line7_field2 line7_field4 line7_field5
line8_field2 line8_field4 line8_field5
line9_field2 line9_field4 line9_field5
line10_field2 line10_field4 line10_field5
解释
zcat file1.gz |
:解压缩file1.gz
并将其内容传送到下一个命令。cut -d' '
:这告诉cut
使用空格而不是默认制表符 (\t
) 作为字段分隔符。-f $(tr '\n' ',' < file2 | sed 's/,$//')
:-f
告诉cut
要打印哪些字段。它可以采用逗号分隔的字段列表,因此我们使用tr '\n' ','
将所有换行符转换为逗号并将结果作为字段列表传递。 [1]:https://www.gnu.org/software/bash/manual/html_node/Process-Substitution.html
答案2
未经测试,因为您没有提供我们可以测试的示例输入/输出,但应该是正确的:
zcat file1.gz | awk '
NR==FNR { out2inFldNr[++numOutFlds] = $1; next }
{
for (outFldNr=1; outFldNr<=numOutFlds; outFldNr++) {
inFldNr = out2inFldNr[outFldNr]
printf "%s%s", $inFldNr, (outFldNr<numOutFlds ? OFS : ORS)
}
}
' file2 -
答案3
使用perl:
#!/usr/bin/perl
use strict;
my @file1;
# read in first file, assuming one column number per line
# subtract 1 because perl arrays start from 0 and append to
# an array called @file1
while(<>) {
push @file1, $_-1;
last if eof; # exit loop after end of the first file
};
# process second file, splitting it into an array called @line
# and then printing only the elements listed in the @file1 array
# (this is known as an "array slice", and perl is very flexible
# about how it can be specified. see `man perldata` for details)
while(<>) {
my @line = split;
print join("\t", @line[@file1]),"\n";
};
使用输入文件f1.txt
和f2.txt.gz
(见下文),它会产生以下输出:
$ ./extract.pl f1.txt <(zcat f2.txt.gz)
a c e g
a c e g
a c e g
a c e g
a c e g
输入文件:
$ cat f1.txt
1
3
5
7
$ zcat f2.txt.gz
a b c d e f g h i j k l m n o p q r s t u v w x y z
a b c d e f g h i j k l m n o p q r s t u v w x y z
a b c d e f g h i j k l m n o p q r s t u v w x y z
a b c d e f g h i j k l m n o p q r s t u v w x y z
a b c d e f g h i j k l m n o p q r s t u v w x y z
也可以写成一行:
$ perl -lne 'push @file1, $_-1; last if eof;
END {
while(<>) {
my @line=split;
print join("\t", @line[@file1]);
};
}' f1.txt <(zcat f2.txt.gz)
两个版本的输出相同。
顺便说一句,上面两个版本的脚本要求两个(或更多)文件名参数(实际文件名或通过进程替换)。相反,如果您想从标准输入读取第二个文件,则必须将其编写为:
#!/usr/bin/perl
use strict;
my @file1;
my $f1 = shift;
open(my $fh,"<",$f1) || die "couldn't open $f1: $!\n";
while(<$fh>) {
push @file1, $_-1;
};
close($f1);
while(<>) {
my @line = split;
print join("\t", @line[@file1]), "\n";
};
这将允许你像这样运行它:
$ zcat f2.txt.gz | ./extract.pl f1.txt
或者您仍然可以像第一个版本一样运行它:
$ ./extract.pl f1.txt <(zcat f2.txt.gz)
换句话说,在此版本中,第一个文件必须由文件名给出,但第二个文件可以是文件或标准输入。
另一种变化是允许两个文件都来自标准输入。
#!/usr/bin/perl
use strict;
my @file1;
while(<>) {
my @line = split;
if (@line == 1) {
push @file1, $_-1;
} else {
print join("\t", @line[@file1]), "\n";
}
};
此版本检查每个输入行上有多少个字段。如果只有一个,我们仍在读取第一个文件,因此将其添加到 @file1 数组中。否则打印出数组切片。
它将运行如下:
$ (cat f1.txt ; zcat f2.txt.gz) | ./extract.pl
或者作为单行代码,使用 Perl 的-a
自动拆分为数组@F
选项(其工作方式类似于awk
自动将其输入拆分为 $1、$2、$3 等):
$ (cat f1.txt ; zcat f2.txt.gz) |
perl -lane 'if (@F==1) {push @file1,$_-1} else {print join("\t",@F[@file1])}'
答案4
您可以按如下方式进行操作。首先,在对 file2 进行数字排序并对其进行唯一化之后,以范围的形式生成字段组合。
然后 Perl 正则表达式(高级)会将输入转换为 24, 25, 26, 33 => 24-26,33,然后我们将其输入以剪切选项。
$ cols=$(< file2 sort -nu | perl -00pe '$_ = s/(\d+)(?{$1})\K(?:\n(\d+)(?(?{++$^R!=$2})(*F)))+/-$2/gr =~ s/\n(?!\z)/,/gr')
$ gunzip -c file1.gz | cut -d' ' -f"$cols"
如果列号是连续的,那么您可以简单地获取前 n 个最后列号,如下所示:
$ cols=$(< file2 sort -nu | sed '$q;1!d' | paste -sd- -)
并像以前一样进行切割。