Filtra il file per numero di riga


17

Dato un file L con un numero intero non negativo per riga e file di testo F, quale sarebbe un modo rapido per mantenere solo quelle righe in F, il cui numero di riga appare nel file L?

Esempio:

$ cat L.txt
1
3

$ cat F.txt
Hello World
Hallo Welt
Hola mundo

$ command-in-question -x L.txt F.txt
Hello World
Hola mundo

Sto cercando un comando in grado di gestire un file L con 500 milioni o più voci; il file L è ordinato numericamente.

Nota: sono a metà di un'implementazione per un command-in-questionma mi chiedevo solo se uno potesse essere in grado di usare anche alcuni strumenti Unix qui.


Aggiornamento: grazie per tutte le risposte, ho imparato molto oggi! Vorrei accettare più di una risposta, ma non è possibile.


Quante righe ci sono nel file F?
Peter

1
Ci sono almeno tante righe in F come in L.
miku,

Risposte:


8

Con l' Comissione di messaggi di errore significativi:

#include <stdio.h>
#include <stdlib.h>

int main (int argc, char *argv[]) {

    FILE *L;
    FILE *F;

    unsigned int to_print;
    unsigned int current = 0;
    char *line = NULL;
    size_t len = 0;

    if ((L = fopen(argv[1], "r")) == NULL) {
        return 1;
    } else if ((F = fopen(argv[2], "r")) == NULL) {
        fclose(L);
        return 1;
    } else {

        while (fscanf(L, "%u", &to_print) > 0) {
            while (getline(&line, &len, F) != -1 && ++current != to_print);
            if (current == to_print) {
                printf("%s", line);
            }
        }

        free(line);
        fclose(L);
        fclose(F);
        return 0;
    }
}

2
Questa è la risposta più efficace qui. Almeno, è così dai miei test. Nel caso in cui qualcuno è interessato, ho compilato piace: xsel -bo | cc -xc - -o cselect. E ha funzionato: ha solo bisogno delle due librerie.
Mikeserv,

1
Grazie, è fantastico! Spero non ti dispiaccia, ma ho racchiuso il tuo codice in un piccolo strumento .
Miku,

1
@miku Vai avanti, sono felice di poterti aiutare. Ho notato che sei aumentato LINE_MAXnella tua versione, quindi probabilmente lavori con linee molto grandi nei tuoi file. Ho aggiornato A con una versione usando getline()per rimuovere il limite della dimensione della linea.
FloHimself

@FloHimself, beh, grazie ancora:) In effetti, alcune righe di input potrebbero superare LINE_MAX, quindi getlinesembra giusto.
Miku,

10

Userei awk, ma non memorizzerei l'intero contenuto L.txtin memoria e farei inutili ricerche di hash ;-).

list=L.txt file=F.txt
LIST="$list" awk '
  function nextline() {
    if ((getline n < list) <=0) exit
  }
  BEGIN{
    list = ENVIRON["LIST"]
    nextline()
  }
  NR == n {
    print
    nextline()
  }' < "$file"

Esattamente, ho provato le mappe hash e avrebbero superato la memoria; i bitets ti compreranno più spazio per la testa; ma usando il fatto che l'input è ordinato, puoi eliminare completamente questo problema (spaziale).
Miku,

1
@Janis; non è solo un caso di buone pratiche di codifica standard: non scrivere letteralmente nel codice - usa invece le variabili ... (più flessibile e meno soggetto a errori, e più facile da mantenere)
Peter.O

1
@ StéphaneChazelas: ha bisogno di pre-ciclo di inizializzazione n, altrimenti (così com'è) manca 1inL.txt
Peter.O

1
@ Peter.O, oops, è quello che avevo cercato di affrontare con NR> = n, ma era sbagliato. Dovrebbe essere migliore ora.
Stéphane Chazelas,

1
@Janis, l'idea era che se quel codice doveva essere incorporato in uno command-in-questionscript, non è possibile inserire il nome file nel codice. -v list="$opt_x"non funziona neanche a causa del backslash-prcessing fatto da awk su di esso. Ecco perché uso ENVIRON invece qui.
Stéphane Chazelas,

10

grep -n | sort | sed | cut

(   export LC_ALL=C
    grep -n ''   | sort -t:  -nmk1,1 ./L - |
    sed /:/d\;n  | cut  -sd: -f2-
)   <./F

Dovrebbe funzionare abbastanza rapidamente (alcuni test a tempo sono inclusi di seguito) con input di qualsiasi dimensione. Alcune note su come:

  • export LC_ALL=C
    • Poiché il punto della seguente operazione è ottenere l'intero file di ./Fstacking in linea con il suo ./Lfile lineno, gli unici caratteri di cui dovremo davvero preoccuparci sono le [0-9]cifre ASCII e i :due punti.
    • Per questo motivo è più semplice preoccuparsi di trovare quegli 11 caratteri in un set di 128 possibili che non se UTF-8 è altrimenti coinvolto.
  • grep -n ''
    • Ciò inserisce la stringa LINENO:nella testa di ogni riga in stdin - o <./F.
  • sort -t: -nmk1,1 ./L -
    • sorttrascura di ordinare i suoi file di input, e invece (correttamente) presume che siano preordinati e -mli rinnova in -numericallyordine, ignorando praticamente qualsiasi cosa al di là di ogni possibile carattere di due punti -k1,1presente -t:.
    • Anche se questo può richiedere un po 'di spazio temporaneo (a seconda di quanto distanti possono verificarsi alcune sequenze) , non richiederà molto rispetto a un ordinamento corretto e sarà molto veloce perché comporta zero backtracking.
    • sortprodurrà un singolo flusso in cui qualsiasi lineno in ./Lprecederà immediatamente le corrispondenti linee in ./F. ./LLe linee vengono sempre prima perché sono più brevi.
  • sed /:/d\;n
    • Se la riga corrente corrisponde a /:/due punti, deliminala dall'output. Altrimenti, stampa automaticamente la nriga corrente ed ext.
    • E così sedelimina sortl'output solo per coppie di linee sequenziali che non corrispondono ai due punti e alla seguente riga - o, solo a una linea da ./Le poi alla successiva.
  • cut -sd: -f2-
    • cut -ssopprime dall'output quelle delle sue linee di input che non contengono almeno una delle sue -d:stringhe di eliminazione - e quindi ./Lle linee vengono potate completamente.
    • Per quelle righe che lo fanno, il loro primo campo :delimitato da due punti -fè cutassente - e così va tutto grepil lineno inserito.

piccolo test di input

seq 5 | sed -ne'2,3!w /tmp/L
        s/.*/a-z &\& 0-9/p' >/tmp/F

... genera 5 righe di input di esempio. Poi...

(   export LC_ALL=C; </tmp/F \
    grep -n ''   | sort -t:  -nmk1,1 ./L - |
    sed /:/d\;n  | cut  -sd: -f2-
)|  head - /tmp[FL]

... stampe ...

==> standard input <==
a-z 1& 0-9
a-z 4& 0-9
a-z 5& 0-9

==> /tmp/F <==
a-z 1& 0-9
a-z 2& 0-9
a-z 3& 0-9
a-z 4& 0-9
a-z 5& 0-9

==> /tmp/L <==
1
4
5

test a tempo più grandi

Ho creato un paio di file piuttosto grandi:

seq 5000000 | tee /tmp/F |
sort -R | head -n1500000 |
sort -n >/tmp/L

... che inserisce 5mil di linee /tmp/Fe 1,5mil di linee selezionate casualmente in quella /tmp/L. Ho quindi fatto:

time \
(   export LC_ALL=C
    grep -n ''   | sort -t:  -nmk1,1 ./L - |
    sed /:/d\;n  | cut  -sd: -f2-
)   <./F |wc - l

Ha stampato:

1500000
grep -n '' \
    0.82s user 0.05s system 73% cpu 1.185 total
sort -t: -nmk1,1 /tmp/L - \
    0.92s user 0.11s system 86% cpu 1.185 total
sed /:/d\;n \
    1.02s user 0.14s system 98% cpu 1.185 total
cut -sd: -f2- \
    0.79s user 0.17s system 80% cpu 1.184 total
wc -l \
    0.05s user 0.07s system 10% cpu 1.183 total

(Ho aggiunto le barre rovesciate lì)

Tra le soluzioni attualmente offerte qui, questa è la più veloce di tutte tranne una se confrontata con il set di dati generato sopra sulla mia macchina. Degli altri solo uno si è avvicinato a contendersi il secondo posto, e questo è meuh perl qui .

Questa non è affatto la soluzione originale offerta: ha perso un terzo dei suoi tempi di esecuzione grazie ai consigli / ispirazioni offerti da altri. Vedi la cronologia dei post per soluzioni più lente (ma perché?) .

Inoltre, vale la pena notare che alcune altre risposte potrebbero contendere meglio se non fosse per l'architettura multi-CPU del mio sistema e l'esecuzione simultanea di ciascuno dei processi in quella pipeline. Funzionano tutti allo stesso tempo - ognuno sul proprio core del processore - passando attorno ai dati e facendo la loro piccola parte del tutto. È molto bello.

ma la soluzione più veloce è ...

Ma non è la soluzione più veloce. La soluzione più veloce offerto qui, mani verso il basso, è il programma di C . L'ho chiamato cselect. Dopo averlo copiato negli appunti X, l'ho compilato come segue:

xsel -bo | cc -xc - -o cselect

Ho quindi fatto:

time \
    ./cselect /tmp/L /tmp/F |
wc -l

... e i risultati sono stati ...

1500000
./cselect /tmp/L /tmp/F  \
    0.50s user 0.05s system 99% cpu 0.551 total
wc -l \
    0.05s user 0.05s system 19% cpu 0.551 total

1
Puoi renderlo significativamente più veloce (quasi quanto il mio sui sistemi multi-core) con sed -ne'/:/!{n;p;}' | cut -d: -f2-invece dised -ne'/:/!N;/\n/s/[^:]*://p'
Stéphane Chazelas,

@ StéphaneChazelas - potresti ottenere risultati migliori se cambi seds - il sedche sto usando è il cimelio sed- puoi vedere il aliasvalore nei timerisultati. Il mio pacchetto di cimelio, a proposito, è staticamente compilato contro una musl libc - l'implementazione di regex per la quale si basa su TRE . Quando lo cambio su GNU sed- e lo eseguo senza cut- aggiunge un secondo intero al tempo di completamento (2,8 secondi) - lo compone di oltre un terzo. E questo è solo .3 secondi più veloce del tuo sul mio sistema.
Mikeserv,

1
sort -mnal contrario sort -nmk1,1potrebbe essere migliore in quanto non è necessario eseguire la divisione qui (non testato)
Stéphane Chazelas

@ StéphaneChazelas - sì, ho pensato lo stesso e l'ho provato in tutti i modi. -nè fatto solo per fare la prima stringa numerica su una riga, quindi ho pensato, ok -mno -nme, per qualsiasi motivo, le uniche volte in cui è mai scesa sotto i 2 secondi nel tempo di completamento è stata quando ho aggiunto tutte le opzioni così come sono. È strano - ed è la ragione per cui ieri non ho attaccato -min primo luogo - sapevo di cosa parlavo, ma sembrava solo funzionare come una sorta di ottimizzazione automatica. È interessante notare che il cimelio sortha -zun'opzione di lunghezza di stringa che si applica solo a -[cm]....
Mikeserv

-nnon è la prima stringa numerica sulla riga . Considera solo la linea come un numero, quindi abc 123sarebbe 0. Quindi non può essere meno efficiente rispetto a-t: -k1,1
Stéphane Chazelas,

9

Vorrei usare awk:

awk 'NR==FNR {a[$1]; next}; FNR in a' L.txt F.txt

Aggiornamento: ho eseguito misure di prestazione; sembra che questa versione si ridimensioni ancora meglio con set di dati molto grandi (come nel caso dei requisiti dichiarati), poiché il confronto è molto veloce e compensa gli sforzi necessari per costruire la tabella hash.


1
@miku; Sì, è una bella soluzione compatta. Ma un avvertimento; non tutti awkpotrebbero essere in grado di gestire insiemi di dati così enormi. - Sto usando GNU awke non ci sono problemi; il test con 500 milioni di righe di dati ha richiesto 7 minuti.
Janis,

1
Questo piuttosto lento (al confronto) real 16m3.468s- user 15m48.447s- sys 0m10.725s. Ha usato 3,3 GB di RAM testando una dimensione di 1/10 Lcon 50.000.000 di linee; e Fcon 500.000.000 di righe - vs time for awk anser di Stéphane Chazelas: real 2m11.637s- user 2m2.748s- sys 0m6.424s- Non sto usando una scatola veloce, ma il confronto è interessante.
Peter

@ Peter.O; Grazie per i dati! Una velocità più lenta era prevedibile, dato che (nel mio caso di test) mezzo miliardo di linee erano memorizzate in un array associativo. (Ecco perché ho commentato "(+1)" sopra per la proposta di Stephane.) - Anche se ero stupito che questa soluzione concisa stesse ancora elaborando 1 milione di righe al secondo! Penso che renda questo modello di codice (per la sua semplicità!) Un'opzione praticabile, e in particolare in casi con dimensioni di dati meno estreme.
Janis,

È sicuramente una soluzione praticabile. Sui dati del test che ho usato (5mil linee / 1,5mil L) il tuo è stato completato in poco più di 4 secondi - solo un secondo dietro la risposta di Stephane. Il codice utilizzato per GEN il PRO è nella mia risposta, ma è soprattutto solo sequscita e quindi un più piccolo, sottoinsieme scelto a caso di stesso in L .
Mikeserv,

1
Ho appena preso alcune misure di prestazioni in più con una dimensione del file di dati di 500 milioni di righe e una dimensione del file chiave di 50 milioni e risp. 500 milioni di linee, con un'osservazione degna di nota. Con il file chiave più piccolo i tempi sono 4 min (Stephane) contro 8 min (Janis), mentre con il file chiave più grande sono 19 min (Stephane) contro 12 min (Janis).
Janis,

3

Solo per completezza: possiamo unire l'eccellente script awk nella risposta di Stéphane Chazelas e lo script perl nella risposta di kos ma senza tenere l'intero elenco in memoria, nella speranza che il perl sia più veloce di awk. (Ho cambiato l'ordine di args in modo che corrisponda alla domanda originale).

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

die "Usage: $0 l f\n" if $#ARGV+1 != 2;
open(L,$ARGV[0]) or die "$ARGV[0]: $!";
open(F,$ARGV[1]) or die "$ARGV[1]: $!";

while(my $number = <L>){
    #chop $number;
    while (<F>) {
        if($. == $number){
            print;
            last;
        }
    }
}

Questo è molto più veloce del awk. È veloce quasi quanto il mio: ho provato entrambe le volte tre volte e ogni volta il mio ha gestito il mio set di test da 5 miglia in 1,8 ... secondi e 1,9 ... secondi ogni volta. Il codice gen testset è nella mia risposta se ti interessa, ma il punto è che è molto buono. Inoltre, l'output è corretto - Non riesco ancora a far awkfunzionare il lavoro ... Tuttavia, entrambe le nostre risposte sono vergognose di FloHimself .
Mikeserv,

@mikeserv, dobbiamo avere diversi awks. Nel tuo campione, ottengo 1.4s con gawk (4s per Janis), 0.9s con mawk, 1.7s con questa soluzione perl, 2.3s con kos ', 4.5s con il tuo (GNU sed) e 1.4s con il tuo ( GNU sed) e il mio miglioramento suggerito (e 0,5 per la soluzione C).
Stéphane Chazelas,

@mikeserv, ah! ovviamente con il tuo approccio, la localizzazione fa la differenza. Qui sotto da 4.5s a 2.3s quando si passa da UFT-8 a C.
Stéphane Chazelas,

3

Ho scritto un semplice script Perl per farlo:

Usage: script.pl inputfile_f inputfile_f

#!/usr/bin/env perl

$number_arguments = $#ARGV + 1;
if ($number_arguments != 2) {
    die "Usage: script.pl inputfile_f inputfile_l\n";
}

open($f, '<', $ARGV[0])
    or die "$ARGV[0]: Not found\n";
open($l, '<', $ARGV[1])
    or die "$ARGV[1]: Not found\n";

@line_numbers = <$l>;

while ($line = <$f>) {
    $count_f ++;
    if ($count_f == @line_numbers[$count_l]) {
        print $line;
        $count_l ++;
    }
}
  • carichi F.txt
  • carichi L.txt
  • Memorizza ciascuna riga di L.txtin un array
  • Legge F.txtriga per riga, tenendo traccia del numero di riga corrente e dell'indice di array corrente; aumenta il F.txtnumero di riga corrente; se il F.txtnumero di riga corrente corrisponde al contenuto dell'array nell'indice di array corrente, stampa la riga corrente e aumenta l'indice

Considerazioni su costi e complessità :

Considerando il costo per eseguire le assegnazioni, il costo per effettuare i confronti e il costo per stampare le linee, dato N 1 come numero di linee in F.txte N 2 come numero di linee in L.txt, il whileciclo viene eseguito al massimo N 1 volte, portando a 2N 1 + N 2 incarichi (ovviamente assumendo N 1 > N 2 ), a 2N 1 confronti e N 2 stampe; dato come uguale al costo di ogni operazione, il costo totale per eseguire il whileloop è 4N 1 + 2N 2 , il che porta a una complessità dello script di O (N).

Test su un file di input di 10 milioni di righe :

Utilizzo di un file di 10 milioni di righe F.txtcontenente righe casuali di 50 caratteri e un file di 10 milioni di righe L.txtcontenente numeri da 1 a 10000000 (scenario peggiore):

~/tmp$ for ((i=0; i<3; i++)); do time ./script.pl F.txt L.txt > output; done

real    0m15.628s
user    0m13.396s
sys 0m2.180s

real    0m16.001s
user    0m13.376s
sys 0m2.436s

real    0m16.153s
user    0m13.564s
sys 0m2.304s

2

Questa soluzione perl è più veloce delle altre soluzioni awk o perl del 20% o giù di lì, ma ovviamente non è veloce come la soluzione in C.

perl -e '
  open L, shift or die $!;
  open F, shift or die $!;
  exit if ! ($n = <L>);
  while (1) {
    $_ = <F>;
    next if $. != $n;
    print;
    exit if ! ($n = <L>);
  }
' -- L F

0
cat <<! >L.txt
1
3
!

cat <<! >F.txt
Hello World
Hallo Welt
Hola mundo
!

cmd(){
 L=$1 F=$2
 cat -n $F |
 join $L - |
 sed 's/[^ ]* //'
}

cmd L.txt F.txt
Hello World
Hola mundo

Poiché L.txt è ordinato, è possibile utilizzare join. Basta numerare ogni riga in F.txt, unire i due file, quindi rimuovere il numero di riga. Non sono necessari file intermedi di grandi dimensioni.

In realtà, quanto sopra rovinerà le tue linee dati sostituendo tutto lo spazio bianco con un singolo spazio. Per mantenere intatta la linea devi scegliere come delimitatore un carattere che non appare nei tuoi dati, ad es. "|". Il cmd è quindi

cmd(){
 L=$1 F=$2
 cat -n $F |
 sed 's/^ *//;s/\t/|/' |
 join -t'|' $L - |
 sed 's/[^|]*|//'
}

Il primo sed rimuove gli spazi iniziali dall'output "cat -n" e sostituisce la scheda. Il secondo sed rimuove il numero di riga e "|".


Temo che non funzionerà su file più grandi. Ha bisogno di <10 righe. Ho avuto la stessa idea e ho provato, join L.txt <(nl F.txt )ma non funzionerà su file di grandi dimensioni. Benvenuto sul sito, a proposito, spesso non riceviamo risposte così chiare e ben formattate dai nuovi utenti!
terdon

@terdon, Sì, un peccato che join/ commnon può funzionare con input ordinati numericamente.
Stéphane Chazelas,

@terdon: ho seguito il tuo esempio (ora eliminato) e ho provato join -t' ' <(<L.txt awk '{printf("%010s\n",$0)}') <(<F.txt awk '{printf("%010s %s\n",NR,$0)}') | cut -d' ' -f2-- È stato lento! - e anche quando ho inserito file preparati con tasti adatti a 0 join -t' ' L.txt F.txt | cut -d' ' -f2- , era ancora lento (escluso il tempo di preparazione) - più lento della awkrisposta di @Janis (dove ho pubblicato un commento sui tempi effettivi presi per entrambi la sua e la risposta di @ StéphaneChazelas
Peter.O,

@ Peter.O sì. Ho provato un approccio simile che evita uno dei problemi, ma non sono riuscito a trovare un modo per farlo funzionare e valerne la pena.
terdon

@terdon e altri: il tempo effettivo per il join+ awk printf substiturion processo era real 20m11.663s user 19m35.093s sys 0m10.513s contro Stéphane Chazelas' real 2m11.637s user 2m2.748s sys 0m6.424s con L50 milioni di linee, F500 milioni di linee.
Peter

0

Per completezza, un altro tentativo di joinsoluzione:

sed -r 's/^/00000000000000/;s/[0-9]*([0-9]{15})/\1/' /tmp/L | join <( nl -w15 -nrz /tmp/F ) - | cut -d' ' -f2-

Questo funziona formattando la colonna del numero di riga che unisce funziona come lunghezza fissa con zeri iniziali, in modo che i numeri siano sempre lunghi 15 cifre. Ciò elude il problema del join che non gradisce il normale ordinamento numerico, poiché la colonna è stata effettivamente forzata ad essere ordinata dal dizionario. nlviene utilizzato per aggiungere numeri di riga in questo formato a F.txt. Purtroppo è sednecessario utilizzare per riformattare la numerazione in L.txt.

Questo approccio sembra funzionare correttamente sui dati di test generati utilizzando il metodo di @ mikeserv. Ma è ancora molto lento: la soluzione c è 60 volte più veloce sulla mia macchina. circa i 2/3 del tempo viene impiegato sede 1/3 del tempo join. Forse c'è un'espressione sed migliore ...


Ok - ma perché stiamo anteponendo tutti gli zeri? Sto cercando di farmi un'idea di questo. Inoltre, nlè fantastico, ma non è possibile utilizzarlo in modo affidabile su input non testati. Una delle cose che lo rende così interessante è il suo elimitatore di pagine -d logiche. Per impostazione predefinita, se c'è una linea in input composta solo dalle stringhe :\` (ma senza la fossa finale) 1, 2, 3 o tre volte in successione, i tuoi conteggi diventeranno un po 'pazzi. Sperimenta con esso - è abbastanza pulito. Soprattutto dai un'occhiata a cosa succede quando nl` legge una riga con 1 stringa delimitatore e poi un'altra con w / 3 o 2
mikeserv

0

Dato che la risposta accettata è in C, ho pensato che fosse giusto gettare una soluzione Python qui:

# Read mask
with open('L.txt', 'r') as f:
    mask = [int(line_num) for line_num in f.read().splitlines()]

# Filter input file
filtered_lines = []
with open('F.txt', 'r') as f:
    for i, line in enumerate(f.read().splitlines()):
        if (i+1) in mask:
            filtered_lines.append(line)

# Write newly filtered file
with open('F_filtered.txt', 'w') as f:
    for line in filtered_lines:
        f.write('%s\n' % line)

Se si utilizza una libreria esterna come numpy, una soluzione sembrerebbe ancora più elegante:

import numpy as np

with open('L.txt', 'r') as f:
    mask = np.array([int(line_num)-1 for line_num in f.read().splitlines()])

with open('F.txt', 'r') as f:
    lines = np.array(f.read().splitlines())
filtered_lines = lines[mask]

with open('F_filtered.txt', 'w') as f:
    for line in filtered_lines:
        f.write('%s\n' % line)
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.