重複行をペアで削除しますか?


16

今日、このユースケースに出会いました。これは、一見シンプルなようだが、と周りいじるsortuniqsedそしてawkそれは自明だことを明らかにしました。

重複行のすべてのペアを削除するにはどうすればよいですか?つまり、指定された行の重複が偶数個ある場合は、それらをすべて削除します。重複する行の数が奇数の場合、1つを除くすべてを削除します。(ソートされた入力を想定できます。)

クリーンでエレガントなソリューションが望ましいです。

入力例:

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

出力例:

a
d
e

回答:


6

私はsedこの質問を投稿してからまもなく答えを出しました。sedこれまで他の誰も使用していませんので、ここにあります:

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

より一般的な問題(3組の行を削除するのはどうですか?または4、5の場合はどうですか?)で少し遊んでみると、次の拡張可能なソリューションが提供されました。

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

行のトリプルを削除するために拡張されました:

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

または、四角形の行を削除するには:

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

sed 他のほとんどのオプションに比べて追加の利点があります。これは、実際に重複をチェックする行数よりも多くのメモリストレージを必要とせずに、ストリームで実際に動作する能力です。


以下のようcuonglmはコメントで指摘し、Cにロケールを設定すると、正常にマルチバイト文字を含む行を削除するために失敗を避けるために必要です。したがって、上記のコマンドは次のようになります。

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:ロケールをに設定したい場合がありますC。そうしないと、マルチバイトロケールで、そのロケールの無効な文字が原因でコマンドが失敗します。
-cuonglm

4

それは非常にエレガントではありませんが、私が思いつくことができるほど簡単です:

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

substr()はuniq出力を削除します。これは、行の重複が9,999,999を超えるまで機能します(この場合、uniqの出力は9文字を超える可能性があります)。


試したところuniq -c input | awk '{if ($1 %2 == 1) { print $2 } }'、同じように機能するように見えました。substrバージョンが優れている理由は何ですか?
ジョセフR.

1
@JosephR。、行に空白がある場合、コメントのバージョンは失敗します。
ワイルドカード

それは本当です。その場合、フィールド$2を印刷するループは$NFより堅牢ではないでしょうか?
ジョセフR.

@JosephR .:なぜあなたの代替手段がより堅牢になると思いますか?複数の連続したスペースがある場合、正しく動作させるのが難しい場合があります。例えばfoo   bar
G-マンは「元に戻すモニカ言う

@JosephR。、いいえ、空白の区切りを変更/削除するためです。 uniq(少なくともGNU coreutilsでは)テキスト自体の前に正確に9文字を確実に使用するようです。しかし、これはどこにも文書化されておらず、POSIX仕様にはありません。
ワイルドカード

4

awk以下のこのスクリプトを試してください:

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

lines.txtファイルはソートされていると想定されます。

テスト:

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

4

pcregrep与えられたサンプルのために:

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

またはより一般的な方法で:

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

最後に「行末」アンカーがあるべきではありませんか?そうしないと、後続の文字以外の行と一致する行で失敗します。
ワイルドカード

@Wildcardええ、それはましです。修正済み、thx。
-jimmij

とてもかっこいい!(+1)
JJoao

4

入力がソートされている場合:

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

ここでアンカーの失敗があります。たとえばpineapple\napple\ncoconut、実行してみてくださいpinecoconut。出力はです。
ワイルドカード

@Wildcard:ありがとう。あなたが正しいです。私の更新が...理にかなっているかどうかを確認してください
JJoao

1
うん。なぜ修飾子\n$指定する代わりに使用しているのか疑問に思っていましたが/m、使用$すると削除された行の代わりに空白行が残ることに気付きました。よさそうだ。ノイズを追加しただけなので、間違ったバージョンを削除しました。:)
ワイルドカード

@wildcardは、ノイズリダクション☺をありがとう
JJoao

3

pythonはこれが好きです、例えば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

各レコードのハッシュを使用してawkを選択した質問を理解したので、この場合はRS = \ nと仮定していますが、他の種類の配置を考慮するように変更することができ、パラメータまたは小さなダイアログを使用して、奇数ではなく偶数の担当者。すべての行がハッシュとして使用され、そのカウントが増加します。ファイルの最後で、配列がスキャンされ、レコードの偶数カウントごとに出力されます。チェックするためにカウントを含めていますが、a [x]を削除するだけでその問題を解決できます。

HTH

カウントラインコード

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

サンプルデータ:

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

サンプル実行:

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

これはすばらしいawkコードですが、残念ながらawk連想配列はまったく順序付けられておらず、順序を維持していません。
ワイルドカード

@Wildcard、私はあなたに同意します。ソート順ではなく入力順が必要な場合は、追加のハッシュキーを介して実装できます。これの利点は、ソート順が入力をソートする必要がないことです最後に小さな出力で作成できます;)
モイゼスナジャー

@Wildcard注文を保存する必要がある場合は、質問にその旨を記載してください。このアプローチも私の最初の考えであり、ファイルがソートされていると仮定できると言う以外に順序については言及しません。もちろん、ファイルが並べ替えられている場合は、このソリューションの出力をいつでも渡すことができますsort
テルドン

@terdon、もちろんあなたは正しい。出力を再度並べ替えることができます。いい視点ね。また、ことは注目に値します!=0かによって暗示されるawkには、この還元性を作り、真/偽の値に変換数awk '{a[$0]++}END{for(x in a)if(a[x]%2)print x}'
ワイルドカード

1

入力がこれについてどうソートされている場合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

perlの場合:

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

1

シェル構造を使用して、

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

1
空白で始まる行または空白で終わる行で中断します(引用を忘れたため、それ以上$b)。
ジル 'SO-悪である停止

1

楽しいパズル!

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

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)

Haskellで簡潔に:

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

0

バージョン:「デリミタ」を使用して内部ループを簡素化します(最初の行が __unlikely_beginning__で、テキストがline:__unlikely_ending__で終わらないことを前提とし、入力された行の最後に特別な区切り行を追加します。アルゴリズムは両方を想定できます:)

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

そう :

  • 現在見ているパターンを覚えており、再発するたびに1つずつ増やしていきます。[そして、それが再発した場合、次の2つのアクションをスキップします。これは、パターンが変更された場合のためです]
  • パターンが変更されるとき:
    • 2の倍数でない場合、記憶されたパターンの1つの発生を出力します
    • そして、パターンが変更されたすべての場合において、新しい記憶されたパターンは現在のパターンであり、一度だけ見ました。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.