Sahi案例分享:音乐批量下载
本文将要向大家分享一个音乐批量下载的脚本。它使用了Shell脚本和Sahi脚本。该实例向大家展示了Sahi在除了Web UI自动化测试以外的一个实际应用。在阅读本文前,如果您还不知道Sahi是什么,建议您可以先阅读一下我的另一篇文章《使用Sahi测试Dojo应用》,也可以直接访问[Sahi的官方网站](http://sahi.co.in/w/)。另外,您最好对Shell编程以及Linux中的sed,grep以及awk等命令能有一定的概念。
偶然的机会,我发现了gmbox (http://code.google.com/p/gmbox/)。它是一款用Python编写的开源软件,放在Google Code上,可以提供音乐的批量下载。但是,用了不久,就发现它无法下载音乐了。在浏览了gmbox的Wiki之后,发现原因是这样的:针对每首歌曲,gmbox最终是得到并发送这样的一个URLhttp://www.google.cn/music/songstreaming?id=Sb085ad586f0447da&cad=localUser_player&cd&sig=87cd90dde819885241640b3d9cc7271a&output=xml并从返回的XML中得到mp3的下载链接、歌词以及专辑封面的下载链接的。这个URL中有一个参数叫sid,很显然它是一串经过加密的字符串。通过阅读gmbox的源码,不难发现它的生成规则。
flashplayer_key = "a3230bc2ef1939edabc39ddd03009439" sig = hashlib.md5(flashplayer_key + self.id).hexdigest()
问题出现在flashplayer_key上。谷歌每过一段时间就会修改这个flashplayer_key的值,于是导致gmbox相关的功能无法工作(比如试听音乐下载以及歌词和专辑封面下载)。
如果我们用Sahi来模拟用户搜索音乐、播放音乐的操作并使用某种机制记录下所有的获取音乐信息URL,然后再用wget或者curl之类的命令进行下载不就可以绕过这个flashplayer_key的问题了吗?这就是我的解决方案。
为了完成整个的过程,让我们一起看看有哪些关键步骤以及响应的结局方案。
最后,我用了wget。
想要具体了解实现的细节,请阅读“代码详解”部分。
在开始讲解代码之前,先让我们来看看如何运行这个脚本。
如果一切正常,在执行完以上的步骤之后,脚本会自动启动Sahi代理服务器。然后,弹出谷歌音乐的在线播放器页面,执行搜索。接着,会看到歌曲一首一首被“播放”。所谓“播放”,只是让浏览器发送获取歌曲的URL从而使Sahi记录下来。直到最后一首歌曲被“播放”完,Sahi代理服务器自动关闭。之后,wget开始下载mp3文件和lrc文件(如果有的话)。所有mp3以及歌词文件会被下载到以搜索关键字命名的目录下。为了能够在未下载完的情况下,下次仍能继续下载而无需启动Sahi重新获取URL,所有获取歌曲信息的URL被保存在当前目录下的songs.txt中。如果需要,你可以运行“~/googlemusic/download.sh songs.txt “阿黛尔(Adele)"”继续之前未完的下载。当中,第二个参数是下载目录,download会在下载前检查文件是否已经存在,如果存在就跳过。因此,你如果想完全重新下载所有歌曲,指定一个新的目录。
googlemusic.zip中包含了三个脚本文件。
googlemusic.sh的工作流程如下: 1. 检查环境变量SAHI_HOME是否设置。如果没有,退出程序。 2. 清理之前运行的残余文件(包括songs.txt,keyword文件以及Sahi的traffic log目录)。 3. 判断是否已经有Sahi的进程在运行,如果有,就Kill掉。 判断的方法是通过lsof检查运行在端口9999上进程(Sahi服务器默认的端口是9999)。结合grep和awk命令获取pid并kill掉该进程。 4. 启动Sahi服务器,并把进程pid保存到变量sahi_pid中。 获取sahi_pid的方法与上相同。 5. 执行googlemusic.sah脚本。
谷歌在线播放器的URL事实上是http://g.top100.cn/16667639/html/player.html#loaded ,而脚本使用的却是http://www.google.cn/music/top100/player_page,为什么?这是尝试的结果。第一次打开在线播放器页面的时候,会出现一个“服务条款”页面问你是否同意。在使用播放前默认的URL的时候,Sahi无法点击到“同意”按钮。通过Chrome 的Developer Tools,我发现该页面有很多iframe构成,于是开始尝试用http://www.google.cn/music/player,这是它最内层真正显示播放器的iframe URL。这次,Sahi成功地点击了“同意”按钮进入播放器界面。 

 6. Kill Sahi进程(PID记录在变量sahi_pid中)。 7. 从traffic log文件里提取音乐信息URL保存到songs.txt文件中。 这段代码结合了grep和awk两个命令并把结果导出到songs.txt文件中。 8)执行download.sh下载mp3及歌词文件。
下面这个URL是songs.txt中的一行。
http://www.google.cn/music/songstreaming?id=S82816ab0c2814785&cad=localUser_player&cd&sig=1ccf866dca1cdc1853fb921e01f0438a&output=xml
脚本中通过curl命令得到如下请求结果:
<results> <songStreaming> <id>S82816ab0c2814785 <songUrl> http://audio2.top100.cn/201205262149/63C7DB9ECEC3287F9F6DB0423C4F81F9/streaming1/Special_101259/M0101259012.mp3 </songUrl> <lyricsUrl> http://lyric.top100.cn/Special_101259/M0101259012.lrc </lyricsUrl> <albumThumbnailLink> http://lh6.googleusercontent.com/public/_AMNePcsm5yfz_WxM_iSmJUvYa0aIyP7W5bTih_z5XmDOGmfA7SpTh3gdPe2tDFqk2x5rGzr1pToVWmPpnjzucip7WCxoS35zqEQvFtprb-cUPk_e3Sd0fUXYFT0aXW7oqUR </albumThumbnailLink> <label>索尼音乐娱乐(中国) <labelHash>ca646574dfb918889fcc1ed02d933f6c <providerId>M0101259012 <artistId>A37ef8fc531bca276 <language>en <genre>rnb <genre>pop </songStreaming> </results>
可以看出,songUrl节点的值就是mp3的下载地址。我们还需要知道歌曲名和歌手名,把它们拼接成mp3文件名。所以又用curl命令请求了另一个URL"http://www.google.cn/music/song?id=$songId&output=xml", $songId正是上面XML内容中的id节点值(S82816ab0c2814785)。返回的XML内容如下。artist 以及name节点正是歌手名和歌曲名。
<results> <songList> <!-- freemusic/song/result/S82816ab0c2814785 --> <song> <id>S82816ab0c2814785 <name>Who Is It <artist>迈克尔 杰克逊(Michael Jackson) <artistId>A37ef8fc531bca276 <album>King Of Pop CD1 <duration>241.0 <canBeDownloaded>true <hasFullLyrics>true <canBeStreamed>true <albumId>B8cacd47437481c83 <hasSimilarSongs>true <hasRecommendation>false </song> </songList> <estimatedResultCount>1 </results>
readXmlAttr() 函数用来从指定的XML文档中读取指定的节点的值。方法是用sed命令把“</”全部替换成“<”后再用“awk -F”把指定的节点标签作为分割符对文档进行分割并取出第2个元素值。
googlemusic.sah脚本的运行过程如下:
1 检查有没有文本是“同意”的div,如果有,就点击,没有,便跳过(“服务条款”被“同意”过一次之后,就不会再出现,直到你清楚浏览器的Cookie)。
2 如果之前你有在播放器里搜索过歌曲,当再次打开播放器时,哪些歌曲仍会显示在歌曲列表中。因此,需要先清除所有已有的歌曲。每首歌曲都会在一个class为"artist-cell"的td里显示歌手名称,统计这类td的数目就可以知道有多少首歌曲当前显示在列表里。如果$count的值大于0就说明有歌曲显示。接着就是点击“全选”checkbox。这里用的是near函数,还有一种方法是"checkbox(count(“checkbox”,‘’)-1)“,也就是得到最后一个checkbox,这个checkbox就是“全选”。最后点“删除”按钮并在跳出的确认对话框里点“Yes”(既然是简体中文网页,谷歌事实上应该显示“是”)。
var $count=_count('_cell','artist-cell') if($count>0){ _click(_checkbox(0,_near(_span("全选")))) _click(_div("删除")) _click(_submit("Yes")) }
3 搜索歌曲。.sah文件在被调用时无法传参数,所以keyword是通过一个文本文件中转的。
var $keyword=_readFile("/tmp/googlemusic_keyword.txt")
谷歌音乐搜索结果的显示是以一种增量的方式进行的,默认它会显示20首歌曲,然后随着你向下拖动,它会显示更多,直到最后出现“已经到达最后一条搜索结果”。所以Sahi脚本也必须模拟这种“滚动”操作来显示出所有结果。结束的标志就是看页面上是否出现了“_div(“已经到达最后一条搜索结果”)”这个元素。通过重新设定歌曲列表div的scrollTop属性可以实现“滚动”操作。根据Sahi脚本的编写要求,这类操作必须放在browser标签中 – 这就是scrollOnce函数。
var $done=false; while(!$done){ _set($test,scrollOnce()); $done=_isVisible(_div("已经到达最后一条搜索结果")); } <browser> function scrollOnce(){ var $list=_div("list-content"); $list.scrollTop=$list.scrollHeight; } </browser>
4 “播放”所有歌曲。前面已经讲过,所谓“播放”只是为了发送音乐信息URL的请求以便Sahi把URL记录下来。首先,通过计算class为artist-cell的td的数目得到歌曲总数,然后对“下一首”按钮(id为“:4”的div)进行相应次数的点击。
var $count=_count('_cell','artist-cell') _log($count,'custom1') while($count>0){ _click(_div(':4')) $count=$count-1 }
整个脚本的开发是一个不断尝试和探索的过程。最初,部分的代码是用Python写的,但这样无形中增加了额外的依赖性,于是我把Python实现的逻辑用Shell进行了重写。用Sahi脚本下载谷歌音乐显然不是最好的解决方案,因为它存在一些用户体验的问题。第一,有浏览器窗口弹出;第二,当歌曲数量较多时,逐一地点击歌曲生成歌曲URL并记录的方式通常会花费较长时间(与当时的网络速度也存在一定关系)。关于第一个问题,通过Xvfb可以不弹出浏览器窗口(http://sahi.co.in/w/configuring-sahi-with-xvbf)。关于第二个问题,我曾经试图直接在DOM 树上直接拿到歌曲的URL,但最终没有找到。有兴趣的读者可以研究一下。无论如何,对于学习Sahi来讲,这个脚本仍是一个很好的实践,而且,它说明了Web UI自动化测试不是Sahi唯一的用武之地。
另外,这只是一个用来学习交流的脚本,没有经过充分地测试。所以,如果在运行中出现问题,敬请谅解。
免责声明:本文涉及的代码仅供学习Sahi使用,请不要用来进行制作盗版音乐等非法行为,如果发生类似情况,InfoQ中文站与作者均不负责。
沈锐,目前从事Web UI功能测试工作,对Web UI自动化测试有着浓厚的兴趣。