行を削除したり、ファイルの順序を変更せずにn回以上発生した単語をすべて削除します。

行を削除したり、ファイルの順序を変更せずにn回以上発生した単語をすべて削除します。

複数のセクションで構成されるテキストファイルがあり、各セクションには2つのヘッダー行とスペースで区切られた単語で構成される本文行があります。例は次のとおりです。

Shares for DED-SHD-ED-1:
    [--- Listable Shares ---]
        backup      backup2
Shares for DED-SHD-ED-2:
    [--- Listable Shares ---]
        ConsoleSetup        REMINST     SCCMContentLib$     SCCMContentLibC$        SEFPKGC$        SEFPKGD$        SEFPKGE$        SEFSIG$     Source      UpdateServicesPackages      WsusContent     backup      backup2
Shares for DED-SHD-BE-03:
    [--- Listable Shares ---]
        backup      backup2     print$

削除したいボディラインで見るとすべての単語が発生3回以上

  • 削除したいみんな「最初の2回以降のすべての発生」と同様に発生。
  • 一致させるトークンは、スペースで区切られた「単語」です。つまり、print$英数字部分だけでなく全体ですprint
  • 一致は「完全な単語」にのみ適用する必要があります。つまり、部分文字列の一致は適用されません。たとえば、すべての発生はbackup削除にのみ含まれ、backup削除には含まれませんbackup2
  • ヘッダー行(Shares for ...および)は考慮されません。[--- Listable Shares ---]

上記の入力に必要な出力は次のとおりです。

Shares for DED-SHD-ED-1:
    [--- Listable Shares ---]
                
Shares for DED-SHD-ED-2:
    [--- Listable Shares ---]
        ConsoleSetup        REMINST     SCCMContentLib$     SCCMContentLibC$        SEFPKGC$        SEFPKGD$        SEFPKGE$        SEFSIG$     Source      UpdateServicesPackages      WsusContent
Shares for DED-SHD-BE-03:
    [--- Listable Shares ---]
                        print$

ご覧のように、3つの部分の本文行にbackupandという単語だけがbackup2表示されるため、削除されました。ただし、ヘッダー行は編集対象とは見なされないため、セクションヘッダー行の、およびSharesfor変更されていません。Listable

いくつかの注意:

  • これらのファイルのサイズは100kBから1MBまでです。
  • 同様の解決策が見つかりawk '++A[$0] < 3'ましたが、最初の2つの項目を維持し、行全体を見てみましょう。
  • 私は特にAwkベースのソリューションを探しているのではなく、何でも(Perlを除く;)することができます。

答え1

最大1MBのファイルを処理する必要があるため、効率を向上させるには複数の配列の反転が必要です。単語を削除しているので、正確な間隔を維持することは重要ではないと思うので、代替行の各単語の前にTABが続きます。

これは、独自の awk プログラムを含む単一のシェル関数を含む Bash スクリプトです。入力ファイル引数を使用してstdoutに出力します。

結果をどのように確認したいのか分かりません。私は開発中に多くのデバッグをしました。たとえば、削除された単語とその頻度をstderrに書き込むのは簡単です。

#! /bin/bash

delByFreq () {

    local Awk='
BEGIN { SEP = "|"; Freq = 3; }
#.. Store every input line.
{ Line[NR] = $0; }
#.. Do not look for words on header lines.
/^Shares for / { next; }
/--- Listable Shares ---/ { next; }

#.. Keep an index to row/column of every unique word.
#.. So like: Ref ["backup2"] = "|2|3|5|1|5|7";
function Refer (row, txt, Local, f) {
    for (f = 1; f <= NF; ++f)
        Ref[$(f)] = Ref[$(f)] SEP row SEP f;
}
{ Refer( NR, $0); }

#.. Rearrange field indexes by line.
#.. So like: Del[row] = "|3|7|11"; for field numbers.
function refByLine (Local, word, j, n, V) {
    for (word in Ref) {
        n = split (Ref[word], V, SEP);
        if (n <= 2 * Freq) continue;
        for (j = 2; j < n; j += 2)
            Del[V[j]] = Del[V[j]] SEP (V[j+1]);
    }
}
#.. For every line with deletions, cross off the frequent words.
function Deletions (Local, row, j, f, n, V, X) {
    for (row in Del) {
        split (Del[row], V, SEP);
        split ("", X, FS); for (j = 2; j in V; ++j) X[V[j]];
        #.. Rebuild the line in field order. 
        split (Line[row], V, FS); Line[row] = "";
        for (j = 1; j in V; ++j)
            if (! (j in X)) Line[row] = Line[row] "\t" V[j];
    }
}
function Output (Local, r) {
    for (r = 1; r in Line; ++r) printf ("%s\n", Line[r]);
}
END { refByLine( ); Deletions( ); Output( ); }
'
    awk -f <( printf '%s' "${Awk}" ) "${1}"
}

    delByFreq "${1}"

答え2

出力と同じ間隔を持つように、GNUをawk4番目の引数として使用してsplit()一致する文字列を保持します。FS

$ cat tst.awk
{ begFld = 1 }
/^Shares for/ { begFld = 3 }
/\[--- Listable Shares ---]/ { begFld = NF+1 }
NR == FNR {
    for ( i=begFld; i<=NF; i++ ) {
        cnt[$i]++
    }
    next
}
{
    split($0,unused,FS,seps)
    out = seps[0]
    for ( i=1; i<=NF; i++ ) {
        out = out ( (i >= begFld) && (cnt[$i] >= 3) ? "" : $i ) seps[i]
    }
    print out
}

$ awk -f tst.awk file file
Shares for DED-SHD-ED-1:
    [--- Listable Shares ---]

Shares for DED-SHD-ED-2:
    [--- Listable Shares ---]
        ConsoleSetup        REMINST     SCCMContentLib$     SCCMContentLibC$        SEFPKGC$        SEFPKGD$        SEFPKGE$        SEFSIG$     Source      UpdateServicesPackages      WsusContent
Shares for DED-SHD-BE-03:
    [--- Listable Shares ---]
                   print$

while ( match(...) )代わりに、ループを使用してawkで同じことを実行できます。split(...); for (...)コードを数行だけ追加する必要があります。たとえば、以下はすべてのawkで動作します。

$ cat tst.awk
{ begFld = 1 }
/^Shares for/ { begFld = 3 }
/\[--- Listable Shares ---]/ { begFld = NF+1 }
NR == FNR {
    for ( i=begFld; i<=NF; i++ ) {
        cnt[$i]++
    }
    next
}
{
    i = 0
    out = ""
    while ( match($0,/[^ \t]+/) ) {
        sep = substr($0,1,RSTART-1)
        fld = substr($0,RSTART,RLENGTH)
        out = out sep ( (++i >= begFld) && (cnt[fld] >= 3) ? "" : fld )
        $0 = substr($0,RSTART+RLENGTH)
    }
    print out $0
}

$ awk -f tst.awk file file
Shares for DED-SHD-ED-1:
    [--- Listable Shares ---]

Shares for DED-SHD-ED-2:
    [--- Listable Shares ---]
        ConsoleSetup        REMINST     SCCMContentLib$     SCCMContentLibC$        SEFPKGC$        SEFPKGD$        SEFPKGE$        SEFSIG$     Source      UpdateServicesPackages      WsusContent
Shares for DED-SHD-BE-03:
    [--- Listable Shares ---]
                   print$

END編集:@Paul_Pedantと私は、以下に示すように入力を配列に読み込み、そのセクションで処理することの長所と短所についてコメントしました。彼の台本上記のスクリプトと同様に、入力ファイルを2回読み取ったので、スクリプトをシェルスクリプトに入れ、bash shebangを追加しました。

#!/usr/bin/env bash

awk '
    { begFld = 1 }
    ...
        print out
    }
' "$1" "$1"

次に、次のように、OPs 9行入力ファイルの100万コピーである入力ファイルを作成します。

$ awk '{r=(NR>1 ? r ORS : "") $0} END{for (i=1; i<=1000000; i++) print r}' file > file1m

その後、私のスクリプトを定期的に実行します。

$ time ./tst_ed.sh file1m > ed.out

real    1m3.814s
user    0m57.781s
sys     0m0.265s

しかし、Paulsスクリプトを実行しようとすると、次のようになります。

$ time ./tst_paul.sh file1m > paul.out

ノートパソコンからヘリコプターが離陸する音が聞こえ始めて5分後に停止し、ノートパソコンが再び安定するまで3分ほど待ちました。

その後、100,000個のファイルに対して2つの方法を試しました。

$ awk '{r=(NR>1 ? r ORS : "") $0} END{for (i=1; i<=100000; i++) print r}' file > file100k

$ time ./tst_ed.sh file100k > ed.out                                            
real    0m6.035s
user    0m5.875s
sys     0m0.031s

$ time ./tst_paul.sh file100k > paul.out

しかし、結局私はPaulsを邪魔しなければなりませんでした(10分間時間を与えました)。

その後、ファイルを使用して10,000回試しました。

$ awk '{r=(NR>1 ? r ORS : "") $0} END{for (i=1; i<=10000; i++) print r}' file > file10k

$ time ./tst_ed.sh file10k > ed.out                                             
real    0m0.783s
user    0m0.609s
sys     0m0.045s

$ time ./tst_paul.sh file10k > paul.out

real    0m1.039s
user    0m0.921s
sys     0m0.031s

今回は両方の出力が得られたので、両方diff -bで実行して出力が異なることを発見しました。

$ diff -b ed.out paul.out |head
1c1
< Shares for
---
> Shares for DED-SHD-ED-1:
4c4
< Shares for
---
> Shares for DED-SHD-ED-2:
7c7
< Shares for

私は行末から重複した値を削除しましたが、Shares for ...Paulはそうではありませんでした。 IdkはこれがOPが望む動作であるかもしれませんし、重要であっても非現実的な入力かもしれません。

それから1,000回を試しました。

$ awk '{r=(NR>1 ? r ORS : "") $0} END{for (i=1; i<=1000; i++) print r}' file > file1k

$ time ./tst_ed.sh file1k > ed.out

real    0m0.133s
user    0m0.077s
sys     0m0.015s

$ time ./tst_paul.sh file1k > paul.out

real    0m0.133s
user    0m0.046s
sys     0m0.046s

そして100番:

$ awk '{r=(NR>1 ? r ORS : "") $0} END{for (i=1; i<=100; i++) print r}' file > file100

$ time ./tst_ed.sh file100 > ed.out

real    0m0.080s
user    0m0.000s
sys     0m0.015s

$ time ./tst_paul.sh file100 > paul.out

real    0m0.081s
user    0m0.000s
sys     0m0.000s

したがって、約1k以下のOPデータ冗長(つまり、入力ファイルの最大約10k行)の場合、データをメモリに保存してENDセクションから解析するか、入力ファイルを2回読み取るかは実行速度の問題です(10分1秒の実行時間に達すると誰が気になりますか?)約10,000回の繰り返し(約100,000の入力行)で2読み方が少し速いですが、どちらもすぐに約1秒の実行時間に行われます。ただし、入力ファイルのサイズがこれより大きくなると、実際にはそれをメモリに保存しようとしたくありません。

関連情報