줄을 삭제하거나 파일 순서를 변경하지 않고 n번 이상 발생하는 모든 단어를 제거합니다.

줄을 삭제하거나 파일 순서를 변경하지 않고 n번 이상 발생하는 모든 단어를 제거합니다.

여러 섹션으로 구성된 텍스트 파일이 있는데, 각 섹션에는 두 개의 헤더 줄과 공백으로 구분된 단어로 구성된 본문 줄이 있습니다. 예는 다음과 같습니다:

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$

삭제하고 싶어요바디라인으로 보면모든 단어 발생세 번 이상.

  • 삭제하고 싶어요모두"처음 두 번 이후의 모든 발생"뿐만 아니라 발생.
  • 일치시킬 토큰은 공백으로 구분된 "단어"입니다. 즉, 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$

보시다시피 세 부분의 본문 라인에 backupand 라는 단어만 backup2나타나기 때문에 제거되었습니다. 그러나 헤더 행은 편집 대상으로 간주되지 않으므로 섹션 헤더 행의 Shares, for및 는 변경되지 않은 상태로 유지됩니다.Listable

몇 가지 참고사항:

  • 이러한 파일의 크기는 100kB에서 1MB까지입니다.
  • 비슷한 솔루션을 찾았 awk '++A[$0] < 3'지만 처음 두 항목을 유지하고 전체 행을 살펴봅니다.
  • 나는 특별히 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를 awk네 번째 인수로 사용하여 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와 저는 아래 표시된 것처럼 입력을 배열로 읽은 다음 해당 섹션에서 처리하는 것의 장단점에 대해 의견에서 논의했습니다.그의 대본위의 스크립트와 마찬가지로 입력 파일을 두 번 읽으므로 스크립트를 쉘 스크립트에 넣고 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개 파일에 대해 다음 두 가지 방법을 시도했습니다.

$ 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 섹션에서 구문 분석할지 아니면 입력 파일을 두 번 읽는지는 실행 속도의 문제입니다( 10분의 1초 실행 시간에 도달하면 누가 신경쓰나요?) 약 10,000회 반복(약 100,000개 입력 행)에서 2읽기 방법이 조금 더 빠르지만 둘 다 곧 약 1초의 실행 시간에 수행됩니다. 그러나 입력 파일 크기가 이보다 커지면 실제로는 이를 메모리에 저장하려고 시도하고 싶지 않습니다.

관련 정보