分析歌词
几天前,我正在阅读关于披头士乐队的历史,偶然发现一个有趣的事实。作者说,披头士乐队在他们的歌曲中使用了超过 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 获取第四页,依此类推。
每页都可以使用 GET
或 curl
以 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 本身中进行基本的计算。
我将在我的下一篇文章中扩展所有这些内容。下次见!