使用 Shell 脚本分析歌词,第二部分

作者:Dave Taylor

在我的上一篇文章中,我开始探索歌词。 这不是为了让您拥有史诗般的卡拉 OK 之夜,而是更多地从分析歌词和其中的单词用法的角度出发。 激发我好奇心的具体问题是一篇文章,该文章声称多产的歌曲创作二人组 Paul McCartney 和 John Lennon 在披头士乐队的歌曲中提到了 160 次“love”这个词。

您如何测试这种说法? 您可以通过从专门研究歌词的网站(在本例中为 MLDb)提取歌词,并使用 shell 脚本对其进行分析来实现。

我在我的上一篇文章中编写了第一部分,这是一个脚本,用于提取归因于披头士乐队的每首已发布歌词的链接,并逐步浏览该网站的每 30 页分页结构。 该网站总共列出了该乐队的 240 首歌曲。 在 240 首歌曲中,他们只提到了 160 次“love”? 我对此表示怀疑。

在本文中,我扩展了这个想法,下载了所有这些歌曲中每一首的歌词,然后使用一些基本的命令行工具来分析单词用法和频率。

告诉我你看到了什么

我上一篇文章中脚本的输出是一组具有以下内容的文件


<a href="song-32476-i-am-the-walrus.html">I Am The Walrus
<a href="song-32520-come-together.html">Come Together
<a href="song-32461-yellow-submarine.html">Yellow Submarine
<a href="song-32585-day-tripper.html">Day Tripper
<a href="song-32557-let-it-be.html">Let It Be

以站点域名为前缀,使其成为完全限定的 URL,并且每个歌曲页面地址如下所示:http://www.mldb.org/song-32520-come-together.html。

让我们回到源代码中,看看这些行是如何被提取出来的,因为拼接一个更好的 URL 并将其输出保存为歌词源文件应该很容易,对吧?

这是有问题的行


curl -s "$url&from=$start" | sed 's/</\
</g' | grep 'href="song-' > $output$start

然而,与其只是将其写入输出文件,不如构建一个合适的 URL 并将其传递给一个子例程,该子例程可以使用它来提取歌词? 听起来很容易,但请记住,上面产生的是 30 首歌曲的列表,而不是单个歌曲匹配项。

事实上,最简单的解决方案是更改代码以坚持使用输出文件,但将其设为临时文件,因为它仅供内部使用。 然后我可以根据需要逐行浏览该文件。

首先,curl 语句中的简单更改


curl -s "$url&from=$start" | sed 's/</\
</g' | grep 'href="song-' > $tempfile

接下来,这是可以遍历输出文件的代码,逐行调用 shell 脚本函数


while read lineofdata
do
  songnum=$(echo $lineofdata | cut -d\" -f2 | cut -d- -f2)
  fullurl="http://www.mldb.org/$(echo $lineofdata | \
     cut -d\" -f2)"
 savelyrics "$songnum" "$fullurl"
done < $tempfile

我为什么要单独保存歌曲编号? 因为这使得文件输出名称很容易,因为我想将每首匹配歌曲的歌词都保存下来。 是的,我可以将它们放在一个巨大的文件中,但不知何故这似乎不太合适。

所有工作都由 savelyrics 函数完成,这是我编写它的方式,花了一些时间微调过滤和转换


function savelyrics
{
   # extract just the lyrics and save them
   songnum="$1"
   fullurl="$2"

   curl -s "$fullurl" | sed -n '/songtext/,/\/table/p' | \
     sed 's/>/\
/g;s/\<\/p>//g' | grep -E "(<br|</p)" | \
     sed 's/\<br \///g;s/\<\/p//g' | uniq > $output$songnum.txt

   return 0
}

curl 语句获取包含完整歌词的网页,这些歌词大致由 CSS 类 ID songtext 划定,并包含在一个简陋的 HTML 表格中,因此歌词的最后一行出现在表格关闭之前:</table>

正如我之前提到的,当您想要提取清晰划分的文本段落时,sed 是您的朋友。 使用 sed -n 来停止其通常的回显所有看到的内容的行为,并使用 /start/,/end/p 仅打印这两个模式之间的行。

问题在于,即使您将每个右尖括号转换为回车符(以将源文件分解为大量单独的行以进行进一步处理),它仍然有点混乱。 大多数歌词行都以序列 <br /> 结尾,但歌词的最后一行却以 </p> 结尾。

为了捕获这两行并筛选掉其他所有内容,grep 具有方便的 -E 标志,它允许您指定正则表达式。 正则表达式本身就是一个世界(我在之前的专栏中深入探讨过),但足以说明 (A|B) 形式的模式会生成具有模式 A 或模式 B 的行,正如您所希望的那样。

这实际上就是所有的工作。 管道中的第三个 sed 只是删除了片段残留的 HTML 代码


sed 's/\<br \///g;s/\<\/p//g'

(请记住,s/old/new/g 格式用于全局替换。 这只是看起来更复杂,因为“/”是源模式的一部分。 “;”允许您为了方便起见将两个 sed 命令序列放在同一行上。)

快速执行 uniq 以最大程度地减少空白行,就完成了,准备保存。 歌曲歌词输出示例


$ head lyrics.32586.txt
Try to see it my way
Do I have to keep on talking till I can't go on
While you see it your way
Run the risk of knowing that our love may soon be gone
We can work it out, we can work it out

Think of what you're saying
You can get it wrong and still you think that it's alright
Think of what I'm saying

知道这首歌吗? 现在脑海中是否回响着它? 如果此时切换到卡拉 OK,我绝对可以继续处理其余的歌词。

试着从我的角度看

我对脚本做了一处小的调整,以便在运行时状态输出会很有趣。 现在它出现在调用 savelyrics 之前


echo "$lineofdata ($songnum)" | cut -d\> -f2

因此,运行时,脚本具有这样的输出


$ sh getsongs.sh
I Am The Walrus (32476)
Across The Universe (32554)
Come Together (32520)
Yellow Submarine (32461)
Day Tripper (32585)
. . .
Maggie Mae (61310)
Back In The USSR (61300)
When I'm Sixty-Four (61299)
Good Morning Good Morning (61286)
Got To Get You Into My Life (61285)

看起来不错。 这里快速复查一下


$ ls lyrics.* | wc -l
     240

获得了所有 240 首歌曲,所以让我们进行一些分析。 首先,有多少首歌曲的标题中包含“love”这个词? 借助新的改进的脚本输出,这很容易


$ sh getsongs.sh | grep -i love  | wc -l
      13

纵观所有歌曲,有多少歌词行包含“love”这个词?


$ cat lyrics.* | grep -i love  | wc -l
     445

这比 160 多得多! 但是,对于包含“love”这个词不止一次的行呢? 它们只会被计算一次。 事实上,更传统的词语分析可能会很有趣。 但是,让我们从一首歌曲开始,即欢快地命名为“I'm A Loser”的歌曲


$ cat lyrics.61278.txt  | tr ' ' '\
' | tr '[[:upper:]]' '[[:lower:]]' | sort | \
  uniq -c | sort -rn | head
  17 i
  13 a
  12 i'm
   9 and
   8 to
   8
   7 loser
   6 have
   5 what
   4 not

请注意,第一个 tr 将所有空格转换为回车符,第二个确保所有内容都为小写(使用 ANSI 集符号以提高可移植性),然后我只需 sort 所有单词,使用 uniq -c 生成计数,然后按数字计数反向 sort 并检查前十个匹配项。 “I”是这首歌词中最常见的词,其次是“a”。 并不令人惊讶。 请注意,“loser”在这首歌中仅出现七次(实际上都在再现部分)。

而且,如果我大量检查每首歌词呢? 这是一个非常相似的命令行调用


$ cat lyrics.*.txt  | tr ' ' '\
' | tr '[[:upper:]]' '[[:lower:]]' | sort | \
  uniq -c | sort -rn | head
5990
1728 you
1475 i
1060 the
 862 to
 781 me
 769 and
 765 a
 438 in
 432 my

这些都是语义分析中通常被认为是“噪音词”的词,因此让我们扩展 head 以包含更多匹配项,我将手动编辑最终结果以供您阅读


1728 you
781 me
399 love
366 know
250 she
205 her

还有很多,但现在有答案了,女士们先生们! 我现在可以明确地说,单词 love 在披头士乐队的歌曲中准确地出现了 399 次,在该乐队的歌曲标题中也出现了 13 次(如前所述)。

你好再见

花了一段时间才找到解决方案,但这种分析是博弈论中所谓的分而治之的一个极好的例子。 解决一个大问题,并不断将其分解为越来越小的部分,直到您开始了解如何解决小部分。 然后将所有内容重新构建起来,以便您可以解决更大的挑战。

现在,The Monkees 乐队呢? 他们在歌词中实际引用猴子的频率有多高? 嗯……

Dave Taylor 长期以来一直在 UNIX 和 Linux 系统上编写 shell 脚本。 他是 Learning Unix for Mac OS XWicked Cool Shell Scripts 的作者。 您可以在 Twitter 上找到他,账号为 @DaveTaylor,您也可以通过他的技术问答网站联系他:Ask Dave Taylor

加载 Disqus 评论