Sahi案例分享:音乐批量下载

Sahi案例分享:音乐批量下载

本文将要向大家分享一个音乐批量下载的脚本。它使用了Shell脚本和Sahi脚本。该实例向大家展示了Sahi在除了Web UI自动化测试以外的一个实际应用。在阅读本文前,如果您还不知道Sahi是什么,建议您可以先阅读一下我的另一篇文章《使用Sahi测试Dojo应用》,也可以直接访问[Sahi的官方网站](http://sahi.co.in/w/)。另外,您最好对Shell编程以及Linux中的sed,grep以及awk等命令能有一定的概念。

1 背景介绍

偶然的机会,我发现了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相关的功能无法工作(比如试听音乐下载以及歌词和专辑封面下载)。

2 核心原理

如果我们用Sahi来模拟用户搜索音乐、播放音乐的操作并使用某种机制记录下所有的获取音乐信息URL,然后再用wget或者curl之类的命令进行下载不就可以绕过这个flashplayer_key的问题了吗?这就是我的解决方案。

为了完成整个的过程,让我们一起看看有哪些关键步骤以及响应的结局方案。

  1. 自动启动Sahi(如果它没有启动的话)。 Sahi默认启动在端口9999,通过lsof就能够知道是否Sahi已启动。如果启动的话,就Kill掉它,然后重新启动Sahi。
  2. 通过Sahi脚本在浏览器中模拟用户搜索播放音乐的操作。 这当中碰到的一个问题是如何将搜索关键字作为参数传递个Sahi的脚本。最后,我是用文件的方式传递的。
  3. 记录下所有获取音乐信息的URL到一个文件中。 起初,我准备用dsniff记录URL,但很快我意识到这不是一个好办法,因为用户不得不额外安装dsniff。 后来,我发现Sahi通过启用一个调试选项,能够记录所有HTTP(S)请求响应通过Sahi代理服务器时被修改前和修改后的内容(traffic log)。这仅需要用户在配置文件中添加一个设置。因此,我选用了这种方式。
  4. 通过wget或者curl从文件中读取URL并下载。

最后,我用了wget。

想要具体了解实现的细节,请阅读“代码详解”部分。

3 使用方法

在开始讲解代码之前,先让我们来看看如何运行这个脚本。

  1. 下载及安装Sahi(http://sahi.co.in/w/using-sahi)。
  2. 把googlemusic.zip解压缩到你喜欢任意目录下。
  3. 设置环境变量SAHI_HOME指向你的SAHI安装目录。
  4. 在$SAHI_HOME/userdata/config/userdata.properties中添加如下代码行,用来启用Sahi调试的traffic log。 debug.traffic.log.unmodified=true
  5. 在$SAHI_HOME/userdata/scripts目录下建立一个软链接到解压缩的目录。假设你把压缩包解压到你的home目录下,就运行“ln sf ~/googlemusic/”。另一种方法是直接把压缩包解压到$SAHI_HOME/userdata/scripts下,这样,就省去创建软链接的步骤。
  6. 脚本中默认指定的browser名称是“chrome”。如果你没有安装Chrome或者其name属性不是“chrome”的话,需要修改googlemusic.sh中的browser变量为相应的值。
  7. 进入你想要保存音乐文件的目录,运行“~/googlemusic/googlemusic.sh “阿黛尔(Adele)"”(假设脚本解压缩在你的home目录下)。如果需要下载某位歌手的全部歌曲,为了确保最后下载歌曲的准确性,最好先到谷歌音乐上去尝试着搜索一下。例如,你如果在http://www.google.cn/music/homepage中输入“阿黛尔(Adele)”,就会发现搜索结果只包含阿黛尔的歌曲。但你若输入“Adele”,结果就相当混乱。所以,应该使用“阿黛尔(Adele)”。

如果一切正常,在执行完以上的步骤之后,脚本会自动启动Sahi代理服务器。然后,弹出谷歌音乐的在线播放器页面,执行搜索。接着,会看到歌曲一首一首被“播放”。所谓“播放”,只是让浏览器发送获取歌曲的URL从而使Sahi记录下来。直到最后一首歌曲被“播放”完,Sahi代理服务器自动关闭。之后,wget开始下载mp3文件和lrc文件(如果有的话)。所有mp3以及歌词文件会被下载到以搜索关键字命名的目录下。为了能够在未下载完的情况下,下次仍能继续下载而无需启动Sahi重新获取URL,所有获取歌曲信息的URL被保存在当前目录下的songs.txt中。如果需要,你可以运行“~/googlemusic/download.sh songs.txt “阿黛尔(Adele)"”继续之前未完的下载。当中,第二个参数是下载目录,download会在下载前检查文件是否已经存在,如果存在就跳过。因此,你如果想完全重新下载所有歌曲,指定一个新的目录。

4 代码详解

googlemusic.zip中包含了三个脚本文件。

  1. googlemusic.sh: 负责自动启动关闭Sahi代理服务器;运行googlemusic.sah之后解析traffic log文件并将结果保存到songs.txt文件;最后调用download.sh执行下载。
  2. download.sh: 执行mp3与歌词文件下载。
  3. googlemusic.sah: 模拟用户在谷歌音乐在线播放器中的搜索播放操作。

googlemusic.sh代码讲解

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成功地点击了“同意”按钮进入播放器界面。 

Sahi案例分享:音乐批量下载_第1张图片  6. Kill Sahi进程(PID记录在变量sahi_pid中)。 7. 从traffic log文件里提取音乐信息URL保存到songs.txt文件中。 这段代码结合了grep和awk两个命令并把结果导出到songs.txt文件中。 8)执行download.sh下载mp3及歌词文件。

download.sh代码讲解

下面这个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代码讲解

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中文站与作者均不负责。

代码下载

Sahi案例分享:音乐批量下载_第2张图片

关于作者

沈锐,目前从事Web UI功能测试工作,对Web UI自动化测试有着浓厚的兴趣。

 

 

你可能感兴趣的:(Sahi案例分享:音乐批量下载)