web-dev-qa-db-fra.com

bash, Linux: définir la différence entre deux fichiers texte

J'ai deux fichiers A-nodes_to_delete et B-nodes_to_keep. Chaque fichier comporte plusieurs lignes avec des identifiants numériques.

Je veux avoir la liste des identifiants numériques qui sont dans nodes_to_delete mais PAS dans nodes_to_keep, par exemple. alt text .

Le faire dans une base de données PostgreSQL est excessivement lent. Une bonne façon de le faire en bash en utilisant les outils CLI Linux?

MISE À JOUR: Cela semblerait être un travail Pythonic, mais les fichiers sont vraiment très gros. J'ai résolu des problèmes similaires en utilisant uniq, sort et certaines techniques de théorie des ensembles. C'était environ deux ou trois ordres de grandeur plus rapides que les équivalents de la base de données.

66
Adam Matan

La commande comm fait cela.

97
msw

Quelqu'un m'a montré comment faire exactement cela en sh il y a quelques mois, puis je ne l'ai pas trouvé pendant un certain temps ... et en regardant, je suis tombé sur votre question. C'est ici :

set_union () {
   sort $1 $2 | uniq
}

set_difference () {
   sort $1 $2 $2 | uniq -u
}

set_symmetric_difference() {
   sort $1 $2 | uniq -u
}
38
slinkp

Utilisez comm - il comparera deux fichiers triés ligne par ligne.

La réponse courte à votre question

Cette commande renvoie des lignes uniques à deleteNodes, mais pas des lignes dans keepNodes.

comm -1 -3 <(sort keepNodes) <(sort deleteNodes)

Exemple de configuration

Créons les fichiers nommés keepNodes et deleteNodes, et utilisons-les comme entrées non triées pour la commande comm.

$ cat > keepNodes <(echo bob; echo amber;)
$ cat > deleteNodes <(echo bob; echo ann;)

Par défaut, l'exécution de comm sans arguments imprime 3 colonnes avec cette disposition:

lines_unique_to_FILE1
    lines_unique_to_FILE2
        lines_which_appear_in_both

En utilisant nos fichiers d'exemple ci-dessus, exécutez comm sans arguments. Notez les trois colonnes.

$ comm <(sort keepNodes) <(sort deleteNodes)
amber
    ann
        bob

Suppression de la sortie de colonne

Supprimez les colonnes 1, 2 ou 3 avec -N; notez que lorsqu'une colonne est masquée, l'espace se rétrécit.

$ comm -1 <(sort keepNodes) <(sort deleteNodes)
ann
    bob
$ comm -2 <(sort keepNodes) <(sort deleteNodes)
amber
    bob
$ comm -3 <(sort keepNodes) <(sort deleteNodes)
amber
    ann
$ comm -1 -3 <(sort keepNodes) <(sort deleteNodes)
ann
$ comm -2 -3 <(sort keepNodes) <(sort deleteNodes)
amber
$ comm -1 -2 <(sort keepNodes) <(sort deleteNodes)
bob

Le tri est important!

Si vous exécutez comm sans d'abord trier le fichier, il échoue correctement avec un message indiquant quel fichier n'est pas trié.

comm: file 1 is not in sorted order

6
activedecay

comm a été spécialement conçu pour ce type de cas d'utilisation, mais il nécessite une entrée triée.

awk est sans doute un meilleur outil pour cela car il est assez simple de trouver la différence d'ensemble, ne nécessite pas sort et offre une flexibilité supplémentaire.

awk 'NR == FNR { a[$0]; next } !($0 in a)' nodes_to_keep nodes_to_delete

Par exemple, vous souhaitez peut-être trouver uniquement la différence dans les lignes qui représentent des nombres non négatifs:

awk -v r='^[0-9]+$' 'NR == FNR && $0 ~ r {
    a[$0]
    next
} $0 ~ r && !($0 in a)' nodes_to_keep nodes_to_delete
4
John B

Peut-être avez-vous besoin d'une meilleure façon de le faire dans les postgres, je peux presque parier que vous ne trouverez pas un moyen plus rapide de le faire en utilisant des fichiers plats. Vous devriez être capable de faire une simple jointure interne et en supposant que les deux colonnes id sont indexées, ce qui devrait être très rapide.

1
Dark Castle

Donc, c'est légèrement différent des autres réponses. Je ne peux pas dire qu'un compilateur C++ est exactement un "outil CLI Linux", mais exécuter g++ -O3 -march=native -o set_diff main.cpp (avec le code ci-dessous dans main.cpp peut faire l'affaire):

#include<algorithm>
#include<iostream>
#include<iterator>
#include<fstream>
#include<string>
#include<unordered_set>

using namespace std;

int main(int argc, char** argv) {
    ifstream keep_file(argv[1]), del_file(argv[2]);
    unordered_multiset<string> init_lines{istream_iterator<string>(keep_file), istream_iterator<string>()};
    string line;
    while (getline(del_file, line)) {
        init_lines.erase(line);
    }
    copy(init_lines.begin(),init_lines.end(), ostream_iterator<string>(cout, "\n"));
}

Pour l'utiliser, exécutez simplement set_diff B A (pasA B, puisque B est nodes_to_keep) et la différence résultante sera imprimée sur stdout.

Notez que j'ai renoncé à quelques bonnes pratiques C++ pour garder le code plus simple.

De nombreuses optimisations de vitesse supplémentaires pourraient être apportées (au prix de plus de mémoire). mmap serait également particulièrement utile pour les grands ensembles de données, mais cela impliquerait beaucoup plus le code.

Puisque vous avez mentionné que les ensembles de données sont volumineux, je pensais que la lecture de nodes_to_delete une ligne à la fois peut être une bonne idée pour réduire la consommation de mémoire. L'approche adoptée dans le code ci-dessus n'est pas particulièrement efficace s'il y a beaucoup de dupes dans votre nodes_to_delete. De plus, l'ordre n'est pas préservé.


Quelque chose de plus facile à copier et à coller dans bash (c'est-à-dire en sautant la création de main.cpp):

g++ -O3 -march=native -xc++ -o set_diff - <<EOF
#include<algorithm>
#include<iostream>
#include<iterator>
#include<fstream>
#include<string>
#include<unordered_set>

using namespace std;

int main(int argc, char** argv) {
        ifstream keep_file(argv[1]), del_file(argv[2]);
        unordered_multiset<string> init_lines{istream_iterator<string>(keep_file), istream_iterator<string>()};
        string line;
        while (getline(del_file, line)) {
                init_lines.erase(line);
        }
        copy(init_lines.begin(),init_lines.end(), ostream_iterator<string>(cout, "\n"));
}
EOF
0
YenForYang