Strumento da riga di comando per "cat" espansione a coppie di tutte le righe in un file


13

Supponiamo che io abbia un file (chiamalo sample.txt) che assomiglia a questo:

Row1,10
Row2,20
Row3,30
Row4,40

Voglio essere in grado di lavorare su un flusso da questo file che è essenzialmente la combinazione a coppie di tutte e quattro le righe (quindi dovremmo finire con 16 in totale). Ad esempio, sto cercando un comando di streaming (ovvero efficiente) in cui l'output sia:

Row1,10 Row1,10
Row1,10 Row2,20
Row1,10 Row3,30
Row1,10 Row4,40
Row2,20 Row1,10
Row1,20 Row2,20
...
Row4,40 Row4,40

Il mio caso d'uso è che voglio trasmettere questo output in un altro comando (come awk) per calcolare alcune metriche su questa combinazione a coppie.

Ho un modo per farlo in awk, ma la mia preoccupazione è che il mio uso del blocco END {} significhi che sto fondamentalmente memorizzando l'intero file in memoria prima dell'output. Codice di esempio:

awk '{arr[$1]=$1} END{for (a in arr){ for (a2 in arr) { print arr[a] " " arr[a2]}}}' samples/rows.txt 
Row3,30 Row3,30
Row3,30 Row4,40
Row3,30 Row1,10
Row3,30 Row2,20
Row4,40 Row3,30
Row4,40 Row4,40
Row4,40 Row1,10
Row4,40 Row2,20
Row1,10 Row3,30
Row1,10 Row4,40
Row1,10 Row1,10
Row1,10 Row2,20
Row2,20 Row3,30
Row2,20 Row4,40
Row2,20 Row1,10
Row2,20 Row2,20

Esiste un modo di streaming efficiente per farlo senza dover essenzialmente archiviare il file in memoria e quindi emetterlo nel blocco END?


1
Devi sempre leggere un file fino alla fine prima di poter iniziare a produrre output per la seconda riga dell'altro file. L'altro file è possibile eseguire lo streaming.
reinierpost,

Risposte:


12

Ecco come farlo in awk in modo che non debba archiviare l'intero file in un array. Questo è fondamentalmente lo stesso algoritmo di Terdon.

Se lo desideri, puoi anche assegnargli più nomi di file sulla riga di comando e elaborerà ogni file in modo indipendente, concatenando i risultati insieme.

#!/usr/bin/awk -f

#Cartesian product of records

{
    file = FILENAME
    while ((getline line <file) > 0)
        print $0, line
    close(file)
}

Sul mio sistema, questo funziona in circa 2/3 del tempo della soluzione perl di terdon.


1
Grazie! Tutte le soluzioni a questo problema sono state fantastiche, ma alla fine ho optato per questo per 1) semplicità e 2) rimanere in awk. Grazie!
Tom Hayden,

1
Sono contento che ti piaccia, Tom. Tendo a programmare principalmente in Python in questi giorni, ma mi piace ancora awk per l'elaborazione del testo riga per riga a causa dei suoi loop integrati su linee e file. Ed è spesso più veloce di Python.
PM 2Ring

7

Non sono sicuro che sia meglio che farlo in memoria, ma con una sedche rsegna la sua infile per ogni linea nella sua infile e un'altra dall'altra parte di una pipe alternando il Hvecchio spazio con le linee di input ...

cat <<\IN >/tmp/tmp
Row1,10
Row2,20
Row3,30
Row4,40
IN

</tmp/tmp sed -e 'i\
' -e 'r /tmp/tmp' | 
sed -n '/./!n;h;N;/\n$/D;G;s/\n/ /;P;D'

PRODUZIONE

Row1,10 Row1,10
Row1,10 Row2,20
Row1,10 Row3,30
Row1,10 Row4,40
Row2,20 Row1,10
Row2,20 Row2,20
Row2,20 Row3,30
Row2,20 Row4,40
Row3,30 Row1,10
Row3,30 Row2,20
Row3,30 Row3,30
Row3,30 Row4,40
Row4,40 Row1,10
Row4,40 Row2,20
Row4,40 Row3,30
Row4,40 Row4,40

L'ho fatto in un altro modo. Memorizza alcuni in memoria - memorizza una stringa come:

"$1" -

... per ogni riga nel file.

pairs(){ [ -e "$1" ] || return
    set -- "$1" "$(IFS=0 n=
        case "${0%sh*}" in (ya|*s) n=-1;; (mk|po) n=+1;;esac
        printf '"$1" - %s' $(printf "%.$(($(wc -l <"$1")$n))d" 0))"
    eval "cat -- $2 </dev/null | paste -d ' \n' -- $2"
}

È molto veloce. È catil file tante volte quante sono le righe nel file a |pipe. Dall'altro lato della pipa quell'input viene unito al file stesso tante volte quante sono le righe nel file.

La caseroba è solo per la portabilità - yashe zshsia aggiunta un elemento di per la scissione, mentre mkshe poshentrambi perde una. ksh, dash, busybox, E bashtutto diviso fuori esattamente come molti campi come ci sono zeri come stampato da printf. Come scritto sopra, si ottengono gli stessi risultati per ognuna delle shell sopra menzionate sulla mia macchina.

Se il file è molto lungo, potrebbero esserci $ARGMAXproblemi con troppi argomenti nel qual caso dovresti introdurre xargso simili.

Dato lo stesso input che ho usato prima che l'output fosse identico. Ma se dovessi diventare più grande ...

seq 10 10 10000 | nl -s, >/tmp/tmp

Ciò genera un file quasi identico a quello che ho usato prima (senza "Row") , ma a 1000 righe. Puoi vedere tu stesso quanto è veloce:

time pairs /tmp/tmp |wc -l

1000000
pairs /tmp/tmp  0.20s user 0.07s system 110% cpu 0.239 total
wc -l  0.05s user 0.03s system 32% cpu 0.238 total

A 1000 linee c'è una leggera variazione nelle prestazioni tra le shell - bashè invariabilmente la più lenta - ma poiché l'unico lavoro che fanno comunque è generare la stringa arg (1000 copie di filename -) l'effetto è minimo. La differenza di prestazioni tra zsh- come sopra - ed bashè qui al centesimo di secondo.

Ecco un'altra versione che dovrebbe funzionare per un file di qualsiasi lunghezza:

pairs2()( [ -e "$1" ] || exit
    rpt() until [ "$((n+=1))" -gt "$1" ]
          do printf %s\\n "$2"
          done
    [ -n "${1##*/*}" ] || cd -P -- "${1%/*}" || exit
    : & set -- "$1" "/tmp/pairs$!.ln" "$(wc -l <"$1")"
    ln -s "$PWD/${1##*/}" "$2" || exit
    n=0 rpt "$3" "$2" | xargs cat | { exec 3<&0
    n=0 rpt "$3" p | sed -nf - "$2" | paste - /dev/fd/3
    }; rm "$2"
)

Crea un soft-link al suo primo argomento in /tmpcon un nome semi-casuale in modo che non si blocchi su nomi di file strani. Questo è importante perché catgli arg vengono alimentati tramite una pipe xargs. catL'output viene salvato <&3mentre viene sed pstampata ogni riga nel primo argomento tante volte quante sono le righe in quel file - e il suo script viene anche inviato ad esso tramite una pipe. pasteUnisce nuovamente il suo input, ma questa volta prende solo due argomenti -ancora per il suo input standard e il nome del collegamento /dev/fd/3.

Quest'ultimo - il /dev/fd/[num]collegamento - dovrebbe funzionare su qualsiasi sistema Linux e molti altri ancora, ma se non crea una pipa denominata mkfifoe ne utilizza invece dovrebbe funzionare anche.

L'ultima cosa che fa è rmil soft link che crea prima di uscire.

Questa versione è in realtà ancora più veloce sul mio sistema. Immagino sia perché, sebbene esegua più applicazioni, inizia immediatamente a consegnare loro i loro argomenti, mentre prima li impilava tutti per primi.

time pairs2 /tmp/tmp | wc -l

1000000
pairs2 /tmp/tmp  0.30s user 0.09s system 178% cpu 0.218 total
wc -l  0.03s user 0.02s system 26% cpu 0.218 total

La funzione coppie suppone di essere in un file, se non come la dichiareresti?

@Jidder - come dovrei dichiarare cosa? Puoi semplicemente copiarlo + incollarlo in un terminale, no?
Mikeserv,

1
Dichiarare la funzione. Quindi puoi! Pensavo che avresti evitato le newline, sono diffidente nel solo incollare il codice, grazie comunque :) Anche questa è una risposta estremamente veloce, bella!

@Jidder - Di solito scrivo questi in una shell dal vivo solo usando ctrl+v; ctrl+jper ottenere nuove righe come faccio io.
Mikeserv,

@Jidder - grazie mille. Ed è saggio essere cauti - bene per te. Funzioneranno anche in un file: puoi copiarlo in e . ./file; fn_namein quel caso.
Mikeserv,

5

Bene, potresti sempre farlo nella tua shell:

while read i; do 
    while read k; do echo "$i $k"; done < sample.txt 
done < sample.txt 

È molto più lento della tua awksoluzione (sulla mia macchina, ci sono voluti ~ 11 secondi per 1000 linee, contro ~ 0,3 secondi in awk) ma almeno non contiene mai più di un paio di linee in memoria.

Il ciclo sopra funziona per i dati molto semplici che hai nel tuo esempio. Si strozzerà con le barre rovesciate e divorerà gli spazi finali. Una versione più robusta della stessa cosa è:

while IFS= read -r i; do 
    while IFS= read -r k; do printf "%s %s\n" "$i" "$k"; done < sample.txt 
done < sample.txt 

Un'altra scelta è quella di utilizzare perlinvece:

perl -lne '$line1=$_; open(A,"sample.txt"); 
           while($line2=<A>){printf "$line1 $line2"} close(A)' sample.txt

Lo script sopra leggerà ogni riga del file di input ( -ln), lo salverà come $l, si aprirà di sample.txtnuovo e stamperà ogni riga insieme $l. Il risultato sono tutte le combinazioni a coppie mentre solo 2 righe sono mai memorizzate. Sul mio sistema, ci sono voluti solo circa 0.6secondi su 1000 linee.


Wow grazie! Mi chiedo perché la soluzione perl sia molto più veloce della dichiarazione bash while
Tom Hayden,

@TomHayden sostanzialmente perché perl, come awk, è molto più veloce di bash.
terdon

1
Ho dovuto votare per il tuo ciclo while. 4 diverse cattive pratiche lì dentro. Lo sai meglio.
Stéphane Chazelas,

1
@ StéphaneChazelas bene, in base alla tua risposta qui , non riuscivo a pensare a nessun caso in cui il echoproblema potesse essere un problema. Quello che avevo scritto (ho aggiunto printfora) dovrebbe funzionare con tutti loro, giusto? Per quanto riguarda il whileloop, perché? Cosa c'è che non va while read f; do ..; done < file? Sicuramente non stai suggerendo un forciclo! Qual è l'altra alternativa?
terdon

2
@cuonglm, quello suggerisce solo una possibile ragione per cui bisogna evitarlo. Tra gli aspetti concettuali , affidabilità , leggibilità , prestazioni e sicurezza , che copre solo l' affidabilità .
Stéphane Chazelas,

4

Con zsh:

a=(
Row1,10
Row2,20
Row3,30
Row4,40
)
printf '%s\n' $^a' '$^a

$^asu un array attiva l'espansione di tipo parentesi graffa (come in {elt1,elt2}) per l'array.


4

È possibile compilare questo codice per risultati abbastanza rapidi.
Si completa in circa 0,19 - 0,27 secondi su un file di 1000 righe.

Attualmente legge le 10000righe in memoria (per velocizzare la stampa sullo schermo) che se avessi 1000caratteri per riga utilizzeresti meno 10mbmemoria che non penserei sarebbe un problema. Puoi comunque rimuovere completamente quella sezione e stampare direttamente sullo schermo se questo causa un problema.

È possibile compilare utilizzando g++ -o "NAME" "NAME.cpp"
Dov'è NAMEil nome del file in cui salvarlo ed NAME.cppè il file in cui è salvato questo codice

CTEST.cpp:

#include <iostream>
#include <string>
#include <fstream>
#include <iomanip>
#include <cstdlib>
#include <sstream>
int main(int argc,char *argv[])
{

        if(argc != 2)
        {
                printf("You must provide at least one argument\n"); // Make                                                                                                                      sure only one arg
                exit(0);
   }
std::ifstream file(argv[1]),file2(argv[1]);
std::string line,line2;
std::stringstream ss;
int x=0;

while (file.good()){
    file2.clear();
    file2.seekg (0, file2.beg);
    getline(file, line);
    if(file.good()){
        while ( file2.good() ){
            getline(file2, line2);
            if(file2.good())
            ss << line <<" "<<line2 << "\n";
            x++;
            if(x==10000){
                    std::cout << ss.rdbuf();
                    ss.clear();
                    ss.str(std::string());
            }
    }
    }
}
std::cout << ss.rdbuf();
ss.clear();
ss.str(std::string());
}

Dimostrazione

$ g++ -o "Stream.exe" "CTEST.cpp"
$ seq 10 10 10000 | nl -s, > testfile
$ time ./Stream.exe testfile | wc -l
1000000

real    0m0.243s
user    0m0.210s
sys     0m0.033s

3
join -j 2 file.txt file.txt | cut -c 2-
  • unire in un campo inesistente e rimuovere il primo spazio

Il campo 2 è vuoto e uguale per tutto l'elemento in file.txt quindi joinconcatenerà ogni elemento con tutti gli altri: sta infatti calcolando il prodotto cartesiano.


2

Un'opzione con Python è mappare la memoria del file e trarre vantaggio dal fatto che la libreria di espressioni regolari Python può funzionare direttamente con i file mappati in memoria. Sebbene questo abbia l'aspetto di eseguire loop nidificati sul file, la mappatura della memoria garantisce che il sistema operativo porti in modo ottimale la RAM fisica disponibile

import mmap
import re
with open('test.file', 'rt') as f1, open('test.file') as f2:
    with mmap.mmap(f1.fileno(), 0, flags=mmap.MAP_SHARED, access=mmap.ACCESS_READ) as m1,\
        mmap.mmap(f2.fileno(), 0, flags=mmap.MAP_SHARED, access=mmap.ACCESS_READ) as m2:
        for line1 in re.finditer(b'.*?\n', m1):
            for line2 in re.finditer(b'.*?\n', m2):
                print('{} {}'.format(line1.group().decode().rstrip(),
                    line2.group().decode().rstrip()))
            m2.seek(0)

In alternativa, una soluzione rapida in Python, sebbene l'efficienza della memoria potrebbe essere ancora una preoccupazione

from itertools import product
with open('test.file') as f:
    for a, b  in product(f, repeat=2):
        print('{} {}'.format(a.rstrip(), b.rstrip()))
Row1,10 Row1,10
Row1,10 Row2,20
Row1,10 Row3,30
Row1,10 Row4,40
Row2,20 Row1,10
Row2,20 Row2,20
Row2,20 Row3,30
Row2,20 Row4,40
Row3,30 Row1,10
Row3,30 Row2,20
Row3,30 Row3,30
Row3,30 Row4,40
Row4,40 Row1,10
Row4,40 Row2,20
Row4,40 Row3,30
Row4,40 Row4,40

Questo, per definizione, non manterrà l'intero file in memoria? Non conosco Python ma la tua lingua suggerisce sicuramente che lo farà.
terdon

1
@terdon, se ti riferisci alla soluzione di mappatura della memoria, il sistema operativo manterrà in modo trasparente solo la maggior parte del file in memoria che può permettersi in base alla RAM fisica disponibile. La RAM fisica disponibile non deve superare la dimensione del file (sebbene avere RAM fisica aggiuntiva sarebbe ovviamente una situazione vantaggiosa). Nel peggiore dei casi, ciò potrebbe degradare alla velocità di scorrere i file su disco o peggio. Il vantaggio principale di questo approccio è l'uso trasparente della RAM fisica disponibile in quanto ciò potrebbe variare nel tempo
iruvar

1

In bash, anche ksh dovrebbe funzionare, usando solo gli shell incorporati:

#!/bin/bash
# we require array support
d=( $(< sample.txt) )
# quote arguments and
# build up brace expansion string
d=$(printf -- '%q,' "${d[@]}")
d=$(printf -- '%s' "{${d%,}}' '{${d%,}}")
eval printf -- '%s\\n' "$d"

Si noti che mentre questo contiene l'intero file in memoria in una variabile di shell, necessita solo di un singolo accesso in lettura ad esso.


1
Penso che il punto centrale dell'OP sia di non tenere il file in memoria. Altrimenti, il loro attuale approccio gawk è sia più semplice che molto più veloce. Immagino che questo debba funzionare con file di testo di diverse dimensioni di gigabyte.
terdon

Sì, è esattamente corretto - Ho un paio di file di dati ENORMI con cui devo fare questo e non voglio tenere in memoria
Tom Hayden

Nel caso in cui tu sia così limitato dalla memoria, consiglierei di utilizzare una delle soluzioni di @terdon
Franki,

0

sed soluzione.

line_num=$(wc -l < input.txt)
sed 'r input.txt' input.txt | sed -re "1~$((line_num + 1)){h;d}" -e 'G;s/(.*)\n(.*)/\2 \1/'

Spiegazione:

  • sed 'r file2' file1 - leggi tutto il contenuto del file di file2 per ogni riga del file1.
  • Costruzione 1~isignifica 1a linea, quindi 1 + i linea, 1 + 2 * i, 1 + 3 * i, ecc. Pertanto, 1~$((line_num + 1)){h;d}significa hvecchia linea appuntita al buffer, delimina lo spazio del modello e inizia un nuovo ciclo.
  • 'G;s/(.*)\n(.*)/\2 \1/'- per tutte le righe, tranne quelle selezionate nel passaggio precedente, eseguire next: Get line dal buffer di attesa e aggiungerlo alla riga corrente. Quindi scambia i punti delle linee. Era current_line\nbuffer_line\n, è diventatobuffer_line\ncurrent_line\n

Produzione

Row1,10 Row1,10
Row1,10 Row2,20
Row1,10 Row3,30
Row1,10 Row4,40
Row2,20 Row1,10
Row2,20 Row2,20
Row2,20 Row3,30
Row2,20 Row4,40
Row3,30 Row1,10
Row3,30 Row2,20
Row3,30 Row3,30
Row3,30 Row4,40
Row4,40 Row1,10
Row4,40 Row2,20
Row4,40 Row3,30
Row4,40 Row4,40
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.