思路和想法
虾米音乐有个网页播放器,这回我抓的就是在登陆的时候,这个播放器上所有的歌单里的所有歌曲。如图:
xiami-界面.jpg我之前没用过虾米,所以注册了个新账号,新建了三个歌单,每个歌单(左侧红框)大概3-5首歌,我的目标就是将这些歌曲都下载到电脑里来。
当我们点击这个播放器页面里的链接时,页面的地址不会发生变化,显然这又是个异步加载,但是这次找XHR要注意一点细节:
xiami-xhr分析.jpg经过一定的分析,左侧第一个红框里的XHR包含了所有的歌单以及每个歌单的list_id,之后我们可以用这个id来构建一条类似于蓝框里的XHR,用来获取某个歌单里所有歌曲的location,location就是该歌曲加密后的下载URL。这里有几个细节:
- 如果你点击播放某个歌曲,会收到如第二个红框那样的XHR,它里面也包含了该歌曲的location,这种方法也是可行的,只是每次只能获取一首歌曲的location,用蓝框里那种XHR可以一次获得整个歌单里所有歌曲的location。
- 在构建蓝框里那条XHR时,要注意请求头的字段,不仅要包含cookies,也要包含referer,后者很重要否则服务器会返回404错误,至于referer的内容,里面涉及到一个ID,但据我测试,任何一首歌的songID都可以。
- 请求的两个参数
_ksTS
和callback
,前者是由当前的时间(time.time())加一个数字组成,后者是jsonp加一个数字,后一个数字比前一个数字大1。这里我没具体测出数字的范围,取一个1000以内的应该没问题。callback的值会在之后返回的json文件里体现出来,说实话,个人觉得有点画蛇添足地味道。
现在我们有了每个歌单的id,并且根据这个id,获取了歌单里所有歌曲的信息,这里我只取了歌曲名、歌手和位置。接着你会发现这个位置是加密过的,所以这时应该去js里找这个加密函数。
找js文件其实也是有技巧的,一般来说,像这种功能肯定是自定义的,所以要去自定义的js文件里找,然后搜索一些如location啊,url之类的关键字,这次运气好,很快就找了。加密函数在https://g.alicdn.com/music/music-player/1.1.6/??common/global-min.js,pages/index/page/init-min.js
里,格式化后第1087-1104行,如图。
这里我暂且不谈这个函数是怎么运作的。先说点别的,我在网上看到了一些关于下载虾米音乐的资料,几乎全部在获取了加密后的location之后都是开始自己琢磨如何解密,个人以为这其实是不可取的,像js这种端脚本语言,如果要解密肯定是有迹可循的,而且肯定在某个js文件里,区别只是写代码的人把它隐藏到哪种程度。所以,如果遇到类似的情况,应该去找js,而不是先自己琢磨。
这里还有一个技巧就是:其实我们根本不需要理解这个函数是怎么运作的,也不需要将它改变成python版本,python有一个名为PyExecJS的库可以直接运行js代码。很好很强大。
代码
代码很简单,首先获取所有歌单的list_id,然后根据每个list_id获取其中的所有歌曲的信息,然后解密location的值,最后在将歌曲下载下来。最后下载下来的效果如图:
xiami-下载.jpgjs.py
jsstr = '''
function getLocation(a) {
if (-1 !== a.indexOf("http://"))
return a;
for (var b = Number(a.charAt(0)), c = a.substring(1), d = Math.floor(c.length / b), e = c.length % b, f = new Array, g = 0; e > g; g++)
void 0 == f[g] && (f[g] = ""),
f[g] = c.substr((d + 1) * g, d + 1);
for (g = e; b > g; g++)
f[g] = c.substr(d * (g - e) + (d + 1) * e, d);
var h = "";
for (g = 0; g < f[0].length; g++)
for (var i = 0; i < f.length; i++)
h += f[i].charAt(g);
h = unescape(h);
var j = "";
for (g = 0; g < h.length; g++)
j += "^" == h.charAt(g) ? "0" : h.charAt(g);
return j = j.replace("+", " ")
}
'''
xiami.py
configure.py请参考拙作:爬取糗事百科的内容和图片并展示。
import requests
import re
import json
import os
import time
from random import choice
import random
import execjs
from js import jsstr
import Configure
url = "https://www.xiami.com/playercollect/list"
header = {'user-agent': choice(Configure.FakeUserAgents)}
cookies = {}
cookiestr = '''gid=152425845220935;
_unsign_token=1ce1fe55087ea1f21db47780701aa248;
cna=BOBeExJaZ2MCAUWMrkuEudTQ;
UM_distinctid=162e4e236948cf-0773ed2704ac51-3b60490d-1fa400-162e4e236957a6;
join_from=0zqfTI9Kv2Ew3f7BEdw;
_xiamitoken=9934bc4572854b88e3b35017a976a3c7;
user_from=2;
PHPSESSID=abdb66f05f100adc171436cdc0db744a;
s_uid=2523264732;
access_token=2.00Q73lkC81ERME01ffcdd5d907MPnY;
__XIAMI_SESSID=9dd80406b67dc4af1df541f343f5ecdf;
xmgid=6539bf5e-7321-44d3-ab3d-0d8ff66d0f50;
connect_sina=76633;
expires_in=2652725;
CNZZDATA2629111=cnzz_eid%3D1724500643-1524255732-https%253A%252F%252Fwww.google.com%252F%26ntime%3D1525313285;
_umdata=535523100CBE37C33075D564A95D152BC5B96D713321CA2CED3D987F95DDB172352CD465FA09B190CD43AD3E795C914C6D829DD7915A193EB257667C30CCA2E2;
member_auth=hWrNTo4duj9lgqPAT4FlIiIW4OLdHDLSwo0C3rIl5AMhJ9wBa4TxlauSRAlB3SiVqVEmwDAiBbv9xkT9%2FlYdtts;
user=362958619%22%E4%BD%8E%E8%B0%83%E9%9A%90%E5%BF%8DOB%22%220%220%22do%220%220%220%22975ef99c85%221525314365;
t_sign_auth=0;
CNZZDATA921634=cnzz_eid%3D1878118879-1524256440-https%253A%252F%252Fwww.google.com%252F%26ntime%3D1525314514;
form_timestamp=1525318171;
XMPLAYER_url=/song/playlist-default;
XMPLAYER_addSongsToggler=0;
__guestplay=MTc3NDMyMTIxNSw2OzE3OTYwMzI0MTMsMjA7MTgwMzAwMjM4MSw0OzE3OTYwMzI0MTIsMg%3D%3D;
XMPLAYER_isOpen=0;
isg=BEREMy-7wt1ZiXahWEFx4c_eFcI8XWhO-TQFnl7k1I_SieVThG1fVPBozTdRyqAf
'''
for cookie in cookiestr.split(';'):
name,value=cookie.strip().split('=',1)
cookies[name]=value
def getRandomParams():
a = int(time.time()*1000)
b = random.randint(200,1000)
return {'_ksTS':'{0:d}_{1:d}'.format(a,b), 'callback':'jsonp{0:d}'.format(b+1)}
def getPlaylist():
playlist = []
try:
response = requests.get(url, headers=header, params=getRandomParams(), cookies=cookies)
content = None
if response.status_code == requests.codes.ok:
content = response.text
except Exception as e:
print (e)
pattern = re.compile('"list_id":(\d+),', re.S)
data = pattern.findall(content)
return data
def getSongLocation(list_id):
url2 = "https://www.xiami.com/song/playlist/id/{0:s}/type/3/cat/json".format(list_id)
header = {}
header['user-agent'] = choice(Configure.FakeUserAgents)
header['referer'] = 'https://www.xiami.com/play?ids=/song/playlist/id/1796032423/object_name/default/object_id/0'
payload = {
'_ksTS':'{0:d}_428'.format(int(time.time()*1000)),
'callback':'jsonp_429'
}
try:
response = requests.get(url2, headers=header, params=payload, cookies=cookies)
content = None
if response.status_code == requests.codes.ok:
content = response.text
except Exception as e:
print (e)
data = json.loads(content[11:][:-1])
tracklists = data.get('data').get('trackList')
ctx = execjs.compile(jsstr)
res = []
for track in tracklists:
tmp = {}
tmp['songName'] = track.get('songName')
tmp['singers'] = track.get('singers')
tmp['location'] = "http:"+ ctx.call('getLocation', track.get('location'))
res.append(tmp)
return res
def DownloadSong(SongURLs):
if not os.path.exists("Download"):
os.makedirs("Download")
for songurl in SongURLs:
req = requests.get(songurl.get('location'))
filename = "{0:s}-{1:s}.mp3".format(songurl.get('songName'), songurl.get('singers'))
with open("Download/"+filename, 'wb') as file:
file.write(req.content)
print ("Download {0:s} Successfully.".format(filename))
if __name__ == '__main__':
ids = getPlaylist()
for list_id in ids:
res = getSongLocation(list_id)
DownloadSong(res)
网友评论