Un moyen rapide de trouver des lignes dans un fichier qui ne sont pas dans un autre?

241

J'ai deux gros fichiers (ensembles de noms de fichiers). Environ 30 000 lignes dans chaque fichier. J'essaie de trouver un moyen rapide de trouver des lignes dans le fichier1 qui ne sont pas présentes dans le fichier2.

Par exemple, s'il s'agit de file1:

line1
line2
line3

Et voici file2:

line1
line4
line5

Alors mon résultat / sortie devrait être:

line2
line3

Cela marche:

grep -v -f file2 file1

Mais c'est très, très lent lorsqu'il est utilisé sur mes gros fichiers.

Je soupçonne qu'il existe un bon moyen de le faire en utilisant diff (), mais la sortie ne devrait être que les lignes, rien d'autre, et je n'arrive pas à trouver un commutateur pour cela.

Quelqu'un peut-il m'aider à trouver un moyen rapide de le faire, en utilisant bash et les binaires linux de base?

EDIT: Pour faire suite à ma propre question, c'est la meilleure façon que j'ai trouvée jusqu'à présent en utilisant diff ():

diff file2 file1 | grep '^>' | sed 's/^>\ //'

Il doit certainement exister un meilleur moyen?

Niels2000
la source
1
vous pouvez essayer ceci si c'est plus rapide:awk 'NR==FNR{a[$0];next}!($0 in a)' file2 file1 > out.txt
Kent
sans exigence rapide: stackoverflow.com/questions/4366533/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
4
Merci d'avoir parlé de grep -v -f file2 file1
Rahul Prasad
Un moyen simple avec un jeu d'outils réduit:, cat file1 file2 file2 | sort | uniq --uniquevoir ma réponse ci-dessous.
Ondra Žižka

Réponses:

233

Vous pouvez y parvenir en contrôlant le formatage des anciennes / nouvelles / lignes inchangées dans la diffsortie GNU :

diff --new-line-format="" --unchanged-line-format=""  file1 file2

Les fichiers d'entrée doivent être triés pour que cela fonctionne. Avec bash(et zsh), vous pouvez trier sur place avec la substitution de processus <( ):

diff --new-line-format="" --unchanged-line-format="" <(sort file1) <(sort file2)

Dans les lignes ci-dessus , les lignes nouvelles et inchangées sont supprimées, donc seules les lignes modifiées (c'est-à-dire les lignes supprimées dans votre cas) sont sorties. Vous pouvez également utiliser quelques diffoptions d' autres solutions n'offrent pas, comme -ipour ignorer la casse, ou diverses options (espaces blancs -E, -b, -vetc) pour la correspondance moins stricte.


Explication

Les options --new-line-format, --old-line-formatet --unchanged-line-formatvous permettent de contrôler la façon de diffformater les différences, comme pour les printfspécificateurs de format. Ces options mettent en forme respectivement les lignes nouvelles (ajoutées), anciennes (supprimées) et inchangées . Mettre un à vide "" empêche la sortie de ce type de ligne.

Si vous connaissez le format diff unifié , vous pouvez le recréer en partie avec:

diff --old-line-format="-%L" --unchanged-line-format=" %L" \
     --new-line-format="+%L" file1 file2

Le %Lspécificateur est la ligne en question, et nous préfixons chacun avec "+" "-" ou "", comme diff -u (notez qu'il ne produit que des différences, il manque les lignes --- +++et @@en haut de chaque changement groupé). Vous pouvez également l'utiliser pour faire d'autres choses utiles comme numéroter chaque ligne avec %dn.


La diffméthode (avec d'autres suggestions commet join) ne produit que la sortie attendue avec une entrée triée , bien que vous puissiez utiliser <(sort ...)pour trier sur place. Voici un awkscript simple (nawk) (inspiré des scripts liés dans la réponse de Konsolebox) qui accepte les fichiers d'entrée commandés arbitrairement et génère les lignes manquantes dans l'ordre dans lequel elles se produisent dans file1.

# output lines in file1 that are not in file2
BEGIN { FS="" }                         # preserve whitespace
(NR==FNR) { ll1[FNR]=$0; nl1=FNR; }     # file1, index by lineno
(NR!=FNR) { ss2[$0]++; }                # file2, index by string
END {
    for (ll=1; ll<=nl1; ll++) if (!(ll1[ll] in ss2)) print ll1[ll]
}

Cela stocke le contenu entier de file1 ligne par ligne dans un tableau indexé de numéro de ligne ll1[], et le contenu entier de file2 ligne par ligne dans un tableau associatif indexé de contenu de ligne ss2[]. Une fois les deux fichiers lus, parcourez ll1et utilisez l' inopérateur pour déterminer si la ligne dans file1 est présente dans file2. (Cela aura une sortie différente de la diffméthode s'il y a des doublons.)

Dans le cas où les fichiers sont suffisamment volumineux pour que leur stockage entraîne un problème de mémoire, vous pouvez échanger la CPU contre de la mémoire en stockant uniquement file1 et en supprimant les correspondances en cours de lecture.

BEGIN { FS="" }
(NR==FNR) {  # file1, index by lineno and string
  ll1[FNR]=$0; ss1[$0]=FNR; nl1=FNR;
}
(NR!=FNR) {  # file2
  if ($0 in ss1) { delete ll1[ss1[$0]]; delete ss1[$0]; }
}
END {
  for (ll=1; ll<=nl1; ll++) if (ll in ll1) print ll1[ll]
}

Ce qui précède stocke l'intégralité du contenu de file1 dans deux tableaux, l'un indexé par numéro de ligne ll1[], l'autre indexé par contenu de ligne ss1[]. Lorsque le fichier2 est lu, chaque ligne correspondante est supprimée de ll1[]et ss1[]. À la fin, les lignes restantes du fichier1 sont sorties, préservant l'ordre d'origine.

Dans ce cas, avec le problème indiqué, vous pouvez également diviser et conquérir à l' aide de GNU split(le filtrage est une extension GNU), des exécutions répétées avec des morceaux de fichier1 et la lecture complète de fichier2 à chaque fois:

split -l 20000 --filter='gawk -f linesnotin.awk - file2' < file1

Notez l'utilisation et le placement du -sens stdinsur la gawkligne de commande. Ceci est fourni par à splitpartir de file1 en morceaux de 20000 lignes par appel.

Pour les utilisateurs sur les systèmes non-GNU, il est presque certainement un paquet coreutils GNU , vous pouvez obtenir, y compris sur OSX dans le cadre des d' Apple Xcode outils qui fournit GNU diff, awk, mais seulement un POSIX / BSD splitplutôt que d' une version GNU.

Mr Spuratic
la source
1
Cela fait exactement ce dont j'ai besoin, dans une infime fraction du temps pris par l'énorme grep. Merci!
Niels2000
1
Trouvé cette page de
manuel
certains d'entre nous ne sont pas sur gnu [OS X bsd ici ...] :)
rogerdpack
1
Je suppose que vous voulez dire pour diff: en général, les fichiers d'entrée seront différents, 1 est retourné par diffdans ce cas. Considérez cela comme un bonus ;-) Si vous testez dans un script shell 0 et 1 sont des codes de sortie attendus, 2 indique un problème.
mr.spuratic
1
@ mr.spuratic ah ouais, maintenant je le trouve dans le man diff. Merci!
Archeosudoerus
246

La commande comm (abréviation de "common") peut être utilecomm - compare two sorted files line by line

#find lines only in file1
comm -23 file1 file2 

#find lines only in file2
comm -13 file1 file2 

#find lines common to both files
comm -12 file1 file2 

Le manfichier est en fait assez lisible pour cela.

JnBrymn
la source
6
Fonctionne parfaitement sur OSX.
pisaruk
41
La nécessité d'une entrée triée devrait peut-être être soulignée.
tripleee
21
comma également une option pour vérifier que l'entrée est triée --check-order(ce qu'il semble faire de toute façon, mais cette option entraînera une erreur au lieu de continuer). Mais pour trier les fichiers, faites simplement: com -23 <(sort file1) <(sort file2)et ainsi de suite
Michael
Je comparais un fichier généré sous Windows avec un fichier généré sous Linux et il semblait que cela commne fonctionnait pas du tout. Il m'a fallu un certain temps pour comprendre qu'il s'agit des fins de ligne: même les lignes qui semblent identiques sont considérées comme différentes si elles ont des fins de ligne différentes. La commande dos2unixpeut être utilisée pour convertir les fins de ligne CRLF en LF uniquement.
ZeroOne
23

Comme l'a suggéré konsolebox, la solution posters grep

grep -v -f file2 file1

fonctionne très bien (rapidement) si vous ajoutez simplement l' -Foption, pour traiter les modèles comme des chaînes fixes au lieu d'expressions régulières. J'ai vérifié cela sur une paire de ~ 1000 listes de fichiers de ligne que j'ai dû comparer. Avec -Fcela a pris 0,031 s (réel), tandis que sans cela a pris 2,278 s (réel), lors de la redirection de la sortie grep vers wc -l.

Ces tests ont également inclus le -xcommutateur, qui est une partie nécessaire de la solution afin d'assurer une précision totale dans les cas où le fichier2 contient des lignes qui correspondent à une partie, mais pas à la totalité, d'une ou plusieurs lignes du fichier1.

Ainsi, une solution qui ne nécessite pas de tri des entrées, est rapide, flexible (respect de la casse, etc.) est:

grep -F -x -v -f file2 file1

Cela ne fonctionne pas avec toutes les versions de grep, par exemple, il échoue dans macOS, où une ligne dans le fichier 1 sera affichée comme non présente dans le fichier 2, même si elle l'est, si elle correspond à une autre ligne qui en est une sous-chaîne . Alternativement, vous pouvez installer GNU grep sur macOS afin d'utiliser cette solution.

pbz
la source
Ouais, ça marche mais même avec -Fça ça ne va pas bien.
Molomby
ce n'est pas si rapide, j'ai attendu 5 minutes pour 2 fichiers de ~ 500k lignes avant d'abandonner
cahen
en fait, cette méthode est toujours plus lente que la méthode comm, car celle-ci peut gérer les fichiers non triés et donc traînés par le tri, comm profite du tri
workplaylifecycle
@workplaylifecycle Vous devez ajouter le temps de tri qui peut être le goulot d'étranglement pour les très grands file2.
1er
Cependant, grep avec l' -xoption utilise apparemment plus de mémoire. Avec un file2contenu de 180 millions de mots de 6 à 10 octets, mon processus est arrivé Killedsur une machine de 32 Go de RAM ...
rwst
11

quelle est la vitesse de tri et de diff?

sort file1 -u > file1.sorted
sort file2 -u > file2.sorted
diff file1.sorted file2.sorted
Puggan Se
la source
1
Merci de m'avoir rappelé la nécessité de trier les fichiers avant de faire le diff. sort + diff est BEAUCOUP plus rapide.
Niels2000
4
un liner ;-) diff <(
fichier de tri1
11

Si vous manquez d '"outils sophistiqués", par exemple dans une distribution Linux minimale, il existe une solution avec juste cat, sortet uniq:

cat includes.txt excludes.txt excludes.txt | sort | uniq --unique

Tester:

seq 1 1 7 | sort --random-sort > includes.txt
seq 3 1 9 | sort --random-sort > excludes.txt
cat includes.txt excludes.txt excludes.txt | sort | uniq --unique

# Output:
1
2    

Ceci est également relativement rapide par rapport à grep.

Ondra Žižka
la source
1
Remarque - certaines implémentations ne reconnaissent pas l' --uniqueoption. Vous devriez pouvoir utiliser l' option POSIX standardisée pour cela:| uniq -u
AndrewF
1
Dans l'exemple, d'où vient le "2"?
Niels2000
1
@ Niels2000, seq 1 1 7crée des nombres de 1, avec incrément 1, jusqu'à 7, c'est-à-dire 1 2 3 4 5 6 7. Et là, c'est votre 2!
Eirik Lygre
5
$ join -v 1 -t '' file1 file2
line2
line3

Le -ts'assure qu'il compare toute la ligne, si vous aviez un espace dans certaines lignes.

Steven Penny
la source
Par exemple comm, les joindeux lignes d'entrée doivent être triées sur le champ sur lequel vous effectuez l'opération de jointure.
tripleee
4

Vous pouvez utiliser Python:

python -c '
lines_to_remove = set()
with open("file2", "r") as f:
    for line in f.readlines():
        lines_to_remove.add(line.strip())

with open("f1", "r") as f:
    for line in f.readlines():
        if line.strip() not in lines_to_remove:
            print(line.strip())
'
Bonjour au revoir
la source
4

Utilisation combinede moreutilspaquet, un utilitaire qui prend en charge des ensembles not, and, or, xoropérations

combine file1 not file2

c'est-à-dire me donner des lignes qui sont dans le fichier1 mais pas dans le fichier2

OU donnez-moi des lignes dans le fichier1 moins des lignes dans le fichier2

Remarque: combine trie et trouve des lignes uniques dans les deux fichiers avant d'effectuer une opération mais diffne le fait pas. Vous pouvez donc trouver des différences entre la sortie de diffet combine.

Donc, en fait, vous dites

Recherchez des lignes distinctes dans les fichiers file1 et file2, puis donnez-moi des lignes dans le fichier1 moins les lignes dans le fichier2

D'après mon expérience, c'est beaucoup plus rapide que les autres options

GypsyCosmonaut
la source
2

L'utilisation de fgrep ou l'ajout de l'option -F à grep pourrait aider. Mais pour des calculs plus rapides, vous pouvez utiliser Awk.

Vous pouvez essayer l'une de ces méthodes Awk:

http://www.linuxquestions.org/questions/programming-9/grep-for-huge-files-826030/#post4066219

konsolebox
la source
2
+1 C'est la seule réponse qui ne nécessite pas de tri des entrées. Bien que le PO soit apparemment satisfait de cette exigence, c'est une contrainte inacceptable dans de nombreux scénarios du monde réel.
tripleee
1

La façon dont je le fais habituellement consiste à utiliser le --suppress-common-linesdrapeau, mais notez que cela ne fonctionne que si vous le faites au format côte à côte.

diff -y --suppress-common-lines file1.txt file2.txt

BAustin
la source
0

J'ai trouvé que pour moi, l'utilisation d'une instruction de boucle normale si et pour fonctionnait parfaitement.

for i in $(cat file2);do if [ $(grep -i $i file1) ];then echo "$i found" >>Matching_lines.txt;else echo "$i missing" >>missing_lines.txt ;fi;done
Tman
la source
2
Voir DontReadLinesWithFor . En outre, ce code se comportera très mal si l'un de vos greprésultats se développe sur plusieurs mots ou si l'une de vos file2entrées peut être traitée par le shell comme un glob.
Charles Duffy