Wybierz linie z pliku tekstowego, które mają identyfikatory wymienione w innym pliku

13

Używam dużo grep awk sort w mojej powłoce uniksowej do pracy ze średnimi (około 10M-100M linii) plikami tekstowymi kolumn oddzielonymi tabulatorami. Pod tym względem powłoka unix jest moim arkuszem kalkulacyjnym.

Ale mam jeden ogromny problem, a mianowicie wybór rekordów na podstawie listy identyfikatorów.

Mając table.csvplik w formacie id\tfoo\tbar...i ids.csvplik z listą identyfikatorów, wybieraj tylko rekordy table.csvz obecnym identyfikatorem ids.csv.

rodzaj /programming/13732295/extract-all-lines-from-text-file-based-on-a-given-list-of-ids ale z powłoką, nie perl.

grep -Foczywiście produkuje fałszywie dodatnie, jeśli identyfikatory mają zmienną szerokość. jointo narzędzie, którego nigdy nie zrozumiałem. Przede wszystkim wymaga sortowania alfabetycznego (moje pliki są zwykle sortowane numerycznie), ale nawet wtedy nie mogę uruchomić go bez narzekań na niewłaściwą kolejność i pomijanie niektórych rekordów. Więc mi się nie podoba. grep -f przeciwko plikowi z ^id\t-s jest bardzo wolny, gdy liczba id jest duża. awkjest uciążliwy.

Czy są na to jakieś dobre rozwiązania? Jakieś specjalne narzędzia do plików rozdzielanych tabulatorami? Dodatkowa funkcjonalność będzie również mile widziana.

UPD: Poprawione sort->join

Alamar
źródło
Jeśli grep -fjest zbyt wolny, utrzymanie tej strategii wydaje się większym kłopotem niż jest to warte - odmiany prawdopodobnie padną ofiarą tych samych problemów z wydajnością O (N * M). Może lepiej poświęcić swój czas na naukę korzystania ze znormalizowanej bazy danych SQL ...
goldilocks
1
Dlaczego nie skorzystać ze skryptu Perl z pytania, które podlinkowałeś? Ewentualnie powinno być możliwe napisanie podobnego skryptu awk.
cjm
Bash 4 ma tablice asocjacyjne, które są potrzebne do obejścia zagnieżdżonych pętli na przykład w perlu.
goldilocks
1
sortpotrafi wykonywać wszelkiego rodzaju sortowania, numeryczne, alfabetyczne i inne. Zobaczyć man sort.
terdon
Mam tutaj pytanie, jak to zrobić, jeśli plik źródłowy, z którego chcemy wyodrębnić dane, jest plikiem

Odpowiedzi:

19

Chyba chodziło o grep -fnie grep -F, ale rzeczywiście trzeba kombinacji obu i -w:

grep -Fwf ids.csv table.csv

Powodem, dla którego otrzymywałeś fałszywe alarmy, jest (myślę, że nie wyjaśniłeś), ponieważ jeśli identyfikator może być zawarty w innym, oba zostaną wydrukowane. -wusuwa ten problem i -Fupewnia się, że wzorce są traktowane jak ciągi, a nie wyrażenia regularne. Od man grep:

   -F, --fixed-strings
          Interpret PATTERN as a  list  of  fixed  strings,  separated  by
          newlines,  any  of  which is to be matched.  (-F is specified by
          POSIX.)
   -w, --word-regexp
          Select  only  those  lines  containing  matches  that form whole
          words.  The test is that the matching substring must  either  be
          at  the  beginning  of  the  line,  or  preceded  by  a non-word
          constituent character.  Similarly, it must be either at the  end
          of  the  line  or  followed by a non-word constituent character.
          Word-constituent  characters  are  letters,  digits,   and   the
          underscore.

   -f FILE, --file=FILE
          Obtain  patterns  from  FILE,  one  per  line.   The  empty file
          contains zero patterns, and therefore matches nothing.   (-f  is
          specified by POSIX.)

Jeśli twoje fałszywe alarmy są spowodowane tym, że identyfikator może znajdować się w polu innym niż ID, zamiast tego zapętl plik:

while read pat; do grep -w "^$pat" table.csv; done < ids.csv

lub szybciej:

xargs -I {} grep "^{}" table.csv < ids.csv

Osobiście zrobiłbym to w perl:

perl -lane 'BEGIN{open(A,"ids.csv"); while(<A>){chomp; $k{$_}++}} 
            print $_ if defined($k{$F[0]}); ' table.csv
terdon
źródło
1
+1 Ale: Co zrobić, jeśli istnieją potencjalnie fałszywe alarmy, które pasują do id dokładnie pod względem słownym, ale nie w kolumnie id? Jeśli nie możesz użyć ^z -F, nie możesz celować konkretnie w pierwszą kolumnę.
goldilocks
@goldilocks, jeśli pasują dokładnie, nie są fałszywie pozytywne. Rozumiem, co masz na myśli, ale w takim przypadku PO powinien pokazać swoje pliki wejściowe.
terdon
^id\tNieco z PO oznacza idmoże występować w innej kolumnie. Jeśli nie, to nie ma znaczenia.
goldilocks
@goldilocks fair point, zredagowano odpowiedź
terdon
Sposób, w jaki to robiliśmy, to tworzenie plików tymczasowych (przy użyciu awk lub sed), które dodawały unikalny znak (powiedzmy, control-A) ograniczający pole, które chcemy wyszukać, a następnie użyj grep -F -f temppatternfile temptargetfile | tr -d '\ 001'
Mark Plotnick
7

joinNarzędzie to jest to, co chcesz. Wymaga to sortowania plików wejściowych.

Zakładając, że twoja powłoka to bash lub ksh:

join -t $'\t' <(sort ids.csv) <(sort table.csv)

Bez konieczności sortowania, typowym rozwiązaniem jest awk

awk -F '\t' 'NR==FNR {id[$1]; next} $1 in id' ids.csv table.csv
Glenn Jackman
źródło
Jak próbowałem, ale ostatecznie nie udało mi się przekazać, join jest kludge. Nie działa mi tak dobrze.
alamar
1
jointo nie kludge: twoich słów nie można było rozgryźć. Otwórz umysł i ucz się. Jakie wyniki otrzymałeś i czym to się różni od oczekiwań?
glenn jackman
+1, to jest praca dla join.
don_crissti
awkRozwiązaniem jest tutaj bardzo szybki i skuteczny dla moich celów (jestem wyodrębnianie podzbiorów kilkuset z plików liniami ~ 100m)
Łk
2

Odpowiedzi na to pytanie SO pomogły mi ominąć niggles z dołączeniem. Zasadniczo, gdy sortujesz plik w ramach przygotowań do wysłania go do przyłączenia, musisz upewnić się, że sortujesz na podstawie kolumny, do której dołączasz. Więc jeśli jest to pierwszy, musisz powiedzieć mu, jaki znak separatora znajduje się w pliku i że chcesz, aby sortował według pierwszego pola (i tylko pierwszego pola). W przeciwnym razie, jeśli pierwsze pole ma zmienne szerokości (na przykład), separatory i ewentualnie inne pola mogą zacząć wpływać na porządek sortowania.

Tak więc użyj opcji -t sortowania, aby określić znak rozdzielający, i użyj opcji -k, aby określić pole (pamiętając, że potrzebujesz pola początkowego i końcowego - nawet jeśli jest to to samo - lub będzie sortować od tego znaku do końca linii).

Tak więc w przypadku pliku oddzielonego tabulatorami, jak w tym pytaniu, powinny działać następujące elementy (dzięki odpowiedzi Glenn na strukturę):

join -t$'\t' <(sort -d ids.csv) <(sort -d -t$'\t' -k1,1 table.csv) > output.csv

(Dla odniesienia, flaga -d oznacza sortowanie słownikowe. Możesz także użyć flagi -b, aby zignorować wiodące białe znaki, patrz man sorti man join).

Jako bardziej ogólny przykład, załóżmy, że łączysz dwa pliki oddzielone przecinkami - input1.csvw trzeciej kolumnie i input2.csvczwartej. Możesz użyć

join -t, -1 3 -2 4 <(sort -d -t, -k3,3 input2.csv) <(sort -d -t, -k4,4 input2.csv) > output.csv

W tym miejscu opcje -1i -2określają, które pola dołączyć odpowiednio w pierwszym i drugim pliku wejściowym.

LangeHaare
źródło
0

Możesz także użyć ruby, aby zrobić coś podobnego:

ruby -pe 'File.open("id.csv").each { |i| puts i if i =~ /\$\_/ }' table.csv
Sójka
źródło