Ferramenta de linha de comando para "cat" expansão pareada de todas as linhas em um arquivo


13

Suponha que eu tenha um arquivo (chame-o de exemplo.txt) que se parece com isso:

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

Quero poder trabalhar em um fluxo desse arquivo que é essencialmente a combinação em pares de todas as quatro linhas (portanto, devemos terminar com 16 no total). Por exemplo, estou procurando um comando de streaming (ou seja, eficiente) em que a saída é:

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

Meu caso de uso é que desejo transmitir essa saída para outro comando (como o awk) para calcular algumas métricas sobre essa combinação em pares.

Eu tenho uma maneira de fazer isso no awk, mas minha preocupação é que meu uso do bloco END {} signifique que eu estou basicamente armazenando o arquivo inteiro na memória antes de sair. Código de exemplo:

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 uma maneira eficiente de streaming para fazer isso sem ter que armazenar essencialmente o arquivo na memória e, em seguida, imprimir no bloco END?


1
Você sempre precisará ler um arquivo até o final antes de poder começar a produzir saída para a segunda linha do outro arquivo. O outro arquivo que você pode transmitir.
Reinierpost

Respostas:


12

Veja como fazê-lo no awk para que ele não precise armazenar o arquivo inteiro em uma matriz. Este é basicamente o mesmo algoritmo que o de Terdon.

Se desejar, você pode até fornecer vários nomes de arquivos na linha de comando e ele processará cada arquivo independentemente, concatenando os resultados juntos.

#!/usr/bin/awk -f

#Cartesian product of records

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

No meu sistema, isso é executado em cerca de 2/3 do tempo da solução perl do terdon.


1
Obrigado! Todas as soluções para esse problema foram fantásticas, mas eu acabei indo com essa, devido a 1) simplicidade e 2) permanecer no awk. Obrigado!
Tom Hayden

1
Que bom que você gostou, Tom. Atualmente, costumo programar principalmente em Python, mas ainda gosto de awk para processamento de texto linha por linha devido a seus loops internos sobre linhas e arquivos. E geralmente é mais rápido que o Python.
PM 2Ring

7

Não tenho certeza se isso é melhor do que fazê-lo na memória, mas com um sedque rpreenche seu infile para cada linha em seu infile e outro no outro lado de um tubo alternando o Hespaço antigo com as linhas 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'

RESULTADO

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

Eu fiz isso de outra maneira. Ele armazena um pouco na memória - armazena uma string como:

"$1" -

... para cada linha no arquivo.

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

É muito rápido. É cato arquivo quantas vezes houver linhas no arquivo para a |pipe. No outro lado do canal, essa entrada é mesclada com o próprio arquivo quantas vezes houver linhas no arquivo.

O casematerial é apenas para portabilidade - yashe zshtanto um elemento de adicionar à divisão, enquanto mkshe poshtanto um perder. ksh, dash, busybox, E bashtudo dividido para exatamente quantos campos existem zeros como impressa pelo printf. Conforme escrito, o acima apresenta os mesmos resultados para cada uma das conchas acima mencionadas na minha máquina.

Se o arquivo for muito longo, poderá haver $ARGMAXproblemas com muitos argumentos; nesse caso, você precisará introduzir xargsou similar também.

Dada a mesma entrada que usei antes da saída é idêntica. Mas se eu fosse maior ...

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

Isso gera um arquivo quase idêntico ao que eu usei antes (sans 'Row') - mas com 1000 linhas. Você pode ver por si mesmo o quão rápido é:

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

Em 1000 linhas, há uma ligeira variação no desempenho entre os shells - bashé invariavelmente o mais lento - mas como o único trabalho que eles fazem é gerar a string arg (1000 cópias filename -), o efeito é mínimo. A diferença de desempenho entre zsh- como acima - e bashé centésimo de segundo aqui.

Aqui está outra versão que deve funcionar para um arquivo de qualquer tamanho:

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

Ele cria um link suave para o seu primeiro argumento /tmpcom um nome semi-aleatório, para que não fique preso a nomes de arquivos estranhos. Isso é importante porque catos argumentos são alimentados através de um cano via xargs. catA saída do arquivo é salva <&3enquanto sed pas linhas do primeiro argumento são copiadas quantas vezes houver linhas nesse arquivo - e seu script também é alimentado por meio de um pipe. Novamente pastemescla sua entrada, mas desta vez são necessários apenas dois argumentos -novamente para sua entrada padrão e o nome do link /dev/fd/3.

Esse último - o /dev/fd/[num]link - deve funcionar em qualquer sistema Linux e muito mais, mas se não criar um pipe nomeado mkfifoe usá-lo, também funcionará.

A última coisa que ele faz é rmo link direto criado antes de sair.

Esta versão é realmente mais rápida ainda no meu sistema. Eu acho que é porque, apesar de executar mais aplicativos, ele começa a entregar seus argumentos imediatamente - enquanto antes os empilhava primeiro.

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

A função dos pares deve estar em um arquivo; caso contrário, como você a declararia?

@Jidder - como eu declararia o que? Você pode simplesmente copiar e colar em um terminal, não?
mikeserv

1
Declare a função. Então você pode! Eu pensei que você teria escapado de novas linhas, eu desconfio de apenas colar código, obrigado :) :) Também isso é extremamente rápido, resposta agradável!

@Jidder - Eu costumo escrever esses em um shell ao vivo apenas usando ctrl+v; ctrl+jpara obter novas linhas como eu faço.
mikeserv

@ Jidder - muito obrigado. E é prudente ser cauteloso - bom para você. Eles também funcionarão em um arquivo - você pode copiá-lo . ./file; fn_namenesse caso.
mikeserv

5

Bem, você sempre pode fazer isso no seu shell:

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

É muito mais lento que a sua awksolução (na minha máquina, demorou ~ 11 segundos para 1000 linhas, contra ~ 0,3 segundos awk), mas pelo menos nunca mantém mais do que algumas linhas na memória.

O loop acima funciona para os dados muito simples que você possui no seu exemplo. Ele engasga com barras invertidas e come espaços à direita e à esquerda. Uma versão mais robusta da mesma coisa é:

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

Outra opção é usar perl:

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

O script acima lerá cada linha do arquivo de entrada ( -ln), salve-o como $l, abra sample.txtnovamente e imprima cada linha junto com $l. O resultado são todas as combinações aos pares, enquanto apenas 2 linhas são armazenadas na memória. No meu sistema, isso levou apenas 0.6alguns segundos em 1000 linhas.


Uau, obrigada! Eu me pergunto por que a solução perl é muito mais rápido do que a festa while
Tom Hayden

@ TomHayden basicamente porque o perl, como o awk, é muito mais rápido que o bash.
terdon

1
Teve que votar para o seu loop enquanto. 4 práticas ruins diferentes lá. Você sabe melhor.
Stéphane Chazelas

1
@ StéphaneChazelas bem, com base na sua resposta aqui , não consegui pensar em nenhum caso em que isso echopossa ser um problema. O que eu escrevi (adicionei printfagora) deve funcionar com todos eles, certo? Quanto ao whileloop, por quê? O que há de errado while read f; do ..; done < file? Certamente você não está sugerindo um forloop! Qual é a outra alternativa?
terdon

2
@cuonglm, isso apenas sugere uma possível razão pela qual alguém deveria evitá-la. Fora dos aspectos conceituais , de confiabilidade , legibilidade , desempenho e segurança , isso abrange apenas a confiabilidade .
Stéphane Chazelas

4

Com zsh:

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

$^aem uma matriz ativa a expansão do tipo cinta (como em {elt1,elt2}) para a matriz.


4

Você pode compilar esse código para obter resultados bastante rápidos.
É concluído em cerca de 0,19 - 0,27 segundos em um arquivo de 1000 linhas.

Atualmente lê 10000 linhas na memória (para acelerar a impressão na tela) que, se você tivesse 1000caracteres por linha, usaria menos que a 10mbmemória, o que eu acho que não seria um problema. Você pode remover completamente essa seção e apenas imprimir diretamente na tela, se isso causar um problema.

Você pode compilar usando g++ -o "NAME" "NAME.cpp"
Onde NAMEé o nome do arquivo para salvá-lo e NAME.cppé o arquivo em que esse código é salvo

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

Demonstração

$ 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-
  • junte-se por um campo não existente e remova o primeiro espaço

O campo 2 está vazio e igual para todo o elemento em file.txt, portanto joinconcatenará cada elemento com todos os outros: na verdade, está calculando o produto cartesiano.


2

Uma opção do Python é mapear o arquivo na memória e tirar proveito do fato de que a biblioteca de expressões regulares do Python pode trabalhar diretamente com arquivos mapeados na memória. Embora isso pareça executar ciclos aninhados sobre o arquivo, o mapeamento de memória garante que o sistema operacional coloque a RAM física disponível de maneira ideal em jogo

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)

Como alternativa, uma solução rápida em Python, embora a eficiência da memória ainda possa ser uma preocupação

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

Isso, por definição, não manterá o arquivo inteiro na memória? Eu não conheço Python, mas sua linguagem certamente sugere que sim.
terdon

1
@terdon, se você estiver se referindo à solução de mapeamento de memória, o sistema operacional manterá transparente apenas o máximo de arquivo na memória possível, com base na RAM física disponível. A RAM física disponível não precisa exceder o tamanho do arquivo (apesar de ter RAM física extra seria obviamente uma situação vantajosa). Na pior das hipóteses, isso pode degradar a velocidade de repetição de arquivos no disco ou pior. A principal vantagem dessa abordagem é o uso transparente de RAM física disponível como isso é algo que poderia flutuar ao longo do tempo
Iruvar

1

No bash, o ksh também deve funcionar, usando apenas os recursos internos do 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"

Observe que, enquanto isso mantém o arquivo inteiro na memória em uma variável de shell, ele precisa apenas de um único acesso de leitura.


1
Eu acho que o ponto principal do OP é não manter o arquivo na memória. Caso contrário, sua atual abordagem de gawk é mais simples e muito mais rápida. Acho que isso precisa funcionar com arquivos de texto com vários gigabytes de tamanho.
terdon

Sim, isso é exatamente correto - eu tenho alguns arquivos de dados ENORME com os quais preciso fazer isso e não quero guardar na memória
Tom Hayden

Caso você esteja com restrição de memória, eu recomendaria o uso de uma das soluções da @terdon
Franki

0

sed solução.

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

Explicação:

  • sed 'r file2' file1 - leia todo o conteúdo do arquivo2 para cada linha do arquivo1.
  • Construção 1~isignifica 1ª linha, depois 1 + i linha, 1 + 2 * i, 1 + 3 * i, etc. Portanto, 1~$((line_num + 1)){h;d}significa hantiga linha pontiaguda para o buffer, delimina o espaço do padrão e inicia um novo ciclo.
  • 'G;s/(.*)\n(.*)/\2 \1/'- para todas as linhas, exceto as selecionadas na etapa anterior, faça o seguinte: Get line a partir do buffer de retenção e anexe-o à linha atual. Então troque os lugares das linhas. Foi current_line\nbuffer_line\n, tornou-sebuffer_line\ncurrent_line\n

Resultado

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.