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. .
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.
La commande comm fait cela.
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
}
Utilisez comm
- il comparera deux fichiers triés ligne par ligne.
Cette commande renvoie des lignes uniques à deleteNodes, mais pas des lignes dans keepNodes.
comm -1 -3 <(sort keepNodes) <(sort deleteNodes)
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
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
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
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
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.
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