Seleziona le righe dal file di testo che hanno gli ID elencati in un altro file


13

Uso un sacco di grek awk sort nella mia shell unix per lavorare con file di testo di colonne separati da tab di medie dimensioni (circa 10M-100M). A questo proposito unix shell è il mio foglio di calcolo.

Ma ho un grosso problema, ovvero la selezione dei record con un elenco di ID.

Avendo table.csvfile con formato id\tfoo\tbar...e ids.csvfile con elenco di ID, seleziona solo i record table.csvcon ID presente in ids.csv.

tipo di /programming/13732295/extract-all-lines-from-text-file-based-on-a-given-list-of-ids ma con shell, non perl.

grep -Fproduce ovviamente falsi positivi se gli ID hanno una larghezza variabile. joinè un'utilità che non avrei mai potuto capire. Prima di tutto, richiede un ordinamento alfabetico (i miei file sono in genere ordinati numericamente), ma anche in questo caso non riesco a farlo funzionare senza lamentarmi di un ordine errato e saltando alcuni record. Quindi non mi piace. grep -f contro il file con ^id\t-s è molto lento quando il numero di ID è grande. awkè ingombrante.

Ci sono buone soluzioni per questo? Qualche strumento specifico per i file separati da tabulazione? Anche le funzionalità extra saranno le benvenute.

UPD: corretto sort->join


Se grep -fè troppo lento, mantenere questa strategia suona come più problemi di quanti ne valga la pena - le variazioni probabilmente cadranno in preda agli stessi problemi di prestazioni O (N * M). Forse il tuo tempo sarebbe meglio se impari a usare un SQL DB normalizzato ...
goldilocks,

1
Perché non usare lo script Perl dalla domanda che hai collegato? In alternativa, dovrebbe essere possibile scrivere uno script simile in awk.
cjm

Bash 4 ha array associativi, che è ciò di cui hai bisogno per aggirare i loop nidificati come nell'esempio perl.
Riccioli d'oro

1
sortpuò fare tutti i tipi di ordinamento, numerico, alfabetico e altri. Vedere man sort.
terdon

Ho una domanda qui, come possiamo fare lo stesso se il file sorgente da cui vogliamo estrarre i dati è un file non delimitato

Risposte:


19

Credo che si intende grep -fnon grep -F, ma in realtà bisogno di una combinazione di entrambi e -w:

grep -Fwf ids.csv table.csv

Il motivo per cui stavi ottenendo falsi positivi è (immagino, non hai spiegato) perché se un ID può essere contenuto in un altro, entrambi verranno stampati. -wrimuove questo problema e si -Fassicura che i tuoi pattern siano trattati come stringhe, non come espressioni regolari. Da 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.)

Se i tuoi falsi positivi sono dovuti al fatto che un ID può essere presente in un campo non ID, scorrere invece il tuo file:

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

o, più veloce:

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

Personalmente, lo farei perlcomunque:

perl -lane 'BEGIN{open(A,"ids.csv"); while(<A>){chomp; $k{$_}++}} 
            print $_ if defined($k{$F[0]}); ' table.csv

1
+1 Ma: cosa succede se ci sono potenziali falsi positivi che corrispondono esattamente all'ID in termini di parole, ma non nella colonna ID? Se non puoi usare ^con -F, non puoi scegliere come target la prima colonna.
Riccioli d'oro

@goldilocks se corrispondono esattamente, non sono falsi positivi. Ho capito cosa intendi, ma in quel caso l'OP dovrebbe mostrare i loro file di input.
terdon

Il ^id\tbit dell'OP implica idpotrebbe verificarsi in un'altra colonna. In caso contrario, questo non ha importanza.
Riccioli d'oro

@goldilocks fair point, risposta modificata.
terdon

Il modo in cui lo facevamo era creare file temporanei (usando awk o sed) che aggiungessero un carattere unico (diciamo, control-A) che delimita il campo che volevamo cercare, quindi usa grep -F -f temppatternfile temptargetfile | tr -d '\ 001'
Mark Plotnick,

7

L' joinutilità è ciò che vuoi. Richiede che i file di input siano ordinati in modo lessicale.

Supponendo che la tua shell sia bash o ksh:

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

Senza la necessità di ordinare, la solita soluzione awk è

awk -F '\t' 'NR==FNR {id[$1]; next} $1 in id' ids.csv table.csv

Come ho provato, ma alla fine non sono riuscito a comunicare, join è un kludge. Non funziona così bene per me.
alamar,

1
joinnon è un kludge: le tue parole se non riuscissi a capirlo. Apri la tua mente e impara. Quale output hai ottenuto e in che modo differisce da quello che ti aspetti?
Glenn Jackman,

+1, questo è un lavoro per join.
don_crissti,

La awksoluzione qui è molto rapida ed efficiente per i miei scopi (sto estraendo sottoinsiemi di alcune centinaia di file da file con ~ 100 milioni di righe)
Luca

2

Le risposte a questa domanda SO mi hanno aiutato a aggirare i nervi con join. In sostanza, quando si ordina il file in preparazione per inviarlo a join, è necessario assicurarsi che si stia ordinando in base alla colonna su cui si unisce. Quindi, se questo è il primo, devi dirgli quale sia il carattere separatore nel file e che vuoi che sia ordinato sul primo campo (e solo sul primo campo). Altrimenti se il primo campo ha larghezze variabili (ad esempio), i tuoi separatori e forse altri campi potrebbero iniziare a influenzare l'ordinamento.

Quindi, usa l'opzione -t di ordinamento per specificare il tuo carattere di separazione e usa l'opzione -k per specificare il campo (ricordando che hai bisogno di un campo di inizio e fine - anche se è lo stesso - o ordinerà da quel carattere fino alla fine della linea).

Quindi, per un file separato da tabulazioni come in questa domanda, dovrebbe funzionare quanto segue (grazie alla risposta di Glenn per la struttura):

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

(Per riferimento, il flag -d indica l'ordinamento del dizionario. È inoltre possibile utilizzare il flag -b per ignorare gli spazi bianchi iniziali, vedere man sorte man join).

Come esempio più generale, supponiamo di unire due file separati da virgola: input1.csvnella terza colonna e input2.csvnella quarta. Puoi usare

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

Qui le opzioni -1e -2specificano su quali campi unire rispettivamente il primo e il secondo file di input.


0

Puoi anche usare il rubino per fare qualcosa di simile:

ruby -pe 'File.open("id.csv").each { |i| puts i if i =~ /\$\_/ }' table.csv
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.