Supprimer les lignes en double par paire?

16

J'ai rencontré ce cas d'utilisation aujourd'hui. Il semble simple au premier coup d' œil, mais bidouiller avec sort, uniq, sedet a awkrévélé qu'il est non négligeable.

Comment supprimer toutes les paires de lignes en double? En d'autres termes, s'il existe un nombre pair de doublons d'une ligne donnée, supprimez-les tous; s'il y a un nombre impair de lignes en double, supprimez toutes sauf une. (Une entrée triée peut être supposée.)

Une solution propre et élégante est préférable.

Exemple d'entrée:

a
a
a
b
b
c
c
c
c
d
d
d
d
d
e

Exemple de sortie:

a
d
e
Caractère générique
la source

Réponses:

6

J'ai trouvé la sedréponse peu de temps après avoir posté cette question; personne d'autre n'a utilisé sedjusqu'à présent, alors voici:

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

Un peu de jeu avec le problème plus général (qu'en est-il de la suppression de lignes par ensembles de trois? Ou quatre ou cinq?) A fourni la solution extensible suivante:

sed -e ':top' -e '$!{/\n/!{N;b top' -e '};};/^\(.*\)\n\1$/d;P;D' temp

Étendu pour supprimer des triplets de lignes:

sed -e ':top' -e '$!{/\n.*\n/!{N;b top' -e '};};/^\(.*\)\n\1\n\1$/d;P;D' temp

Ou pour supprimer des quads de lignes:

sed -e ':top' -e '$!{/\n.*\n.*\n/!{N;b top' -e '};};/^\(.*\)\n\1\n\1\n\1$/d;P;D' temp

sed a un avantage supplémentaire par rapport à la plupart des autres options, qui est sa capacité à vraiment fonctionner dans un flux, sans plus de stockage en mémoire nécessaire que le nombre réel de lignes à vérifier pour les doublons.


Comme cuonglm l'a souligné dans les commentaires , la définition des paramètres régionaux sur C est nécessaire pour éviter les échecs de suppression correcte des lignes contenant des caractères multi-octets. Ainsi, les commandes ci-dessus deviennent:

LC_ALL=C sed '$!N;/^\(.*\)\n\1$/d;P;D' temp
LC_ALL=C sed -e ':top' -e '$!{/\n/!{N;b top' -e '};};/^\(.*\)\n\1$/d;P;D' temp
LC_ALL=C sed -e ':top' -e '$!{/\n.*\n/!{N;b top' -e '};};/^\(.*\)\n\1\n\1$/d;P;D' temp
# Etc.
Caractère générique
la source
2
@Wildcard: Vous souhaiterez peut-être définir les paramètres régionaux sur C, sinon dans les paramètres régionaux à plusieurs octets, un caractère non valide dans ces paramètres régionaux entraînera l'échec de la commande.
cuonglm
4

Ce n'est pas très élégant, mais c'est aussi simple que possible:

uniq -c input | awk '{if ($1 % 2 == 1) { print substr($0, 9) }}'

Le substr () coupe juste la uniqsortie. Cela fonctionnera jusqu'à ce que vous ayez plus de 9 999 999 doublons d'une ligne (dans ce cas, la sortie d'uniq peut déborder de 9 caractères).

Jeff Schaller
la source
J'ai essayé uniq -c input | awk '{if ($1 %2 == 1) { print $2 } }'et cela a semblé fonctionner aussi bien. Une raison pour laquelle la substrversion est meilleure?
Joseph R.
1
@JosephR., S'il y a des espaces dans les lignes, la version de votre commentaire échouera.
Wildcard
C'est vrai. Dans ce cas, ne serait pas une boucle pour imprimer les champs $2pour $NFêtre plus robuste?
Joseph R.
@JosephR .: Pourquoi pensez-vous que votre alternative serait plus robuste? Vous pourriez avoir du mal à le faire fonctionner correctement lorsqu'il y a plusieurs espaces consécutifs; par exemple foo   bar.
G-Man dit `` Réintègre Monica '' le
@JosephR., Non, car cela modifierait / éliminerait la délimitation des espaces. uniq(au moins dans GNU coreutils) semble utiliser de manière fiable exactement 9 caractères avant le texte lui-même; Cependant, je ne trouve cela documenté nulle part, et ce n'est pas dans les spécifications POSIX .
Wildcard
4

Essayez ce awkscript ci-dessous:

#!/usr/bin/awk -f
{
  if ((NR!=1) && (previous!=$0) && (count%2==1)) {
    print previous;
    count=0;
  }
  previous=$0;
  count++;
}
END {
  if (count%2==1) {
    print previous;
  }
}

Il est supposé que le lines.txtfichier est trié.

Le test:

$ chmod +x script.awk
$ ./script.awk lines.txt
a
d
e
Jay jargot
la source
4

Avec pcregreppour un échantillon donné:

pcregrep -Mv '(.)\n\1$' file

ou d'une manière plus générale:

pcregrep -Mv '(^.*)\n\1$' file
jimmij
la source
Ne devrait-il pas y avoir une ancre de «fin de ligne» à la fin? Sinon, vous échouerez sur une ligne qui correspond à la ligne précédente autre que d'avoir des caractères de fin.
Wildcard
@Wildcard ouais, c'est mieux. corrigé, thx.
jimmij
Très cool! (+1)
JJoao
4

Si l'entrée est triée:

perl -0pe  'while(s/^(.*)\n\1\n//m){}'
JJoao
la source
Vous avez un échec d'ancrage ici. Essayez de l'exécuter par exemple pineapple\napple\ncoconutet la sortie est pinecoconut.
Wildcard
@Wildcard: merci. Tu as raison. Voyez si ma mise à jour est logique ...
JJoao
1
Oui. Je me demandais pourquoi vous utilisiez \nau lieu de $donner le /mmodificateur, mais j'ai réalisé que l'utilisation $laisserait une ligne vierge à la place des lignes supprimées. A l'air bien maintenant; J'ai supprimé la version incorrecte car elle n'a fait qu'ajouter du bruit. :)
Wildcard
@wildcard, merci pour la réduction du bruit ☺
JJoao
3

J'aime pythonça, par exemple avec python2.7+

from itertools import groupby
with open('input') as f:
    for k, g in groupby(f):
            if len(list(g)) % 2:
                    print(k),
iruvar
la source
2

Comme j'ai compris la question, j'ai opté pour awk, en utilisant un hachage de chaque enregistrement, dans ce cas, je suppose que RS = \ n, mais il peut être modifié pour prendre en compte tout autre type d'arrangements, il peut être organisé pour considérer un nombre pair de répétitions, au lieu de l'impaire, avec un paramètre ou une petite boîte de dialogue. Chaque ligne est utilisée comme hachage et son nombre augmente, à la fin du fichier, le tableau est analysé et imprime chaque nombre pair de l'enregistrement. J'inclus le nombre afin de vérifier mais, la suppression d'un [x] est suffisante pour résoudre ce problème.

HTH

code de compte à rebours

#!/usr/bin/nawk -f
{a[$0]++}
END{for (x in a) if (a[x]%2!=0) print x,a[x] }

Exemples de données:

a
One Sunny Day
a
a
b
my best friend
my best friend
b
c
c
c
One Sunny Day
c
d
my best friend
my best friend
d
d
d
One Sunny Day
d
e
x
k
j
my best friend
my best friend

Exemple d'exécution:

countlines feed.txt
j 1
k 1
x 1
a 3
One Sunny Day 3
d 5
e 1
Moises Najar
la source
C'est un bon morceau de awkcode, mais malheureusement, awkles tableaux associatifs ne sont pas du tout ordonnés, ni ne préservent l'ordre.
Wildcard
@Wildcard, je suis d'accord avec vous, si vous avez besoin de l'ordre d'entrée, plutôt que d'un ordre de tri, il peut être implémenté via une clé de hachage supplémentaire, l'avantage de cela est que vous n'avez pas à trier l'entrée, car l'ordre de tri peut être fait à la fin avec une sortie plus petite;)
Moises Najar
@Wildcard si vous avez besoin que la commande soit conservée, veuillez le mentionner dans la question. Cette approche a également été ma première pensée et vous ne mentionnez l'ordre que pour dire que nous pouvons supposer que le fichier est trié. Bien sûr, si le fichier est trié, vous pouvez toujours transmettre la sortie de cette solution sort.
terdon
@terdon, bien sûr, vous avez raison; la sortie peut simplement être à nouveau triée. Bon point. Il convient également de noter que le !=0est impliqué par la façon dont awkconvertit les nombres en valeurs vraies / fausses, ce qui rend cela réductible àawk '{a[$0]++}END{for(x in a)if(a[x]%2)print x}'
Wildcard
1

Si l'entrée est triée, qu'en est-il awk:

awk '{ x[$0]++; if (prev != $0 && x[prev] % 2 == 1) { print prev; } prev = $0; } END { if (x[prev] % 2 == 1) print prev; }' sorted
taliezin
la source
1

avec perl:

uniq -c file | perl -lne 'if (m(^\s*(\d+) (.*)$)) {print $2 if $1 % 2 == 1}'
xx4h
la source
1

En utilisant des constructions shell,

uniq -c file | while read a b; do if (( $a & 1 == 1 )); then echo $b; fi done
Guido
la source
1
Cela rompt avec les lignes commençant ou se terminant par des espaces (ou plus, car vous avez oublié de citer $b).
Gilles 'SO- arrête d'être méchant'
1

Puzzle amusant!

En Perl:

#! /usr/bin/env perl

use strict;
use warnings;

my $prev;
while (<>) {
  $prev = $_, next unless defined $prev;  # prime the pump

  if ($prev ne $_) {
    print $prev;
    $prev = $_;                           # first half of a new pair
  }
  else {
    undef $prev;                          # discard and unprime the pump
  }
}

print $prev if defined $prev;             # possible trailing odd line

Verbosely dans Haskell:

main :: IO ()
main = interact removePairs
  where removePairs = unlines . go . lines
        go [] = []
        go [a] = [a]
        go (a:b:rest)
          | a == b = go rest
          | otherwise = a : go (b:rest)

Tersely à Haskell:

import Data.List (group)
main = interact $ unlines . map head . filter (odd . length) . group . lines
Greg Bacon
la source
0

une version: j'utilise des "délimiteurs" pour simplifier la boucle intérieure (il suppose que la première ligne ne l'est pas __unlikely_beginning__et il suppose que le texte ne se termine pas par la ligne __unlikely_ending__:, et j'ajoute cette ligne de délimitation spéciale à la fin des lignes entrées. l'algorithme peut supposer les deux:)

{ cat INPUTFILE_or_just_-  ; echo "__unlikely_ending__" ; } | awk '
  BEGIN {mem="__unlikely_beginning__"; occured=0; }  

    ($0 == mem)            { occured++ ; next } 

    ( occured%2 )           { print mem ;} 
                            { mem=$0; occured=1; }
'

Donc :

  • nous nous souvenons du modèle que nous examinons actuellement, en l'augmentant d'un à chaque fois qu'il se reproduit. [et si cela se reproduit, nous sautons les 2 actions suivantes, qui sont pour le cas où le modèle change]
  • Lorsque le motif CHANGE:
    • sinon un multiple de 2, on imprime une occurrence du motif mémorisé
    • et dans tous les cas lorsque le modèle a changé: le nouveau modèle mémorisé est le modèle actuel, et nous ne l'avons vu qu'une seule fois.
Olivier Dulac
la source