web-dev-qa-db-fra.com

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

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 fichier1:

line1
line2
line3

Et c'est file2:

line1
line4
line5

Ensuite, mon résultat/résultat devrait être:

line2
line3

Cela marche:

grep -v -f file2 file1

Mais il 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 le résultat devrait être juste 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 les binaires bash et linux de base?

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

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

Il doit certainement exister un meilleur moyen?

190
Niels2000

Vous pouvez y parvenir en contrôlant le formatage des anciennes/nouvelles/lignes non modifiées dans GNU diff résultat:

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 substitution de processus <( ):

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

Dans ce qui précède , les nouvelles lignes et non modifiées sont supprimées. Seules sont modifiées ( les lignes supprimées dans votre cas) sont sorties. Vous pouvez également utiliser quelques diff options que d’autres solutions ne proposent pas, telles que -i pour ignorer la casse ou diverses options d’espace (-E, -b, -v etc) pour une correspondance moins stricte.


Explication

Les options --new-line-format, --old-line-format et --unchanged-line-format vous permettent de contrôler la façon dont diff formate les différences, de la même manière que les spécificateurs de format printf. Ces options formatent new (ajouté), old (supprimé) et inchangé lignes respectivement. Si vous définissez un "", cela empêche la sortie de ce type de ligne.

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

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

Le spécificateur %L 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 le ---+++ et @@ lignes en haut de chaque modification groupée). Vous pouvez aussi l'utiliser pour faire d'autres choses utiles comme numéroter chaque ligne avec %dn.


La méthode diff (avec d’autres suggestions comm et join) ne produit la sortie attendue qu’avec l’entrée triée , bien que vous puissiez utiliser <(sort ...) pour trier sur place. Voici un simple script awk (nawk) (inspiré des scripts liés à la réponse de Konsolebox) qui accepte les fichiers d'entrée ordonnés de manière arbitraire, et affiche les lignes manquantes du ordre ils 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]
}

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

Si les fichiers sont suffisamment volumineux pour que leur stockage soit à l'origine d'un problème de mémoire, vous pouvez échanger de la CPU en ne stockant que fichier1 et en supprimant les correspondances au fur et à mesure de la lecture du fichier2.

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 le contenu entier du fichier1 dans deux tableaux, l’un indexé par le numéro de ligne ll1[], l’autre indexé par le contenu de la ligne ss1[]. Ensuite, lorsque le fichier 2 est lu, chaque ligne correspondante est supprimée de ll1[] et ss1[]. A la fin, les lignes restantes de fichier1 sont sorties, en préservant l'ordre d'origine.

Dans ce cas, avec le problème indiqué, vous pouvez également diviser pour régner en utilisant GNU split (le filtrage est un GNU extension), exécutions répétées avec des morceaux de fichier1 et lecture complète de fichier2 à chaque fois:

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

Notez l'utilisation et le placement de - signifiant stdin sur la ligne de commande gawk. Ceci est fourni par split à partir de fichier1 en morceaux de 20000 lignes par invocation.

Pour les utilisateurs de systèmes non GNU, il existe presque certainement un package GNU coreutils que vous pouvez obtenir, y compris sous OSX dans le cadre des outils Apple Xcode , qui fournit GNU diff, awk, mais uniquement une version POSIX/BSD split plutôt qu'une version GNU.

190
mr.spuratic

La commande comm (abréviation de "common") peut être utile comm - 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 fichier man est en fait assez lisible pour cela.

193
JnBrymn

Comme suggéré par konsolebox, la solution grep de posters

grep -v -f file2 file1

fonctionne vraiment très bien (rapidement) si vous ajoutez simplement l’option -F, 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 listes de fichiers d'environ 1000 lignes que je devais comparer. Avec -F, il a fallu 0,031 s (réel), alors que sans cela, il a fallu 2,28 s (réel) pour rediriger la sortie de grep vers wc -l.

Ces tests incluaient également le commutateur -x, élément indispensable de la solution pour garantir une précision totale dans les cas où fichier2 contient des lignes correspondant à une partie, mais pas à toutes, d'une ou plusieurs lignes de fichier1.

Donc, une solution qui ne nécessite pas le tri des entrées, est rapide, flexible (sensibilité à la casse, etc.) et fonctionne également (je pense) sur n’importe quel système POSIX:

grep -F -x -v -f file2 file1
21
pbz

quelle est la vitesse de sorte et de diff?

sort file1 -u > file1.sorted
sort file2 -u > file2.sorted
diff file1.sorted file2.sorted
10
Puggan Se

Si vous manquez d’instruments sophistiqués, par exemple, dans certaines distributions Linux minimales, il existe une solution avec juste cat, sort et 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    

C'est aussi relativement rapide, comparé à grep.

7
Ondra Žižka
$ join -v 1 -t '' file1 file2
line2
line3

Le -t s'assure qu'il compare toute la ligne, si vous avez un espace dans certaines des lignes.

5
Steven Penny

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())
'
2
HelloGoodbye

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

2
konsolebox

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

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

1
BAustin