여러 섹션으로 구성된 텍스트 파일이 있는데, 각 섹션에는 두 개의 헤더 줄과 공백으로 구분된 단어로 구성된 본문 줄이 있습니다. 예는 다음과 같습니다:
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$
보시다시피 세 부분의 본문 라인에 backup
and 라는 단어만 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초의 실행 시간에 수행됩니다. 그러나 입력 파일 크기가 이보다 커지면 실제로는 이를 메모리에 저장하려고 시도하고 싶지 않습니다.