分析歌词

作者:Dave Taylor

几天前,我正在阅读关于披头士乐队的历史,偶然发现一个有趣的事实。作者说,披头士乐队在他们的歌曲中使用了超过 160 次“love”这个词。起初我认为“酷”,但当我越想这件事,我就越怀疑这个数字。事实上,我怀疑“love”这个词出现的次数远不止 160 次。

这引出了一个问题:你实际上是如何弄清楚这样的事情的?当然,答案是用 shell 脚本!那么让我们开始吧,好吗?

按艺术家下载歌词

第一个挑战,也是最重要的工作,是弄清楚在哪里下载艺术家、表演者或乐队的每首歌的歌词。网上有很多档案,但它们完整吗?

一个来源是 MLDb,音乐歌词数据库(模仿互联网电影数据库而建立,有人推测)。一个简单的测试是:该网站列出了披头士乐队多少首歌曲?

从网络浏览器中的交互式会话向后推,搜索艺术家“the beatles”会产生八页匹配结果,每页 30 个匹配项。那是 240 首歌曲。维基百科说乐队有 237 首原创作品,而 BeatlesBible.com 显示有 302 首原创歌曲。令人困惑!

当然,披头士乐队录制的一些歌曲没有歌词。例如,在Magical Mystery Tour专辑中,有一首名为“Flying”的曲目。然而,鉴于 Paul McCartney 和 John Lennon 是如此出色的作词人,绝大多数录制的歌曲至少有一些歌词——甚至包括“The End”。

那么让我们使用 MLDb,并相信它的 240 首歌曲对于这项任务来说已经足够接近了。现在的挑战是获取所有歌曲的列表,然后下载每首匹配歌曲的歌词。

幸运的是,这可以通过逆向工程搜索 URL 来完成。对“the beatles”进行精确短语艺术家搜索的第二页结果,按评分排序,会产生这个特定的 URL:http://www.mldb.org/search?mq=the+beatles&mm=2&si=1&ob=2&from=30

您可以实验性地验证这会产生第二页结果,但是,嘿,让我们继续下去!由于第二页有“from=30”,您可以得出结论,每页有 30 个条目(如前所述),并且 from=60 获取第三页,from=90 获取第四页,依此类推。

每页都可以使用 GETcurl 以 HTML 形式下载,我更喜欢使用后者——它更复杂并且有大量选项。快速浏览一下,发现“Yellow Submarine”出现在第一页,所以这是一个快速测试,url 设置为上面显示的值


$ curl -s "$url" | grep "Yellow Submarine"
<table id="thelist"
cellspacing="0"><tr><th>Artist(s)</th><th>Song</th>
<th width="20">Rating</th></tr><tr class="h"><td
class="fa"><a
href='artist-39-the-beatles.html'>The Beatles</a></td><td
class="ft"><a
href="song-32476-i-am-the-walrus.html">I Am The Walrus</a></td><td
align="right">6</td></tr><tr class="n"><td class="fa"><a
href='artist-39-the-beatles.html'>The Beatles</a></td><td
class="ft"><a
href="song-32461-yellow-submarine.html">Yellow Submarine</a></td><td
align="right">6</td></tr><tr class="h"><td class="fa"><a
href='artist-39-the-beatles.html'>The Beatles</a></td><td
class="ft"><a
href="song-32585-day-tripper.h...

事实证明,整个歌词表是 HTML 的单行。这很糟糕,但很容易管理。请注意,上面指向单首歌曲的 href 链接格式如下


<a href="song-32461-yellow-submarine.html">Yellow Submarine</a>

这是我将在原始 HTML 中寻找的模式,注意指向艺术家的链接使用单引号,但指向歌词的链接使用双引号


curl -s "$url" | grep "Yellow Submarine" | sed 's/</\
</g' | grep 'href="song-'

请注意上面的 sed 模式。我将每个 < 替换为回车符后跟 <,因此净效果是我整齐地展开 HTML 源代码,然后可以使用 grep 来隔离匹配的行并排除其他所有内容。

仅这一行就得到了以下结果


<a href="song-32476-i-am-the-walrus.html">I Am The Walrus
<a href="song-32461-yellow-submarine.html">Yellow Submarine
<a href="song-32585-day-tripper.html">Day Tripper
<a href="song-32520-come-together.html">Come Together
. . . lots of lines removed for clarity . . .
<a href="song-32395-a-hard-day-s-night.html">A Hard Day's Night
<a href="song-32571-i-want-to-hold-your-hand.html">I Want To Hold
 Your Hand
<a href="song-32527-here-comes-the-sun.html">Here Comes The Sun
<a href="song-32609-i-saw-her-standing-there.html">I Saw Her Standing
 There

不错。现在如何将每个结果转换为 curl 页面查询?嗯,等等!让我们首先弄清楚如何获取每首歌曲的完整列表——也就是说,如何从一页到另一页。为此,已经显示的 URL 中有线索:每后续页面的 from=XX。

另一个快速测试显示,如果您指定一个超出列出的最后一首歌曲的 URL 会发生什么:不返回任何匹配项。这很容易处理,因为在这种情况下 wc -l 将返回零。

将各个部分组合在一起,这是一个循环,它将尽可能多地获取匹配项,直到结果为零


url="http://www.mldb.org/search?mq=the+beatles&mm=2&si=1&ob=2"
output="lyrics-page." # you can put these in /tmp
start=0   # increment by 30, first page starts at zero
  max=600 # more than 20 pages of matches = artificial stop

while [ $start -lt $max ]
do
  curl -s "$url&from=$start" | sed 's/</\
</g' | grep 'href="song-' > $output$start
  if [ $(wc -l < $output$start) -eq 0 ] ; then
    # zero results page. let's stop, but let's remove it first
    echo "hit a zero results page with start = $start"
    rm "$output$start"
    break
  fi
  start=$(( $start + 30 ))      # increment by 30
done

稍后我将解释代码中发生的事情,但让我们先看看它做了什么,然后使用 ls 调用来仔细检查它是否创建了非零输出文件


$ sh getsongs.sh
hit a zero results page with start = 240
$ ls -s lyrics-page*
8 lyrics-page.0      8 lyrics-page.180    8 lyrics-page.60
8 lyrics-page.120    8 lyrics-page.210    8 lyrics-page.90
8 lyrics-page.150    8 lyrics-page.30

完美。我预计有八页歌曲,脚本也产生了八页。每个页面都与前面列出的输出具有相同的格式,因此现在的问题是将 href= 格式转换为调用 curl 以获取该特定歌词页面的调用。但是,由于我的空间已经不够用了,我将把脚本的这一部分推迟到我的下一篇文章中。

同时,请注意 start 如何通过 $(( )) 符号进行计算递增 30(您可以使用 expr,但留在 shell 中而不为数学运算生成子 shell 会更快)。此外,识别空输出文件的测试对您来说应该很容易理解


if [ $(wc -l < $output$start) -eq 0 ]

但是,这里有一个细微之处需要注意:$( ) 符号为您提供了一个类似于使用反引号的子 shell,而 $(( )) 符号允许您在 Bash shell 本身中进行基本的计算。

我将在我的下一篇文章中扩展所有这些内容。下次见!

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

加载 Disqus 评论