Eliminare le linee duplicate in coppia?


16

Ho riscontrato questo caso d'uso oggi. Sembra semplice a prima vista, ma trafficando con sort, uniq, sede awkha rivelato che si tratta di non banale.

Come posso eliminare tutte le coppie di righe duplicate? In altre parole, se esiste un numero pari di duplicati di una determinata riga, eliminali tutti; se esiste un numero dispari di righe duplicate, eliminare tutte tranne una. (Si può presumere un input ordinato.)

È preferibile una soluzione pulita ed elegante.

Esempio di input:

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

Esempio di output:

a
d
e

Risposte:


6

Ho elaborato la sedrisposta non molto tempo dopo aver pubblicato questa domanda; nessun altro ha usato sedfinora così eccolo qui:

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

Un po 'a giocare con il problema più generale (che dire di eliminare le righe in gruppi di tre? O quattro, o cinque?) Ha fornito la seguente soluzione estensibile:

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

Esteso per rimuovere le triple di linee:

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

O per rimuovere i quadratini di linee:

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

sed ha un ulteriore vantaggio rispetto alla maggior parte delle altre opzioni, che è la sua capacità di operare veramente in un flusso, senza bisogno di più memoria di memoria rispetto al numero effettivo di linee da controllare per i duplicati.


Come ha sottolineato cuonglm nei commenti , è necessario impostare le impostazioni locali su C per evitare errori nel rimuovere correttamente le righe contenenti caratteri multibyte. Quindi i comandi sopra diventano:

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.

2
@Wildcard: è possibile che si desideri impostare le impostazioni internazionali su C, altrimenti nelle impostazioni internazionali multibyte, il carattere non valido in tale impostazione locale non riuscirà .
cuonglm

4

Non è molto elegante, ma è semplice come posso inventare:

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

Il substr () taglia semplicemente l' uniqoutput. Funzionerà fino a quando non avrai più di 9.999.999 duplicati di una riga (nel qual caso l'output di uniq potrebbe estendersi su 9 caratteri).


Ci ho provato uniq -c input | awk '{if ($1 %2 == 1) { print $2 } }'e sembrava funzionare ugualmente bene. Qualche motivo per cui la substrversione è migliore?
Joseph R.,

1
@JosephR., Se è presente uno spazio bianco nelle righe, la versione nel tuo commento fallirà.
Wildcard il

Questo è vero. In tal caso, non sarebbe un ciclo per stampare i campi $2per $NFessere più robusto?
Joseph R.,

@JosephR .: Perché credi che la tua alternativa sarebbe più robusta? Potresti avere difficoltà a farlo funzionare correttamente quando ci sono più spazi consecutivi; ad es foo   bar.
G-Man dice "Reinstate Monica" il

@JosephR., No, perché cambierebbe / eliminerebbe la delimitazione degli spazi bianchi. uniq(almeno nei coreutils GNU) sembra usare esattamente 9 caratteri prima del testo stesso; Non riesco a trovarlo documentato da nessuna parte, tuttavia, e non è nelle specifiche POSIX .
Wildcard il

4

Prova questo awkscript qui sotto:

#!/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;
  }
}

Si presume che il lines.txtfile sia ordinato.

Il test:

$ chmod +x script.awk
$ ./script.awk lines.txt
a
d
e

4

Con pcregrepper un dato campione:

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

o in modo più generale:

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

Non dovrebbe esserci un ancoraggio "end of line" alla fine? Altrimenti fallirai su una linea che corrisponde alla linea prima di essa oltre ad avere caratteri finali.
Wildcard il

@Wildcard sì, va meglio. corretto, grazie.
jimmij,

Molto bello! (+1)
JJoao,

4

Se l'input è ordinato:

perl -0pe  'while(s/^(.*)\n\1\n//m){}'

Hai un fallimento di ancoraggio qui. Prova ad eseguirlo ad es. pineapple\napple\ncoconutE l'output è pinecoconut.
Wildcard

@Wildcard: grazie. Hai ragione. Vedi se il mio aggiornamento ha senso ...
JJoao,

1
Sì. Mi chiedevo perché stavi usando \ninvece di $dare il /mmodificatore, ma poi ho capito che l'uso $avrebbe lasciato una riga vuota al posto delle righe eliminate. Sembra buono ora; Ho rimosso la versione errata poiché ha appena aggiunto rumore. :)
Wildcard

@wildcard, grazie per la riduzione del rumore ☺
JJoao,

3

Mi piace pythonper questo, ad esempio con python2.7+

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

2

Come ho capito la domanda che ho optato per awk, usando un hash di ogni record, in questo caso sto assumendo che RS = \ n, ma può essere cambiato per considerare qualsiasi altro tipo di arrangiamento, può essere organizzato per considerare un numero pari di ripetizioni, anziché dispari, con un parametro o una piccola finestra di dialogo. Ogni riga viene utilizzata come hash e il suo conteggio viene aumentato, alla fine del file l'array viene scansionato e stampa ogni conteggio pari del record. Includo il conteggio per verificare ma, rimuovere una [x] è sufficiente per risolvere quel problema.

HTH

codice di countline

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

Dati di esempio:

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

Esempio di esecuzione:

countlines feed.txt
j 1
k 1
x 1
a 3
One Sunny Day 3
d 5
e 1

È un bel pezzo di awkcodice, ma sfortunatamente gli awkarray associativi non sono affatto ordinati, né preservano l'ordine.
Carattere jolly

@Wildcard, sono d'accordo con te, se stai richiedendo l'ordine di input, piuttosto che un ordinamento, può essere implementato tramite una chiave hash aggiuntiva, il vantaggio di questo è che non devi ordinare l'input, poiché l'ordinamento può essere realizzato alla fine con un output più piccolo;)
Moises Najar

@Wildcard se è necessario conservare l'ordine, si prega di menzionarlo nella domanda. Questo approccio è stato anche il mio primo pensiero e non menzionate l'ordine se non quello di dire che possiamo presumere che il file sia ordinato. Naturalmente, se il file è ordinato, è sempre possibile passare attraverso l'output di questa soluzione sort.
terdon

@terdon, certo che hai ragione; l'output può essere semplicemente riordinato. Buon punto. Vale anche la pena notare che !=0è implicito da come awkconverte i numeri in valori veri / falsi, rendendolo riducibile aawk '{a[$0]++}END{for(x in a)if(a[x]%2)print x}'
Wildcard

1

Se l'input è ordinato, che ne dici di questo awk:

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

1

con perl:

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

1

Utilizzando costrutti di shell,

uniq -c file | while read a b; do if (( $a & 1 == 1 )); then echo $b; fi done

1
Questo si interrompe con le linee che iniziano o finiscono con uno spazio bianco (o più, perché hai dimenticato di citare $b).
Gilles 'SO- smetti di essere malvagio' il

1

Puzzle divertente!

In 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

Verosimilmente in 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 in Haskell:

import Data.List (group)
main = interact $ unlines . map head . filter (odd . length) . group . lines

0

una versione: utilizzo i "delimitatori" per semplificare il ciclo interno (presuppone che la prima riga non lo sia __unlikely_beginning__e presuppone che il testo non termini con la riga __unlikely_ending__:, e aggiungo quella riga speciale delimitatore alla fine delle righe inserite. l'algoritmo può assumere entrambi:)

{ 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; }
'

Così :

  • ricordiamo il modello che stiamo attualmente osservando, aumentandolo di uno ogni volta che si ripresenta. [e se si ripresenta, saltiamo le 2 azioni successive, che sono valide per il caso in cui il modello cambia]
  • Quando il modello CAMBIA:
    • se non un multiplo di 2, stampiamo una ricorrenza del modello memorizzato
    • e in ogni caso quando il modello è cambiato: il nuovo modello memorizzato è il modello corrente e lo abbiamo visto solo una volta.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.