Outil en ligne de commande pour «cat» l'expansion par paire de toutes les lignes d'un fichier

13

Supposons que j'ai un fichier (appelez-le sample.txt) qui ressemble à ceci:

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

Je veux pouvoir travailler sur un flux de ce fichier qui est essentiellement la combinaison par paire des quatre lignes (nous devrions donc nous retrouver avec 16 au total). Par exemple, je recherche une commande de streaming (c'est-à-dire efficace) où la sortie est:

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

Mon cas d'utilisation est que je veux diffuser cette sortie dans une autre commande (comme awk) pour calculer une métrique sur cette combinaison par paire.

J'ai un moyen de le faire dans awk mais ma préoccupation est que mon utilisation du bloc END {} signifie que je stocke essentiellement le fichier entier en mémoire avant de le sortir. Exemple de code:

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-t-il un moyen de streaming efficace pour le faire sans avoir à stocker essentiellement le fichier en mémoire, puis à le sortir dans le bloc END?

Tom Hayden
la source
1
Vous devrez toujours lire un fichier jusqu'à la fin avant de pouvoir commencer à produire une sortie pour la deuxième ligne de l'autre fichier. L'autre fichier que vous pouvez diffuser.
reinierpost

Réponses:

12

Voici comment le faire dans awk afin qu'il n'ait pas à stocker le fichier entier dans un tableau. Il s'agit essentiellement du même algorithme que celui de terdon.

Si vous le souhaitez, vous pouvez même lui donner plusieurs noms de fichiers sur la ligne de commande et il traitera chaque fichier indépendamment, concaténant les résultats ensemble.

#!/usr/bin/awk -f

#Cartesian product of records

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

Sur mon système, cela s'exécute dans environ 2/3 du temps de la solution Perl de Terdon.

PM 2Ring
la source
1
Merci! Toutes les solutions à ce problème étaient fantastiques mais j'ai fini par choisir celui-ci en raison de 1) la simplicité et 2) de rester dans awk. Merci!
Tom Hayden
1
Content que tu l'aimes, Tom. J'ai tendance à programmer principalement en Python ces jours-ci, mais j'aime toujours awk pour le traitement de texte ligne par ligne en raison de ses boucles intégrées sur les lignes et les fichiers. Et c'est souvent plus rapide que Python.
PM 2Ring
7

Je ne suis pas sûr que ce soit mieux que de le faire en mémoire, mais avec un sedqui rlit son fichier pour chaque ligne de son fichier et un autre de l'autre côté d'un tuyau alternant l' Hancien espace avec des lignes d'entrée ...

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'

PRODUCTION

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

Je l'ai fait d'une autre manière. Il en stocke certains en mémoire - il stocke une chaîne comme:

"$1" -

... pour chaque ligne du fichier.

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

C'est très rapide. C'est catle fichier autant de fois qu'il y a de lignes dans le fichier vers a |pipe. De l'autre côté du canal, cette entrée est fusionnée avec le fichier lui-même autant de fois qu'il y a de lignes dans le fichier.

Le casecontenu est juste pour la portabilité - yashet les zshdeux ajoutent un élément à la division, tandis mkshque les poshdeux en perdent un. ksh, dash, busyboxEt bashtoutes divisées en exactement autant de champs comme il y a des zéros comme imprimé par printf. Tel qu'écrit, ce qui précède donne les mêmes résultats pour chacun des obus mentionnés ci-dessus sur ma machine.

Si le fichier est très long, il peut y avoir des $ARGMAXproblèmes avec trop d'arguments, auquel cas vous devrez également introduire xargsou similaire.

Étant donné la même entrée que j'ai utilisée avant la sortie est identique. Mais si je devais aller plus loin ...

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

Cela génère un fichier presque identique à ce que j'utilisais auparavant (sans 'Row') - mais à 1000 lignes. Vous pouvez voir par vous-même à quelle vitesse il est:

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

À 1000 lignes, il y a une légère variation de performances entre les shells - bashc'est invariablement la plus lente - mais parce que le seul travail qu'ils font de toute façon est de générer la chaîne d'argument (1000 copies de filename -) l'effet est minime. La différence de performances entre zsh- comme ci-dessus - et bashest de 100e de seconde ici.

Voici une autre version qui devrait fonctionner pour un fichier de n'importe quelle longueur:

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

Il crée un lien logiciel vers son premier argument /tmpavec un nom semi-aléatoire afin qu'il ne se bloque pas sur des noms de fichiers étranges. C'est important parce que catles arguments sont alimentés via un tuyau via xargs. catLa sortie de 'est enregistrée dans <&3tandis sed pqu'imprime chaque ligne dans le premier argument autant de fois qu'il y a de lignes dans ce fichier - et son script lui est également envoyé via un tube. pasteFusionne à nouveau son entrée, mais cette fois, il ne prend à nouveau que deux arguments -pour son entrée standard et le nom du lien /dev/fd/3.

Ce dernier - le /dev/fd/[num]lien - devrait fonctionner sur n'importe quel système Linux et bien d'autres encore, mais s'il ne crée pas de canal nommé avec mkfifoet l'utilise à la place, cela devrait également fonctionner.

La dernière chose qu'il fait est rmle lien logiciel qu'il crée avant de quitter.

Cette version est en fait encore plus rapide sur mon système. Je suppose que c'est parce que bien qu'il exécute plus d'applications, il commence à leur transmettre immédiatement leurs arguments - alors qu'avant, il les empilait tous en premier.

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
mikeserv
la source
La fonction paires est-elle supposée être dans un fichier, sinon comment le déclareriez-vous?
@Jidder - comment pourrais-je déclarer quoi? Vous pouvez simplement le copier + coller dans un terminal, non?
mikeserv
1
Déclarez la fonction. Alors vous pouvez! Je pensais que vous auriez pu échapper aux nouvelles lignes, je me méfie de simplement coller du code, merci cependant :) C'est également une réponse extrêmement rapide et agréable!
@Jidder - Je les écris généralement dans un shell en direct en utilisant simplement ctrl+v; ctrl+jpour obtenir des nouvelles lignes comme je le fais.
mikeserv
@Jidder - merci beaucoup. Et il est sage de se méfier - bon pour vous. Ils fonctionneront également dans un fichier - vous pouvez le copier dans et . ./file; fn_namedans ce cas.
mikeserv
5

Eh bien, vous pouvez toujours le faire dans votre shell:

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

C'est beaucoup plus lent que votre awksolution (sur ma machine, cela a pris ~ 11 secondes pour 1000 lignes, contre ~ 0,3 seconde awk), mais au moins, il ne contient jamais plus de quelques lignes en mémoire.

La boucle ci-dessus fonctionne pour les données très simples que vous avez dans votre exemple. Il s'étouffera avec les contre-obliques et mangera les espaces de fuite et de tête. Une version plus robuste de la même chose est:

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 autre choix est d'utiliser à la perlplace:

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

Le script ci-dessus lira chaque ligne du fichier d'entrée ( -ln), l'enregistrera sous $l, rouvrira sample.txtet imprimera chaque ligne avec $l. Le résultat est toutes les combinaisons par paire alors que seulement 2 lignes sont jamais stockées en mémoire. Sur mon système, cela ne prenait que 0.6quelques secondes sur 1 000 lignes.

terdon
la source
Ouah merci! Je me demande pourquoi la solution Perl est tellement plus rapide que la déclaration bash while
Tom Hayden
@TomHayden essentiellement parce que perl, comme awk, est beaucoup plus rapide que bash.
terdon
1
J'ai dû downvote pour votre boucle while. 4 mauvaises pratiques différentes là-dedans. Tu sais mieux.
Stéphane Chazelas
1
@ StéphaneChazelas bien, sur la base de votre réponse ici , je ne pouvais penser à aucun cas où cela echopourrait être un problème. Ce que j'avais écrit (j'ajoute printfmaintenant) devrait fonctionner avec tous, n'est-ce pas? Quant à la whileboucle, pourquoi? Qu'est-ce qui ne va pas while read f; do ..; done < file? Vous ne proposez certainement pas une forboucle! Quelle est l'autre alternative?
terdon
2
@cuonglm, que l'on ne fait allusion qu'à une raison possible pour laquelle on devrait l'éviter. Hors des aspects conceptuels , de fiabilité , de lisibilité , de performance et de sécurité , cela ne couvre que la fiabilité .
Stéphane Chazelas
4

Avec zsh:

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

$^asur un tableau active l'expansion de type accolade (comme dans {elt1,elt2}) pour le tableau.

Stéphane Chazelas
la source
4

Vous pouvez compiler ce code pour des résultats assez rapides.
Il se termine en environ 0,19 à 0,27 seconde sur un fichier de 1 000 lignes.

Il lit actuellement les 10000lignes en mémoire (pour accélérer l'impression à l'écran) qui, si vous aviez des 1000caractères par ligne, utiliserait moins que la 10mbmémoire, ce qui ne serait pas un problème selon moi . Vous pouvez cependant supprimer complètement cette section et l'imprimer directement à l'écran si cela pose un problème.

Vous pouvez compiler en utilisant g++ -o "NAME" "NAME.cpp"
NAMEest le nom du fichier pour l'enregistrer et NAME.cppest le fichier dans lequel ce code est enregistré

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

Manifestation

$ 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

la source
3
join -j 2 file.txt file.txt | cut -c 2-
  • joindre par un champ inexistant et supprimer le premier espace

Le champ 2 est vide et égal pour tous les éléments du fichier.txt donc joinva concaténer chaque élément avec tous les autres: il s'agit en fait de calculer le produit cartésien.

JJoao
la source
2

Une option avec Python consiste à mapper en mémoire le fichier et à tirer parti du fait que la bibliothèque d'expressions régulières Python peut fonctionner directement avec des fichiers mappés en mémoire. Bien que cela ait l'apparence d'exécuter des boucles imbriquées sur le fichier, le mappage de la mémoire garantit que le système d'exploitation met la RAM physique disponible en jeu de manière optimale.

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)

Alternativement, une solution rapide en Python, bien que l'efficacité de la mémoire puisse toujours être un problème

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
iruvar
la source
N'est-ce pas, par définition, garder tout le fichier en mémoire? Je ne connais pas Python mais votre langage suggère certainement que ce sera le cas.
terdon
1
@terdon, si vous faites référence à la solution de mappage de la mémoire, le système d'exploitation ne conservera de manière transparente que la quantité de fichier en mémoire qu'il peut se permettre en fonction de la RAM physique disponible. La RAM physique disponible ne doit pas dépasser la taille du fichier (bien qu'avoir de la RAM physique supplémentaire soit évidemment une situation avantageuse). Dans le pire des cas, cela pourrait dégrader à la vitesse de bouclage du fichier sur le disque ou pire. Le principal avantage de cette approche est l'utilisation transparente de la RAM physique disponible, car cela pourrait fluctuer dans le temps
iruvar
1

En bash, ksh devrait également fonctionner, en utilisant uniquement les fonctions intégrées du 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"

Notez que bien que cela contienne le fichier entier en mémoire dans une variable shell, il n'a besoin que d'un seul accès en lecture.

Franki
la source
1
Je pense que l'intérêt de l'OP est de ne pas conserver le fichier en mémoire. Sinon, leur approche gawk actuelle est à la fois plus simple et beaucoup plus rapide. Je suppose que cela doit fonctionner avec des fichiers texte de plusieurs gigaoctets.
terdon
Ouais, c'est tout à fait correct - J'ai quelques fichiers de données ÉNORMES avec lesquels je dois faire cela et je ne veux pas les garder en mémoire
Tom Hayden
Dans le cas où vous êtes limité par la mémoire, je recommanderais d'utiliser l'une des solutions de @terdon
Franki
0

sed Solution.

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

Explication:

  • sed 'r file2' file1 - lire tout le contenu du fichier de fichier2 pour chaque ligne de fichier1.
  • Construction 1~isignifie 1-ème ligne, puis 1 + ligne i, 1 + 2 * i, 1 + 3 * i, etc. Par conséquent, des 1~$((line_num + 1)){h;d}moyens hancienne ligne pointue dans la mémoire tampon, l' despace de modèle elete et commencer à nouveau cycle.
  • 'G;s/(.*)\n(.*)/\2 \1/'- pour toutes les lignes, sauf celles choisies à l'étape précédente, faites ensuite: Get ligne depuis le tampon de maintien et ajoutez-la à la ligne actuelle. Échangez ensuite les emplacements des lignes. Était current_line\nbuffer_line\n, est devenubuffer_line\ncurrent_line\n

Production

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
MiniMax
la source