从包含两千列的大文件中获取多列

从包含两千列的大文件中获取多列

我想从具有 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.txtf2.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- -)

并像以前一样进行切割。

相关内容