刚开始我是奔着抓API的,发现翻页的参数不好找,后来看到搜索出来的歌曲都存在当前url中,翻页只需更换浏览器中的url参数即可:https://www.xiami.com/list?page=1&query={“searchKey”:”张国荣”}&scene=search&type=song,那么为啥要费那么多事找接口呢?
但是后来发现歌曲的下载地址存在于API接口中,呢我们不得不寻找_s和_q的参数,也就是说我们上面的代码只能抓取张国荣的歌曲和信息,不能下载到本地,显然没有什么卵用,我们在观察一下,同时参考一篇文章:https://www.cnblogs.com/zhuchunyu/p/11496882.html,有兴趣的可以去看看。重点就是找到md5加密的内容是什么,最中在: https://g.alicdn.com/xiami-frontend/web-xiami-main-node/3.8.0/main/index.js js文件中发现了加密的字符串:
虾米音乐爬虫实战分析 批量下载虾米音乐到本地
很明显就可以看到_s的值是通过: cookie中的xm_sg_tk通过_切割取第一个值 + xmMain + 接口地址 + _ + _q ,知道了参数的来源那么我们就可以构造这样的参数放到url中去请求数据。鳄鱼君推荐使用MD5转换工具,便于测试参数是否正确。代码参考:
import requests,json,os
from hashlib import md5
class XiMimusic():
def __init__(self):
self.URL='http://www.xiami.com'
#请求头和cookies
self.headers={
'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding':'gzip, deflate, br',
'Accept-Language':'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'Connection':'keep-alive',
'Host':'www.xiami.com',
'TE':'Trailers',
'Upgrade-Insecure-Requests':'1',
'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0'
}
#维持会话,携带cookies
self.session=requests.Session()
self.session.get(url=self.URL,headers=self.headers)
# 每日音乐API接口推荐
self.APIDailySongs = "/api/recommend/getDailySongs"
# 排行榜音乐API接口
self.APIBillboardDetail = "/api/billboard/getBillboardDetail"
# 歌单API接口
self.APICollectionL = "/api/collect/getCollectDetail"
# # 歌曲详情信息 包含歌曲的下载地址
self.APISongDetails = "/api/song/getPlayInfo"
#获取对应的接口地址
def get_api_url(self, api):
return self.URL + api
# 获取xm_sg_tk的值,用于对数据加密的参数
def get_xm_sg_tk(self):
xm_sg_tk = self.session.cookies.get('xm_sg_tk')
return xm_sg_tk.split("_")[0]
# 获取加密字符串_s
def get_params_s(self,api,_q: str = ""):
xm_sg_tk=self.get_xm_sg_tk()
xxoo = xm_sg_tk + '_xmMain_' + api + '_' + _q
md_5 = md5(xxoo.encode()).hexdigest()
return md_5
def get_daily_songs(self,*args):
"""
获取每日推荐的30首歌曲 只需_s参数
:param args:
:return:
"""
url=self.get_api_url(self.APIDailySongs)
params={
"_s":self.get_params_s(self.APIDailySongs)
}
result = self.session.get(url=url, params=params,)
data = json.loads(result.content)
try:
if data['code']=="SUCCESS":
for i in data['result']['data']['songs']:
item={}
item['songname']=i['songName']
item['songid']=i['songId']
yield item
else:
return None
except Exception as e:
return None
# 获取虾米音乐的音乐排行榜
def get_billboard_song(self, billboard_id):
"""根据排行榜的id来下载不同的排行榜歌曲
:param billboard_id:
:return:
"""
url = self.get_api_url(self.APIBillboardDetail)
_q = '{\"billboardId\":\"%s\"}' % billboard_id
params = {
"_q": _q,
"_s": self.get_params_s(self.APIBillboardDetail, _q)
}
result = self.session.get(url=url, params=params)
data = json.loads(result.content)
try:
if data['code'] == "SUCCESS":
for i in data['result']['data']['billboard']['songs']:
item = {}
item['songname'] = i['songName']
item['songid'] = i['songId']
yield item
else:
return None
except Exception as e:
return None
def get_song_details(self,func):
"""
获取音乐的下载地址
:param func: 参数为一个函数
:return: 返回音乐的下载地址和音乐名字
"""
try:
for i in func:
url=self.get_api_url(self.APISongDetails)
_q = "{\"songIds\":[%s]}" % i['songid']
params = {
"_q": _q,
"_s": self.get_params_s(self.APISongDetails, _q)
}
result = self.session.get(url=url, params=params,)
data = json.loads(result.content)
yield data['result']['data']['songPlayInfos'][0]['playInfos'][1]['listenFile'],i['songname']
except Exception as e:
return None
def download_music(self,songurl,songname:str =""):
"""
下载音乐到本地
:param songurl:
:param songname:
:return:
"""
try :
file_path='{0}/{1}.{2}'.format('G:/XM音乐',songname,'m4a')
if not os.path.exists(file_path):
with open(file_path,'wb') as f:
f.write(requests.get(songurl).content)
except Exception as e:
return None
def user_choice(self):
music_list = ['美国Billboard单曲榜id:204', '韩国MNET音乐排行榜id:206', '英国UK单曲榜id:203', 'Beatport电音榜id:329', '新歌榜id:102','抖音热歌榜id:332', 'K歌榜id:306', '热点讨论榜id:331', '歌单收录榜id:305', '趴间热歌榜id:327', '影视原声榜id:324']
for k in music_list:
print(k)
data = input('请输入需要下载歌曲榜单的id(请不要乱输入否则程序会崩溃....):')
return data
def start(self):
id=self.user_choice()
try:
for songurl, songname in self.get_song_details(self.get_billboard_song(id)):
if songurl and songname:
print("正在下载歌曲:{0}".format(songname))
self.download_music(songurl,songname)
except Exception as e:
return None
# #下载每日30首歌曲到本地
# try:
#
# for songurl,songname in self.get_song_details(self.get_daily_songs()):
# if songurl and songname:
# print("正在下载歌曲:{0}".format(songname))
# self.download_music(songurl,songname)
# except Exception as e:
# return None
if __name__=='__main__':
xm=XiMimusic()
xm.start()
代码测试于2020-4-4日,在这里深切哀悼在抗击新冠肺炎疫情牺牲的烈士和逝世同胞们!!!!
虾米音乐使用了js对请求的参数进行了加密,请求需携带headers且cookies存在过期时间,如果处理不好动不动就会出现:www.xiami.com:443/api/search/searchSongs/tmd/punish?x5secdata。其中tmd格外显眼,这是一个滑动验证的url,你在尝试的时候可能会经常出现在你的控制台中。
代码中有很多内容没有完善,但是下载几百首歌曲是没有问题的,我们已经知道了参数如何构建,那么还有什么难度呢?多花费点时间罢了。
相比于虾米音乐,QQ音乐的参数非常好找,在响应的html源码中是可以找到的,我们也可以轻松构建url获取数据,详情参考:QQ音乐的抓取和下载