删除出现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$

正如您所看到的,只有单词backupbackup2被删除,因为它们出现在三个部分的正文行中。但是,节标题行中的SharesforListable保持不变,因为不应考虑编辑标题行。

一些注意事项:

  • 这些文件的大小范围为 100kB 到 1MB。
  • 我已经找到了类似的解决方案awk '++A[$0] < 3',但这会保留前两次出现并查看整行。
  • 我并不是专门寻找基于 Awk 的解决方案,任何东西(除了 Perl ;))都可以。

答案1

由于要求覆盖最大 1MB 的文件,因此需要进行几次数组反转以提高效率。因为我们要删除单词,所以我认为保留精确的间距并不重要,因此替换行中的每个单词前面都带有 TAB。

它是一个包含单个 shell 函数的 Bash 脚本,该函数本身包含一个 awk 程序。它采用一个输入文件参数,并输出到 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

使用 GNUawk作为第四个参数来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$

编辑:@Paul_Pedant 和我在评论中讨论将输入读取到数组中然后在该END部分中对其进行处理的优点/缺点,如下所示他的剧本与上面的脚本一样,读取输入文件两次,因此我将我的脚本放入 shell 脚本中并添加了 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 分钟,让我的笔记本电脑再次稳定下来。

然后我在 100k 次文件上尝试了这两种方法:

$ 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 分钟)。

然后我尝试了 10k 次文件:

$ 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 ...而保罗的则没有。 idk 这将是 OP 期望的行为,或者即使它很重要,它也可能只是不切实际的输入。

然后我尝试了 1k 次:

$ 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 部分进行解析,还是读取输入文件两次,都与执行有关速度(一旦你进入十分之一秒的执行时间谁在乎?)并且在大约 10k 重复(大约 100k 输入行)时,两次读取方法要快一些,但两者在大约 1 秒的执行时间下都很快。但是,一旦输入文件大小大于此值,您确实不想尝试将其存储在内存中。

相关内容