Недавно я подошел к невероятно быстрому awk, так как мне нужно было разбирать очень большие файлы. Мне пришлось разбирать такой ввод ...

ID   001R_FRG3G              Reviewed;         256 AA.
AC   Q6GZX4;
[...]
SQ   SEQUENCE   256 AA;  29735 MW;  B4840739BF7D4121 CRC64;
     MAFSAEDVLK EYDRRRRMEA LLLSLYYPND RKLLDYKEWS PPRVQVECPK APVEWNNPPS
     EKGLIVGHFS GIKYKGEKAQ ASEVDVNKMC CWVSKFKDAM RRYQGIQTCK IPGKVLSDLD
     AKIKAYNLTV EGVEGFVRYS RVTKQHVAAF LKELRHSKQY ENVNLIHYIL TDKRVDIQHL
     EKDLVKDFKA LVESAHRMRQ GHMINVKYIL YQLLKKHGHG PDGPDILTVK TGSKGVLYDD
     SFRKIYTDLG WKFTPL
//
ID   002L_FRG3G              Reviewed;         320 AA.
AC   Q6GZX3;
[...]
SQ   SEQUENCE   320 AA;  34642 MW;  9E110808B6E328E0 CRC64;
     MSIIGATRLQ NDKSDTYSAG PCYAGGCSAF TPRGTCGKDW DLGEQTCASG FCTSQPLCAR
     IKKTQVCGLR YSSKGKDPLV SAEWDSRGAP YVRCTYDADL IDTQAQVDQF VSMFGESPSL
     AERYCMRGVK NTAGELVSRV SSDADPAGGW CRKWYSAHRG PDQDAALGSF CIKNPGAADC
     KCINRASDPV YQKVKTLHAY PDQCWYVPCA ADVGELKMGT QRDTPTNCPT QVCQIVFNML
     DDGSVTMDDV KNTINCDFSK YVPPPPPPKP TPPTPPTPPT PPTPPTPPTP PTPRPVHNRK
     VMFFVAGAVL VAILISTVRW
//
ID   004R_FRG3G              Reviewed;          60 AA.
AC   Q6GZX1; dog;
[...]
SQ   SEQUENCE   60 AA;  6514 MW;  12F072778EE6DFE4 CRC64;
     MNAKYDTDQG VGRMLFLGTI GLAVVVGGLM AYGYYYDGKT PSSGTSFHTA SPSFSSRYRY

... отфильтруйте его таким файлом ...

Q6GZX4
dog

... чтобы получить такой вывод:

Q6GZX4 MAFSAEDVLKEYDRRRRMEALLLSLYYPNDRKLLDYKEWSPPRVQVECPKAPVEWNNPPSEKGLIVGHFSGIKYKGEKAQASEVDVNKMCCWVSKFKDAMRRYQGIQTCKIPGKVLSDLDAKIKAYNLTVEGVEGFVRYSRVTKQHVAAFLKELRHSKQYENVNLIHYILTDKRVDIQHLEKDLVKDFKALVESAHRMRQGHMINVKYILYQLLKKHGHGPDGPDILTVKTGSKGVLYDDSFRKIYTDLGWKFTPL    256
dog    MNAKYDTDQGVGRMLFLGTIGLAVVVGGLMAYGYYYDGKTPSSGTSFHTASPSFSSRYRY    60

Для этого я придумал такой код:

   BEGIN{
    while(getline<"filterFile.txt">0)B[$1];
}
{
    if ($1=="ID")
        len=$4;
    else{
        if ($1=="AC"){
            acc=0;
            line = substr($0,6,length($0)-6);
            split(line,A,"; ");

            for (i in A){
                if (A[i] in B){
                    acc=A[i];
                }
            }
            if (acc){
                printf acc"\t";
            }
        }
        if (acc){
            if(substr($0, 1, 5) == "     "){
                printf $1$2$3$4$5$6;
            }
            if ($1 == "//"){
                print "\t"len
            }   
        }
    }
}

Однако, поскольку я видел много примеров аналогичных задач, выполняемых с помощью awk, я думаю, что, вероятно, есть гораздо более элегантный и эффективный способ сделать это. Но я не могу понять сверхкомпактные примеры, которые обычно можно найти в Интернете. Поскольку это мой ввод, вывод и код, я думаю, что это хороший повод лучше понять оптимизацию awk с точки зрения производительности и стиля программирования, если у какого-нибудь awk-гуру есть время и терпение, чтобы потратить на эту задачу.

2
Marco Necci 22 Окт 2015 в 13:29

7 ответов

Лучший ответ

Решение awk с другим разделителем полей (таким образом вы избегаете использования substr и split):

BEGIN {
    while (getline<"filterFile.txt">0) filter[$1] = 1;
    FS = "[ \t;]+"; OFS = ""; ORS = "";
}

{
    if (flag) {
        if (len)
            if ($1 == "//") {
                print "\t" len "\n";
                flag = 0; len = 0;
            } else {
                $1 = $1;
                print;
            }
        else if ($1 == "SQ") len = $3;
    } else if ($1 == "AC") { 
        for (i = 1; ++i < NF;)
            if (filter[$i]) {
                flag = 1;
                print $i "\t";
                break;
            }
    }
}

END { if (flag) print "\t" len }

Примечание: этот код не должен быть коротким, но должен быть быстрым. Поэтому я не пытался удалять вложенные условия if / else, а старался максимально сократить глобальное количество тестов для всего файла. Однако после нескольких изменений, внесенных в мою первую версию, и после нескольких тестов, я должен признать, что версия choroba perl стала немного быстрее.

1
Casimir et Hippolyte 22 Окт 2015 в 18:17

Perl на помощь:

#!/usr/bin/perl
use warnings;
use strict;

open my $FILTER, '<', 'filterFile.txt' or die $!;
my %wanted;                # Hash of the wanted ids.
chomp, $wanted{$_} = 1 for <$FILTER>;

$/ = "//\n";               # Record separator.
while (<>) {
    my ($id_string) = /^ AC \s+ (.*) /mx;
    my @ids = split /\s*;\s*/, $id_string;

   if (my ($id) = grep $wanted{$_}, @ids) {
        print "$id\t";
        my ($seq) = /^ SQ \s+ .* $ ((?s:.*)) /mx;
        $seq =~ s/\s+//g;  # Remove whitespace.
        $seq =~ s=//$==;   # Remove the final //.
        print "$seq\t", length $seq, "\n";
    }
}
2
choroba 22 Окт 2015 в 11:42

Для такого рода задач идея состоит в том, чтобы направить ваш второй файл через awk или sed, чтобы на лету создать новый сценарий awk, анализирующий большой файл. Например:

Контрольный файл (f1):

test
dog

Данные (f2):

tree 5
test 2
nothing
dog 1

Идея для начала:

sed 's/^\(.*\)$/\/\1\/ {print $2}/' f1 | awk -f - f2

(где -f - означает: читать сценарий awk из стандартного ввода, а не из именованного файла).

1
Thomas Baruchel 22 Окт 2015 в 11:35

Может быть не намного короче оригинала, но несколько скриптов awk сделают код проще. Первый awk генерирует интересующие записи, второй извлекает информацию, третий форматирует

$ awk 'NR==FNR{keys[$0];next} 
              {RS="//";
               for(k in keys) 
                  if($0~k) 
                     {print "key",k; print $0}}' keys file
| awk '/key/{key=$2;f=0;;next} 
        /SQ/{f=1;print "\n\n"key,$3;next} 
           f{gsub(" ","");printf $0} 
         END{print}' 
| awk -vRS= -vOFS="\t" '{print $1,$3,$2}'

Напечатает

Q6GZX4  MAFSAEDVLKEYDRRRRMEALLLSLYYPNDRKLLDYKEWSPPRVQVECPKAPVEWNNPPSEKGLIVGHFSGIKYKGEKAQASEVDVNKMCCWVSKFKDAMRRYQGIQTCKIPGKVLSDLDAKIKAYNLTVEGVEGFVRYSRVTKQHVAAFLKELRHSKQYENVNLIHYILTDKRVDIQHLEKDLVKDFKALVESAHRMRQGHMINVKYILYQLLKKHGHGPDGPDILTVKTGSKGVLYDDSFRKIYTDLGWKFTPL       256
dog     MNAKYDTDQGVGRMLFLGTIGLAVVVGGLMAYGYYYDGKTPSSGTSFHTASPSFSSRYRY    60
1
karakfa 22 Окт 2015 в 13:46

Ваш код выглядит почти нормально как есть. Сделайте это просто, за один проход.

Только пара предложений:

1) Бизнес вокруг раскола слишком беспорядочный / хрупкий. Может, попробуем так:

        acc="";
        n=split($0,A,"[; ]+");

        for (i=2;i<=n;++i){
            if (A[i] in B){
                acc=A[i];
                break;
            }
        }

2) Не используйте входные данные в первом аргументе ваших printf. Вы никогда не знаете, когда что-то похожее на форматирование printf может прийти и действительно испортить ситуацию:

printf "%s\t",acc";

printf "%s%s%s%s%s%s",$1,$2,$3,$4,$5,$6;

Дополним еще одной возможной «элегантностью»:

3) Стиль awk для pattern{action} уже является формой if / then, так что вы можете избежать большого количества внешних вложений if / then:

$1="ID" {len=$4}
$1="AC" {
    acc="";
    ...
    }
acc {
    if(substr($0, 1, 5) == "     "){
      ...
    }
1
Jeff Y 22 Окт 2015 в 17:15

В Vim на самом деле найти шаблон в одну строку:

/^AC.\{-}Q6GZX4;\_.\{-}\nSQ\_.\{-}\n\zs\_.\{-}\ze\/\//

Где Q6GZX4; - ваш шаблон, который нужно найти, чтобы сопоставить символы последовательности.

Вышеупомянутое в основном будет делать:

  1. Найдите строку с AC в начале (^), за которой следует Q6GZX4;.
  2. Следуйте через несколько строк (\_.\{-}) до строки, начинающейся с SQ (\nSQ).
  3. Затем перейдите к следующей строке, игнорируя текущую (\_.\{-}\n).
  4. Теперь начните выбирать основной шаблон (\zs), который по сути является всем в нескольких строках (\_.\{-}) до (\ze) шаблона // если найден.
  5. Затем выполните обычные команды Vim (norm), которые выбирают шаблон (gn) и вставляют его в регистр x ("xy).
  6. Теперь вы можете вывести регистр (echo @x) или удалить из него пробельные символы.

Это можно расширить до сценария редактора Ex, как показано ниже (например, cmd.ex):

let s="Q6GZX4"
exec '/^AC.\{-}' . s . ';\_.\{-}\nSQ\_.\{-}\n\zs\_.\{-}\ze\/\//norm gn"xy'
let @x=substitute(@x,'\W','','g')
silent redi>>/dev/stdout
echon s . " " . @x
redi END
q!

Затем запустите из командной строки как:

$ ex inputfile < cmd.ex
Q6GZX4 MAFSAEDVLKEYDRRRRMEALLLSLYYPNDRKLLDYKEWSPPRVQVECPKAPVEWNNPPSEKGLIVGHFSGIKYKGEKAQASEVDVNKMCCWVSKFKDAMRRYQGIQTCKIPGKVLSDLDAKIKAYNLTVEGVEGFVRYSRVTKQHVAAFLKELRHSKQYENVNLIHYILTDKRVDIQHLEKDLVKDFKALVESAHRMRQGHMINVKYILYQLLKKHGHGPDGPDILTVKTGSKGVLYDDSFRKIYTDLGWKFTPL

Приведенный выше пример может быть расширен для нескольких файлов или совпадений.

1
kenorb 22 Окт 2015 в 17:23
awk 'FNR == NR { aFilter[ $1 ";"] = $1; next }
   /^AC/ {
      if (String !~ /^$/) print Taken "\t" String "\t" Len
      Taken = ""; String = ""
      for ( i = 2; i <= NF && Taken ~ /^$/; i++) {
         if( $i in aFilter) Taken = aFilter[ $i]
         }
      Take = Taken !~ /^$/ 
      next
      }
   Take && /^SQ/ { Len = $3; next }
   Take && /^[[:blank:]]/ {
      gsub( /[[:blank:]]*/, "")
      String = String $0
      }
   END { if( String !~ /^$/) print Taken "\t" String "\t" Len }
         ' filter.txt YourFile

Не совсем короче, может быть, более общий. Тяжелая часть - извлечь значение, которое служит фильтром из строки

0
NeronLeVelu 22 Окт 2015 в 14:01