Work the Shell - 制作一个 <emphasis>疯狂填词</emphasis> 游戏生成器

作者:Dave Taylor

我的儿子正处于分解句子、分析句子结构和学习词性的年龄。我呢?如果一个湿漉漉的、臭烘烘的红球砸在我的头上,我都分不清副词和形容词。这就是我需要编辑的原因!

然而,任何事物都有游戏,而学习词性的最佳游戏之一是一个简单的游戏,它从我小时候就存在了:疯狂填词。你知道我在说什么,它将简单的句子,比如:“当我的狗高兴时,它会跳跃和吠叫,尾巴摇摆得飞快。” 变成:“当我的 [ 名词 ] 是 [ 形容词 ] 时,它 [ 动词 ] 和 [ 动词 ],它的 [ 名词 ] 摇摆得飞快,像一个 [ 名词 ]。”

问题是,我们能否编写一个 shell 脚本来执行这种转换?当然,答案是肯定的。

识别词性

这个项目有两个挑战:弄清楚要用词性替换哪些词,以及弄清楚给定词的词性。让我们倒过来解决这些问题。

事实证明,许多不同的网站允许您查找单词并提供其定义和词性。我用于此练习的网站来自普林斯顿大学,因为它速度快、易于解析且易于提交查询。

要查找例如 “dog” 的词性,要调用的 URL 很简单:wordnetweb.princeton.edu/perl/webwn?s=dog

结果将词性突出显示为 h3 行,因此隔离该元素轻而易举

curl --silent "lookup$word" | grep '<h3>'

这个特定的词演示了问题的一个细微差别:许多词有不止一种词性,宠物狗和跟踪你每一步的人之间的区别就证明了这一点。果然,结果是

<h3>Noun</h3>
</ul><h3>Verb</h3>

为了简单起见,我们只取第一个匹配项,通过添加以下内容可以轻松完成| head -1到管道。接下来,让我们将其全部转换为小写并去除 HTML

| tr '[:upper:]' '[:lower:]' | sed 's/<h3>//;s/<\/h3>//'

这两者都值得解释一下。你可能已经见过tr '[A-Z]' '[a-z]'作为将大写字母音译为小写字母的更常见方法,如果你用英语工作,这很好用。使用字符集 “:upper:” 和 “:lower:” 是一种更便携且更受欢迎的替代方法。

sed 命令还允许您通过简单地用分号分隔来指定多个要应用的命令参数。我们这里所做的是将 <h3> 替换为空字符串(例如,删除它),然后对 </h3> 执行相同的操作。

这就是我们获得词性所需要的一切。例如

$ lookup="http://wordnetweb.princeton.edu/perl/webwn?s="
$ word="happy"
$ curl --silent "$lookup$word" | grep '<h3>' |
  tr '[:upper:]' '[:lower:]' | sed 's/<h3>//;s/<\/h3>//'
adjective

而且,最难的部分已经完成!

选择要替换的词

对于本文,让我们使用替换密度常数来确定是否应该替换任何给定的词。密度越高,输入流中给定的词就越有可能被其词性替换。

这很懒惰,而且不是一个好的解决方案,因为它可以像匹配 “dog” 或 “tail” 一样容易地匹配 “is” 或 “the”,但现在让我们继续使用它,以了解它将如何组合在一起。我们稍后会回到它,并提高选择标准的复杂性。明白了吗?很好!

对于给定的词,决定是否替换其词性可以按如下方式计算,假设我们有一个名为 density 的变量,它具有非零整数值

if [ $(( $RANDOM % $density )) = 1 ] ; then

$RANDOM 是 Bourne shell 中那些很酷的魔法变量之一,每次您引用它时,它都有不同的值——很方便!

将它们放在一起

让我们把这些放在一起,看看我们得到了什么。我们将使用 5 的初始密度,理论上这意味着,如果我们有一个适当的随机 $RANDOM,每个词都应该有 1:5 的机会被替换。

脚本需要逐字读取输入,并在处理每个词时进行测试。这可以使用以下循环结构轻松完成,假设文本输入来自 stdin

while read sentence ; do
  for word in $sentence ; do

现在,我们添加随机条件,并准备好一个骨架进行测试

while read sentence ; do
  for word in $sentence ; do
    if [ $(( $RANDOM % $density )) -eq 1 ] ; then
      echo "(($word))"
    else
      echo $word
    fi
  done
done

您可以看到,在这个阶段,我们将要用 “(())” 输出我们计划替换的词。这是一个快速测试

echo this is a test mad-lib input | sh make-madlib.sh
this
is
((a))
test
((mad-lib))
input

在本月结束之前,还有一个小小的调整——我们如何让词语出现在同一行上?这很容易。请记住,每个代码循环本质上都是它自己的一个小脚本,因此可以通过在最外层循环的末尾添加四个字符来完成此任务

done
done | fmt

这就是您所要做的——添加|fmt在第二个 done 语句之后。现在当它运行时

echo this is a test mad-lib input | sh make-madlib.sh
this is a ((test)) ((mad-lib)) input

下个月,我们将把词性查找代码添加到条件中,然后花一些时间探索更复杂的选词算法。显然,随机性没有那么有利。

Dave Taylor 破解 shell 脚本已经很长时间了,30 年。他是流行的 Wicked Cool Shell Scripts 的作者,可以在 Twitter 上找到他,账号是 @DaveTaylor,更广泛地可以在 www.DaveTaylorOnline.com 上找到。

加载 Disqus 评论