Herramienta de línea de comando para "cat" expansión por pares de todas las filas en un archivo


13

Supongamos que tengo un archivo (llámelo sample.txt) que se ve así:

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

Quiero poder trabajar en una secuencia de este archivo que es esencialmente la combinación por pares de las cuatro filas (por lo que deberíamos terminar con 16 en total). Por ejemplo, estoy buscando un comando de transmisión (es decir, eficiente) donde la salida es:

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

Mi caso de uso es que quiero transmitir esta salida a otro comando (como awk) para calcular alguna métrica sobre esta combinación por pares.

Tengo una manera de hacer esto en awk, pero mi preocupación es que mi uso del bloque END {} significa que básicamente estoy almacenando todo el archivo en la memoria antes de la salida. Código de ejemplo:

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

¿Existe una forma eficiente de transmisión para hacer esto sin tener que almacenar esencialmente el archivo en la memoria y luego emitirlo en el bloque END?


1
Siempre necesitará leer un archivo hasta el final antes de poder comenzar a producir resultados para la segunda línea del otro archivo. El otro archivo que puede transmitir.
reinierpost

Respuestas:


12

Aquí se explica cómo hacerlo en awk para que no tenga que almacenar todo el archivo en una matriz. Este es básicamente el mismo algoritmo que el de terdon.

Si lo desea, incluso puede darle varios nombres de archivo en la línea de comando y procesará cada archivo de forma independiente, concatenando los resultados juntos.

#!/usr/bin/awk -f

#Cartesian product of records

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

En mi sistema, esto se ejecuta en aproximadamente 2/3 del tiempo de la solución perl de terdon.


1
¡Gracias! Todas las soluciones a este problema fueron fantásticas, pero terminé con esta debido a 1) la simplicidad y 2) permanecer en awk. ¡Gracias!
Tom Hayden

1
Me alegro de que te guste, Tom. Actualmente tiendo a programar principalmente en Python, pero todavía me gusta awk para el procesamiento de texto línea por línea debido a sus bucles integrados sobre líneas y archivos. Y a menudo es más rápido que Python.
PM 2Ring

7

No estoy seguro de que esto sea mejor que hacerlo en la memoria, pero con un sedque rborra su archivo para cada línea en su archivo y otro en el otro lado de una tubería que alterna el Hespacio antiguo con líneas de entrada ...

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'

SALIDA

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

Hice esto de otra manera. Almacena algunos en la memoria, almacena una cadena como:

"$1" -

... para cada línea en el archivo.

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"
}

Es muy rápido. Es catel archivo tantas veces como haya líneas en el archivo a |pipe. En el otro lado de la tubería, esa entrada se fusiona con el archivo en sí tantas veces como haya líneas en el archivo.

El casematerial es sólo para la portabilidad - yashy zshtanto un elemento añadir a la división, mientras mkshy poshtanto uno perder. ksh, dash, busybox, Y bashtoda división a cabo exactamente como muchos campos, ya que hay ceros como impreso por printf. Como está escrito, lo anterior genera los mismos resultados para cada uno de los shells mencionados anteriormente en mi máquina.

Si el archivo es muy largo, puede haber $ARGMAXproblemas con demasiados argumentos, en cuyo caso necesitaría introducir xargso similar también.

Dada la misma entrada que usé antes de que la salida sea idéntica. Pero, si fuera más grande ...

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

Eso genera un archivo casi idéntico al que usé antes (sin 'Fila') , pero con 1000 líneas. Puedes ver por ti mismo lo rápido que es:

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 líneas hay una ligera variación en el rendimiento entre los shells, bashes invariablemente el más lento, pero debido a que el único trabajo que hacen de todos modos es generar la cadena arg (1000 copias de filename -) el efecto es mínimo. La diferencia en el rendimiento entre zsh- como arriba - y bashes la centésima de segundo aquí.

Aquí hay otra versión que debería funcionar para un archivo de cualquier longitud:

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 enlace suave a su primer argumento /tmpcon un nombre semi-aleatorio para que no se obsesione con nombres de archivos extraños. Eso es importante porque catlos args se alimentan a través de una tubería xargs. catLa salida de se guarda en <&3mientras sed pborra cada línea en el primer argumento tantas veces como haya líneas en ese archivo, y su script también se alimenta a través de una tubería. Nuevamente pastefusiona su entrada, pero esta vez solo toma dos argumentos -nuevamente para su entrada estándar y el nombre del enlace /dev/fd/3.

Ese último, el /dev/fd/[num]enlace, debería funcionar en cualquier sistema Linux y muchos más, pero si no crea una tubería con nombre mkfifoy usarlo, también debería funcionar.

Lo último que hace es rmel enlace suave que crea antes de salir.

Esta versión es realmente más rápida aún en mi sistema. Supongo que es porque aunque ejecuta más aplicaciones, comienza a entregarles sus argumentos de inmediato, mientras que antes los apilaba primero.

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

¿Se supone que la función de pares está en un archivo, si no, cómo lo declararías?

@Jidder: ¿cómo declararía qué? Puedes copiarlo y pegarlo en un terminal, ¿no?
mikeserv

1
Declara la función. ¡Así que puedes! Pensé que tendrías que escapar de las nuevas líneas, desconfío de pegar el código, aunque gracias :) También eso es extremadamente rápido, ¡buena respuesta!

@Jidder: generalmente los escribo en un shell en vivo solo ctrl+v; ctrl+jpara obtener nuevas líneas como lo hago.
mikeserv

@Jidder: muchas gracias. Y es sabio ser cauteloso, bien por ti. Funcionarán también en un archivo; puede copiarlo . ./file; fn_nameen ese caso y en ese caso.
mikeserv

5

Bueno, siempre puedes hacerlo en tu caparazón:

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

Es mucho más lento que su awksolución (en mi máquina, tardó ~ 11 segundos para 1000 líneas, versus ~ 0.3 segundos awk), pero al menos nunca tiene más de un par de líneas en la memoria.

El bucle anterior funciona para los datos muy simples que tiene en su ejemplo. Se ahogará con las barras invertidas y comerá espacios finales y principales. Una versión más robusta de lo mismo es:

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

Otra opción es usar perlen su lugar:

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

La secuencia de comandos anterior leerá cada línea del archivo de entrada ( -ln), la guardará como $l, se abrirá sample.txtnuevamente e imprimirá cada línea junto con $l. El resultado son todas las combinaciones por pares, mientras que solo 2 líneas se almacenan en la memoria. En mi sistema, eso tomó solo unos 0.6segundos en 1000 líneas.


¡Wow gracias! Me pregunto por qué la solución perl es mucho más rápida que la declaración bash while
Tom Hayden,

@TomHayden básicamente porque perl, como awk, es mucho más rápido que bash.
terdon

1
Tuve que votar a favor de tu ciclo while. 4 malas prácticas diferentes allí. Tu sabes mejor.
Stéphane Chazelas

1
@ StéphaneChazelas bien, según su respuesta aquí , no podría pensar en ningún caso en el que echopudiera haber un problema. Lo que había escrito (agregué printfahora) debería funcionar con todos ellos, ¿verdad? En cuanto al whilebucle, ¿por qué? ¿Qué tiene de malo while read f; do ..; done < file? ¡Seguramente no estás sugiriendo un forbucle! ¿Cuál es la otra alternativa?
terdon

2
@cuonglm, ese solo insinúa una posible razón por la que uno debería evitarlo. Fuera de los aspectos conceptuales , confiabilidad , legibilidad , desempeño y seguridad , eso solo cubre confiabilidad .
Stéphane Chazelas

4

Con zsh:

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

$^aen una matriz activa la expansión similar a una llave (como en {elt1,elt2}) para la matriz.


4

Puede compilar este código de para obtener resultados bastante rápidos.
Se completa en alrededor de 0.19 - 0.27 segundos en un archivo de 1000 líneas.

Actualmente lee 10000líneas en la memoria (para acelerar la impresión en la pantalla) que si tuviera 1000caracteres por línea usaría menos de la 10mbmemoria, lo que no creo que sea un problema. Sin embargo, puede eliminar esa sección por completo e imprimir directamente en la pantalla si causa un problema.

Puede compilar usando g++ -o "NAME" "NAME.cpp"
Where NAMEes el nombre del archivo para guardarlo y NAME.cppes el archivo en el que se guarda este código

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());
}

Demostración

$ 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-
  • unirse por un campo no existente y eliminar el primer espacio

El campo 2 está vacío e igual para todos los elementos en file.txt, por joinlo que concatenará cada elemento con todos los demás: de hecho, está calculando el producto cartesiano.


2

Una opción con Python es mapear el archivo en memoria y aprovechar el hecho de que la biblioteca de expresiones regulares de Python puede trabajar directamente con archivos mapeados en memoria. Aunque esto tiene la apariencia de ejecutar bucles anidados sobre el archivo, la asignación de memoria asegura que el sistema operativo ponga en juego la RAM física disponible de manera óptima

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)

Alternativamente, una solución rápida en Python, aunque la eficiencia de la memoria podría ser una preocupación.

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

¿No, por definición, mantendrá todo el archivo en la memoria? No sé Python, pero su lenguaje ciertamente sugiere que lo hará.
terdon

1
@terdon, si se refiere a la solución de mapeo de memoria, el sistema operativo mantendrá de forma transparente solo la mayor cantidad de archivos en la memoria que pueda permitirse, en función de la RAM física disponible. La RAM física disponible no tiene que exceder el tamaño del archivo (aunque tener RAM física adicional obviamente sería una situación ventajosa). En el peor de los casos, esto podría degradarse a la velocidad de recorrer el archivo en el disco o peor. La ventaja clave de este enfoque es el uso transparente de la RAM física disponible, ya que esto es algo que podría fluctuar con el tiempo
irávar el

1

En bash, ksh debería funcionar también, utilizando solo los componentes integrados de shell:

#!/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"

Tenga en cuenta que si bien esto contiene todo el archivo en la memoria en una variable de shell, solo necesita un único acceso de lectura.


1
Creo que todo el punto para el OP es no mantener el archivo en la memoria. De lo contrario, su enfoque actual de gawk es más simple y mucho más rápido. Supongo que esto debe funcionar con archivos de texto que tienen varios gigabytes de tamaño.
terdon

Sí, eso es exactamente correcto. Tengo un par de archivos de datos ENORMES con los que necesito hacer esto y no quiero guardarlos en la memoria
Tom Hayden

En caso de que esté tan limitado por la memoria, recomendaría usar una de las soluciones de @terdon
Franki el

0

sed solución.

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/'

Explicación:

  • sed 'r file2' file1 - lea todo el contenido del archivo2 para cada línea del archivo1.
  • La construcción 1~isignifica la 1ª línea, luego la línea 1 + i, 1 + 2 * i, 1 + 3 * i, etc. Por lo tanto, 1~$((line_num + 1)){h;d}significa la hantigua línea puntiaguda al búfer, delegir el espacio del patrón y comenzar un nuevo ciclo.
  • 'G;s/(.*)\n(.*)/\2 \1/'- para todas las líneas, excepto las seleccionadas en el paso anterior, haga lo siguiente: Get line from hold buffer y añádalo a la línea actual. Luego intercambie lugares de líneas. Era current_line\nbuffer_line\n, se convirtióbuffer_line\ncurrent_line\n

Salida

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
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.