Comment supprimer les lignes dupliquées dans un fichier sans le trier sous Unix?

137

Existe-t-il un moyen de supprimer les lignes en double dans un fichier sous Unix?

Je peux le faire avec les commandes sort -uet uniq, mais je veux utiliser sedou awk. Est-ce possible?

Vijay
la source
12
si vous voulez dire des doublons consécutifs, alors uniqseul suffit.
Michael Krelin - hacker
et sinon, je pense que c'est possible avec awk, mais sera assez gourmand en ressources sur des fichiers plus volumineux.
Michael Krelin - hacker le
Les doublons stackoverflow.com/q/24324350 et stackoverflow.com/q/11532157 ont des réponses intéressantes qui devraient idéalement être migrées ici.
tripleee

Réponses:

290
awk '!seen[$0]++' file.txt

seenest un tableau associatif auquel Awk passera chaque ligne du fichier. Si une ligne n'est pas dans le tableau, la valeur seen[$0]sera false. Le !est un opérateur logique NOT et inversera le faux en vrai. Awk imprimera les lignes où l'expression évalue à vrai. Les ++incréments de seensorte seen[$0] == 1qu'après la première fois une ligne soit trouvée, puis seen[$0] == 2, et ainsi de suite.
Awk évalue tout sauf 0et ""(chaîne vide) à true. Si une ligne en double est placé seenalors !seen[$0]évaluera false et la ligne ne sera pas écrit à la sortie.

Jonas Elfström
la source
5
Pour l'enregistrer dans un fichier, nous pouvons le faireawk '!seen[$0]++' merge_all.txt > output.txt
Akash Kandpal
5
Une mise en garde importante ici: si vous devez faire cela pour plusieurs fichiers, et que vous fixez plus de fichiers à la fin de la commande, ou utilisez un caractère générique… le tableau «vu» se remplira de lignes dupliquées de TOUS les fichiers. Si vous souhaitez plutôt traiter chaque fichier indépendamment, vous devrez faire quelque chose commefor f in *.txt; do gawk -i inplace '!seen[$0]++' "$f"; done
Nick K9
@ NickK9 que la déduplication cumulative sur plusieurs fichiers est géniale en soi. Bon conseil
sfscs le
31

De http://sed.sourceforge.net/sed1line.txt : (Veuillez ne pas me demander comment cela fonctionne ;-))

 # delete duplicate, consecutive lines from a file (emulates "uniq").
 # First line in a set of duplicate lines is kept, rest are deleted.
 sed '$!N; /^\(.*\)\n\1$/!P; D'

 # delete duplicate, nonconsecutive lines from a file. Beware not to
 # overflow the buffer size of the hold space, or else use GNU sed.
 sed -n 'G; s/\n/&&/; /^\([ -~]*\n\).*\n\1/d; s/\n//; h; P'
André Miller
la source
geekery ;-) +1, mais la consommation de ressources est inévitable.
Michael Krelin - hacker
3
«$! N; /^(.*)\n\1$/!P; D 'signifie "Si vous n'êtes pas à la dernière ligne, lisez une autre ligne. Maintenant, regardez ce que vous avez et si ce n'est PAS un truc suivi d'un retour à la ligne, puis de nouveau le même truc, imprimez le truc. Maintenant supprimez le truc (jusqu'à la nouvelle ligne). "
Bêta du
2
'G; s / \ n / && /; / ^ ([- ~] * \ n). * \ n \ 1 / d; s / \ n //; h; P 'signifie, à peu près, "Ajoutez tout l'espace d'attente à cette ligne, puis si vous voyez une ligne dupliquée jetez le tout, sinon copiez tout le désordre dans l'espace d'attente et imprimez la première partie (qui est la ligne que vous venez de read. "
Bêta du
La $!pièce est-elle nécessaire? Ne fait pas sed 'N; /^\(.*\)\n\1$/!P; D'la même chose? Je ne peux pas trouver d'exemple où les deux sont différents sur ma machine (fwiw j'ai essayé une ligne vide à la fin avec les deux versions et elles allaient toutes les deux bien).
eddi
1
Presque 7 ans plus tard et personne n'a répondu à @amichair ... <sniff> me rend triste. ;) Quoi qu'il en soit, [ -~]représente une plage de caractères ASCII de 0x20 (espace) à 0x7E (tilde). Ceux-ci sont considérés comme les caractères ASCII imprimables (la page liée a également 0x7F / delete mais cela ne semble pas correct). Cela rend la solution cassée pour quiconque n'utilise pas ASCII ou pour quiconque utilise, par exemple, des caractères de tabulation. Le plus portable [^\n]comprend beaucoup plus de caractères ... tous sauf un, en fait.
B Layer
14

Perl one-liner similaire à la solution awk de @ jonas:

perl -ne 'print if ! $x{$_}++' file

Cette variante supprime les espaces de fin avant de comparer:

perl -lne 's/\s*$//; print if ! $x{$_}++' file

Cette variante modifie le fichier sur place:

perl -i -ne 'print if ! $x{$_}++' file

Cette variante modifie le fichier sur place et effectue une sauvegarde file.bak

perl -i.bak -ne 'print if ! $x{$_}++' file
Chris Koknat
la source
6

Le one-liner qu'Andre Miller a posté ci-dessus fonctionne à l'exception des versions récentes de sed lorsque le fichier d'entrée se termine par une ligne vide et sans caractères. Sur mon Mac, mon processeur tourne simplement.

Boucle infinie si la dernière ligne est vide et n'a pas de caractères :

sed '$!N; /^\(.*\)\n\1$/!P; D'

Ne se bloque pas, mais tu perds la dernière ligne

sed '$d;N; /^\(.*\)\n\1$/!P; D'

L'explication se trouve à la toute fin de la FAQ sed :

Le mainteneur de GNU sed a estimé que malgré les problèmes de portabilité que
cela causerait, changer la commande N pour imprimer (plutôt que
supprimer) l'espace des motifs était plus cohérent avec ses intuitions
sur la façon dont une commande pour "ajouter la ligne suivante" devrait se comporter.
Un autre fait en faveur du changement était que "{N; command;}"
supprimera la dernière ligne si le fichier a un nombre impair de lignes, mais
imprimera la dernière ligne si le fichier a un nombre pair de lignes.

Pour convertir des scripts qui utilisaient l'ancien comportement de N (suppression de
l'espace de motif en atteignant l'EOF) en scripts compatibles avec
toutes les versions de sed, changez un seul "N;" à "$ d; N;" .

Bradley Kreider
la source
5

Une autre façon d'utiliser Vim (compatible Vi) :

Supprimer les lignes consécutives en double d'un fichier:

vim -esu NONE +'g/\v^(.*)\n\1$/d' +wq

Supprimer les lignes dupliquées, non consécutives et non vides d'un fichier:

vim -esu NONE +'g/\v^(.+)$\_.{-}^\1$/d' +wq

Bohr
la source
4

La première solution vient également de http://sed.sourceforge.net/sed1line.txt

$ echo -e '1\n2\n2\n3\n3\n3\n4\n4\n4\n4\n5' |sed -nr '$!N;/^(.*)\n\1$/!P;D'
1
2
3
4
5

l'idée principale est:

print ONLY once of each duplicate consecutive lines at its LAST appearance and use D command to implement LOOP.

Explique:

  1. $!N;: si la ligne courante n'est PAS la dernière ligne, utilisez la Ncommande pour lire la ligne suivante pattern space.
  2. /^(.*)\n\1$/!P: si le contenu de current pattern spaceest deux duplicate stringséparés par \n, ce qui signifie que la ligne suivante est la sameavec la ligne courante, nous ne pouvons PAS l'imprimer selon notre idée de base; sinon, ce qui signifie que la ligne actuelle est la DERNIÈRE apparence de toutes ses lignes consécutives en double, nous pouvons maintenant utiliser la Pcommande pour imprimer les caractères dans l' pattern spaceutilitaire actuel \n( \négalement imprimé).
  3. D: nous utilisons la Dcommande pour supprimer les caractères dans l' pattern spaceutilitaire actuel \n( \négalement supprimé), puis le contenu de pattern spaceest la ligne suivante.
  4. et la Dcommande forcera sedà sauter à sa FIRSTcommande $!N, mais ne lira PAS la ligne suivante du fichier ou du flux d'entrée standard.

La deuxième solution est facile à comprendre (de moi-même):

$ echo -e '1\n2\n2\n3\n3\n3\n4\n4\n4\n4\n5' |sed -nr 'p;:loop;$!N;s/^(.*)\n\1$/\1/;tloop;D'
1
2
3
4
5

l'idée principale est:

print ONLY once of each duplicate consecutive lines at its FIRST appearance and use : command & t command to implement LOOP.

Explique:

  1. lire une nouvelle ligne à partir du flux d'entrée ou du fichier et l'imprimer une fois.
  2. utilisez la :loopcommande set un labelnamed loop.
  3. utilisez Npour lire la ligne suivante dans le pattern space.
  4. utilisez s/^(.*)\n\1$/\1/pour supprimer la ligne actuelle si la ligne suivante est la même que la ligne actuelle, nous utilisons la scommande pour faire l' deleteaction.
  5. si la scommande est exécutée avec succès, alors utilisez la tloopforce de commande sedpour sauter à la labelnommée loop, qui fera la même boucle aux lignes suivantes car il n'y a pas de lignes consécutives en double de la ligne qui est latest printed; sinon, utilisez la Dcommande sur deletela ligne qui est la même que le latest-printed line, et forcez sedà sauter à la première commande, qui est la pcommande, le contenu de current pattern spaceest la nouvelle ligne suivante.
Weike
la source
même commande sous Windows avec busybox:busybox echo -e "1\n2\n2\n3\n3\n3\n4\n4\n4\n4\n5" | busybox sed -nr "$!N;/^(.*)\n\1$/!P;D"
scavenger
-1

Ceci peut être réalisé en utilisant awk
sous la ligne affichera des valeurs uniques

awk file_name | uniq

Vous pouvez générer ces valeurs uniques dans un nouveau fichier

awk file_name | uniq > uniq_file_name

le nouveau fichier uniq_file_name ne contiendra que des valeurs uniques, pas de doublons

Aashutosh
la source
-4
cat filename | sort | uniq -c | awk -F" " '$1<2 {print $2}'

Supprime les lignes dupliquées à l'aide de awk.

Sadhun
la source
1
Cela perturbera l'ordre des lignes.
Vijay
1
Qu'est-ce qu'un fichier texte de 20 Go? Trop lent.
Alexander Lubyagin
Comme toujours, le catest inutile. Quoi qu'il en soit, le fait uniqdéjà par lui-même et ne nécessite pas que l'entrée soit exactement un mot par ligne.
tripleee