【问题标题】:Use grep to remove words from dictionary whose roots are already present使用 grep 从字典中删除已经存在根的单词
【发布时间】:2014-01-20 15:40:06
【问题描述】:

我正在尝试编写一个随机密码生成器。我有一本包含一堆单词的字典,我想删除根已经在字典中的单词,这样字典看起来像:

ablaze
able
abler
ablest
abloom
ably

最终只有

ablaze
able
abloom
ably

因为abler 和ablest 包含之前使用的able。

我更喜欢用 grep 来做这件事,这样我就可以了解更多关于它是如何工作的。我能够用 c 或 python 编写一个程序来做到这一点。

【问题讨论】:

  • 你试过什么?你得到了什么?。现在,您的问题似乎是“为我编写这个程序,以便我可以从中学习” - 请展示一些研究成果。
  • @tucuxi 好点。我真的很想看到有人用 grep 来做,因为我可以用 c 或 python 来做。我已更新问题以反映这一点。
  • 我认为这比您想象的要复杂得多。盲目地删除以“est”、“er”等结尾的词是一回事,而不是知道两个词是相关的。如果遇到 goose vs geese 这样的词,你会怎么做?或者两个相似但不相关的词呢?例如 doner vs done?
  • @user1146334 我很乐意留下 geese 并删除 doner(虽然 doner 不在那个字典里,但音调和碳粉有同样的问题)。我不打算编译一个英语单词词根列表,只是清除那些会使记住正确密码短语变得更加困难的后缀(它正确吗?更正?更正?马?马?)。
  • 添加了概念验证答案。虽然我会推荐 Python(或几乎任何其他东西)而不是 Bash 进行文本处理。

标签: linux bash dictionary grep


【解决方案1】:

如果对列表进行排序以使较短的字符串始终位于较长的字符串之前,那么您可以通过简单的 Awk 脚本获得相当好的性能。

awk '$1~r && p in k { next } { k[$1]++; print; r= "^" $1; p=$1 }' words

如果当前单词匹配前缀正则表达式r(稍后定义)并且前缀p(同上)在可见键列表中,请跳过。否则,将当前单词添加到前缀键中,打印当前行,在行首创建一个匹配当前单词的正则表达式(现在是前缀正则表达式r)并记住p 中的前缀字符串。

如果所有相似的字符串总是相邻的(如果你按词法对文件进行排序,它们会是这样),我猜你也可以完全取消 kp

awk 'NR>1 && $1~r { next } { print; r="^" $1 }' words

【讨论】:

  • 虽然不是基于grep,但这个解决方案给我的印象是最优雅的,并且可能是性能最好的。
  • 是的,这是迄今为止最快的解决方案。
【解决方案2】:

这是基于输入文件已排序的假设。在这种情况下,在查找每个单词时,可以安全地跳过第一个单词之后的所有匹配项(因为它们将对应于“具有不同后缀的相同单词”)。

#/bin/bash
input=$1
while read -r word ; do
    # ignore short words
    if [ ${#word} -lt 4 ] ; then continue; fi
    # output this line
    echo $word 
    # skip next lines that start with $word as prefix
    skip=$(grep -c -E -e  "^${word}" $input)
    for ((i=1; i<$skip; i++)) ; do read -r word ; done
done <$input

拨打./filter.sh input &gt; output

在我的/usr/share/dict/american-english 字典中找到的所有 4 个或更多字母的单词都需要不到 2 分钟的时间。该算法是 O(n²),因此不适合大文件。

但是,如果您完全避免使用 grep,则可以大大加快速度。这个版本只需要 4 秒就可以完成这项工作(因为它几乎不需要每个单词扫描整个文件一次)。由于它对输入执行单次传递,因此其复杂度为 O(n):

#/bin/bash
input=$1
while true ; do
    # use already-read word, or fail if cannot read new
    if [ -n "$next" ] ; then word=$next; unset next;
    elif ! read -r word ; then break; fi
    # ignore short words
    if [ ${#word} -lt 4 ] ; then continue; fi
    # output this word
    echo ${word}
    # skip words that start with $word as prefix
    while read -r next ; do
        unique=${next#$word}
        if [ ${#next} -eq ${#unique} ] ; then break; fi
    done
done <$input

【讨论】:

  • 这比我尝试遍历文件的速度要快得多。即使有 20k 字,也只用了不到一分钟。
  • american-english 文件上(限制为 4 个字母的单词,但最初超过 99k 单词)。使用--mmap 标志并没有太大区别。免责声明:我有一个 SSD。
  • 我的字典是小写的美式英语单词,超过 4 个字母 [a-z] 且没有撇号。
【解决方案3】:

假设您想从具有相同前四个(最多十个)字母的单词开始,您可以执行以下操作:

cp /usr/share/dict/words words
str="...."
for num in 4 5 6 7 8 9 10; do
    for word in `grep "^$str$" words`; do
        grep -v "^$word." words > words.tmp
        mv words.tmp words
    done
    str=".$str"
done

你不会希望以 1 个字母开头,除非你的字典中没有“a”等。

【讨论】:

  • 我错过了什么吗?该脚本不会在任何地方使用 $num。
  • 它不需要$num的值,因为$str的长度每次都在增加。更多的是让用户知道发生了什么。它本可以这样写:for str in .... ..... ...... (etc)
  • 我现在明白了。谢谢。
【解决方案4】:

试试这个 BASH 脚本:

a=()
while read -r w; do
   [[ ${#a[@]} -eq 0 ]] && a+=("$w") && continue
   grep -qvf <(printf "^%s\n" "${a[@]}") <<< "$w" && a+=("$w")
done < file

printf "%s\n" "${a[@]}"
ablaze
able
abloom
ably

【讨论】:

  • 做得很好。建议:将&amp;&amp; continue附加到第一个条件(... -eq 0 ...)的行,这样您就可以从下一行删除条件(... -gt 0 ...)。
  • @mklement0:谢谢你的好建议。已编辑。
  • 很高兴听到它 - 并感谢您的编辑。您现在可以从循环主体的第 2 行中删除 [[ ${#a[@]} -gt 0 ]] &amp;&amp; - 编辑后,现在根据定义始终满足条件。
【解决方案5】:

您似乎想将副词组合在一起。一些副词,包括那些也可以是形容词的副词,使用 er 和 est 来构成比较:

  • 能干的,能干的,最能干的
  • 快,快,快
  • 快,快,快
  • 简单、轻松、简单

此过程在自然语言处理中称为词干提取,可以使用词干分析器或词形还原器来实现。 python的NLTK模块中有流行的实现,但问题并没有完全解决。最好的开箱即用词干分析器是雪球词干分析器,但它不会将副词词根词根。

import nltk

initial = '''
ablaze
able
abler
ablest
abloom
ably
fast
faster
fastest
'''.splitlines()

snowball = nltk.stem.snowball.SnowballStemmer("english")
stemmed = [snowball.stem(word) for word in initial]

print set(stemmed)

输出...

set(['', u'abli', u'faster', u'abl', u'fast', u'abler', u'abloom', u'ablest', u'fastest', u'ablaz'])

另一种选择是使用正则表达式词干分析器,但恐怕这有其自身的困难。

patterns = "er$|est$"
regex_stemmer = nltk.stem.RegexpStemmer(patterns, 4)
stemmed = [regex_stemmer.stem(word) for word in initial]

print set(stemmed)

输出...

set(['', 'abloom', 'able', 'abl', 'fast', 'ably', 'ablaze'])

【讨论】:

  • 我在原始问题的 cmets 中提到,我并没有试图编译一个根词列表,只是消除了可能使随机词组合更难记住的东西(而且我可以在 python 中解决这个问题,但正在寻找一个直接的 grep 解决方案)。不过感谢您的建议。
  • 我的错,第一次尝试堆栈溢出答案,我什至没有阅读问题 :0 。经验教训。
【解决方案6】:

如果你只是想去掉一些单词,这个粗略的命令会起作用。请注意,它会抛出一些像 best 这样的合法词,但这很简单。它假设您有一个每行一个单词的 test.txt 文件

egrep -v "er$|est$" test.txt &gt;&gt; results.txt

egrep 与grep -E 相同。 -v 表示丢弃匹配的行。 x|y 表示 x or y 是否匹配,$ 表示行尾,因此您会寻找以 er 或 est 结尾的单词

【讨论】:

  • 事实上,“best”是最高级的,所以它实际上可能是一个有效的目标,但这也会过滤掉“test”、“behest”和“father”。对于任何实际用途而言,可能过于简单。
猜你喜欢
  • 2016-02-05
  • 2022-12-09
  • 1970-01-01
  • 2019-02-18
  • 1970-01-01
  • 2012-07-25
  • 2015-07-27
  • 2012-06-29
  • 2019-08-29
相关资源
最近更新 更多