购买了一年的QQ音乐绿钻豪华版,还有几天就到期了,虽然平时听音乐比较少,但是还比较喜欢听歌曲的。计划会员到期前下载一些音乐文件,继续针对QQ音乐网站源码分析和歌曲下载链接的进行研究。
平时通过APP和软件播放歌曲也是趋势,所以在QQ音乐Web网站显示的歌曲数量有限。但是还是可以下载一些歌曲。通过分析和思考后,决定把QQ音乐网站所有歌星和所有音乐榜单的歌曲进行下载,其中每个歌星最多10首热门歌曲,每个榜单也就是近期热门Top20的歌曲。如果歌曲有MV文件也同步下载。所有爬虫代码已上传网盘,可以关注公众号【站在前沿】,回复qqmusic,获取网盘下载链接。有其它需求也可以在微信公众号留言。
下载技术使用Python爬虫和多线程技术,在分析页面源码的过程中,主要有三分之二的时间在解决sign参数加密的问题,而且要通过执行js脚本的方式进行解决,如果不能下载,重新拷贝新js脚本程序,QQ网站js脚本有可能已更新。QQ音乐网站资料使用了Webpack打包技术,也学习了webpack逆向分析技术来解决。先看一下音乐下载程序的运行效果,如下图
一、网站代码分析截图
歌手页面默认显示第一页80个歌星,滚动鼠标可以加载第二页80个和更多页。
sign加密分析,需要抓取和分析js代码
获取歌星热门歌曲列表,主要获取歌曲ID和歌曲名称
下载歌曲页面代码分析,也需要使用加密函数返回sign才可以下载
二、部分核心代码
爬虫核心代码
# 获取明星人员列表,根据音乐类型
def getStars(genreID, genreName):
starList = []
url = 'https://y.qq.com/n/ryqq/singer_list?index=-100&genre=' + str(genreID) + '&sex=-100&area=-100'
logging.info('-------------------艺术类型 ' + genreName + '-------------------')
res = until.getResText(url, headers)
# print(res)
stars = re.findall('<a href="/n/ryqq/singer/(.*?)">(.*?)</a>', res)
for starid, star in stars:
starList.append((starid, star))
stars = re.findall('<a class="singer_list_txt__link js_singer" title=".*?" href="/n/ryqq/singer/(.*?)">(.*?)</a>',
res)
for starid, star in stars:
starList.append((starid, star))
urlMusic = 'https://u.y.qq.com/cgi-bin/musics.fcg'
index = 2
while True:
logging.info('计划获取第' + str(index) + '页明星人员姓名')
param = {
'_': time.time() * 1000,
'sign': 'zzb32ecc024u4twyf1jekdcip0smo3psq506afc8e',
'data': ''
}
param[
'data'] = '{"comm":{"cv":4747474,"ct":24,"format":"json","inCharset":"utf-8","outCharset":"utf-8","notice":0,"platform":"yqq.json","needNewCode":1,"uin":你的QQ号,"g_tk_new_20200303":579926986,"g_tk":579926986,"mesh_devops":"DevopsBase"},"req_1":{"module":"music.musichallSinger.SingerList","method":"GetSingerListIndex","param":{"area":-100,"sex":-100,"genre":' + str(
genreID) + ',"index":-100,"sin":' + str(80 * (index - 1)) + ',"cur_page":' + str(index) + '}}}'
# print(param['data'])
param['sign'] = getSign(param['data'])
# print(param['sign'])
jsonSingers = until.getResJson3(urlMusic, headers, param)
# print(jsonSingers)
if jsonSingers['req_1']['code'] != 0:
logging.info('网页请求返回错误,请排查js脚本或加密相关代码,补充环镜信息继续尝试')
break
for singer in jsonSingers['req_1']['data']['singerlist']:
starList.append((singer['singer_mid'], singer['singer_name']))
if len(jsonSingers['req_1']['data']['singerlist']) < 80:
logging.info('已请求到最后一页,完成当前条件下所有明星人员获取')
break
index = index + 1
return starList
# 获取一个明星的歌曲列表
def getStarSongList(url, savePath):
res = until.getResText(url, headers)
# print(res)
jsonStr = re.findall('window.__INITIAL_DATA__ =(.*?)</script>', res, re.S)[0]
# print(jsonStr.replace('undefined','"undefined"'))
jsonData = json.loads(jsonStr.replace('undefined', '"undefined"'))
logging.info('开始下载' + jsonData['singerDetail']['basic_info']['name'] + '的热门歌曲')
for song in jsonData['songList']:
# print(song['id'],song['mid'],song['mv']['id'],song['mv']['vid'])
singers = []
for singer in song['singer']:
singers.append(singer['name'])
songName = song['name'] + '-' + ','.join(singers)
downMp3(song['mid'], str(song['id']), savePath, songName[0:40])
if str(song['mv']['vid']) != '':
downMV(song['mv']['vid'], savePath, songName[0:40])
# 获取音乐榜单列表
def getTopList(url):
topList = []
res = until.getResText(url, headers)
resText = re.findall('window.__INITIAL_DATA__ =(.*?)</script>', res)[0]
# print(resText)
jsonData = json.loads(resText.replace('undefined', '"undefined"'))
# print(jsonData)
for topNavData in jsonData['topNavData']:
for toplist in topNavData['toplist']:
# print(topNavData['groupName'],toplist['topId'],toplist['title'])
topList.append((toplist['topId'], topNavData['groupName'] + '-' + toplist['title']))
return topList
# 获取当前音乐榜单的歌曲列表
def getTopMusic(topID, savePath):
url = 'https://y.qq.com/n/ryqq/toplist/' + str(topID)
res = until.getResText(url, headers)
resText = re.findall('window.__INITIAL_DATA__ =(.*?)</script>', res)[0]
jsonData = json.loads(resText.replace('undefined', '"undefined"'))
# print(jsonData)
logging.info('开始下载音乐榜单' + savePath.rsplit('\\', 1)[1] + '的热门歌曲')
for songInfo in jsonData['songInfoList']:
singers = []
for singer in songInfo['singer']:
singers.append(singer['name'])
songName = songInfo['name'] + '-' + ','.join(singers)
# print(songInfo['mid'], songInfo['id'], songName)
downMp3(songInfo['mid'], str(songInfo['id']), savePath, songName[0:40])
if str(songInfo['mv']['vid']) != '':
downMV(songInfo['mv']['vid'], savePath, songName[0:40])
# 下载歌曲
def downMp3(songMID, songID, savePath, musicName):
durl = 'https://u.y.qq.com/cgi-bin/musics.fcg'
param = {
'_': time.time() * 1000,
'sign': 'zzb6d35ac94xigmigc7koyntjy2hzqsg48c02666',
'data': ''
}
param[
'data'] = '{"comm":{"cv":4747474,"ct":24,"format":"json","inCharset":"utf-8","outCharset":"utf-8","notice":0,"platform":"yqq.json","needNewCode":1,"uin":你的QQ号,"g_tk_new_20200303":1218543479,"g_tk":1218543479},"req_1":{"module":"vkey.GetVkeyServer","method":"CgiGetVkey","param":{"guid":"5591983328","songmid":["' + songMID + '"],"songtype":[0],"uin":"你的QQ号","loginflag":1,"platform":"20"}},"req_2":{"module":"music.musicasset.SongFavRead","method":"IsSongFanByMid","param":{"v_songMid":["' + songMID + '"]}},"req_3":{"module":"music.musichallSong.PlayLyricInfo","method":"GetPlayLyricInfo","param":{"songMID":"' + songMID + '","songID":' + songID + '}},"req_4":{"method":"GetCommentCount","module":"music.globalComment.GlobalCommentRead","param":{"request_list":[{"biz_type":1,"biz_id":"' + songID + '","biz_sub_type":0}]}},"req_5":{"module":"music.musichallAlbum.AlbumInfoServer","method":"GetAlbumDetail","param":{"albumMid":"0033R2xQ2I0Uyf"}},"req_6":{"module":"vkey.GetVkeyServer","method":"CgiGetVkey","param":{"guid":"871930674","songmid":["' + songMID + '"],"songtype":[0],"uin":"你的QQ号","loginflag":1,"platform":"20"}},"req_7":{"module":"music.trackInfo.UniformRuleCtrl","method":"CgiGetTrackInfo","param":{"ids":[' + songID + '],"types":[0]}}}'
param['sign'] = getSign(param['data'])
res = until.getResJson3(durl, headers, param)
# print(res)
purl = 'https://dl.stream.qqmusic.qq.com/' + res['req_1']['data']['midurlinfo'][0]['purl']
# print(musicName, purl)
until.downBinFile(purl, headers, savePath, musicName + '.' + purl.split('?')[0].rsplit('.', 1)[1])
sign加密js脚本代码,2024年2月份最新代码
var window =global;
document = {};
// 在浏览器控制台输入navigator和location,拷贝和完善下列信息
navigator = {
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
};
location = {
"ancestorOrigins": {},
"href": "https://y.qq.com/n/ryqq/singer_list?index=-100&genre=7&sex=-100&area=-100",
"origin": "https://y.qq.com",
"protocol": "https:",
"host": "y.qq.com",
"hostname": "y.qq.com",
"port": "",
"pathname": "/n/ryqq/singer_list",
"search": "?index=-100&genre=7&sex=-100&area=-100",
"hash": ""
}
var qqloader;
require("./qqsignfun")
!function(e) {
function t(t) {
for (var r, n, c = t[0], d = t[1], i = t[2], l = 0, u = []; l < c.length; l++)
n = c[l],
Object.prototype.hasOwnProperty.call(o, n) && o[n] && u.push(o[n][0]),
o[n] = 0;
for (r in d)
Object.prototype.hasOwnProperty.call(d, r) && (e[r] = d[r]);
for (b && b(t); u.length; )
u.shift()();
return f.push.apply(f, i || []),
a()
}
function a() {
for (var e, t = 0; t < f.length; t++) {
for (var a = f[t], r = !0, n = 1; n < a.length; n++) {
var d = a[n];
0 !== o[d] && (r = !1)
}
r && (f.splice(t--, 1),
e = c(c.s = a[0]))
}
return e
}
var r = {}
, n = {
21: 0
}
, o = {
21: 0
}
, f = [];
function c(t) {
if (r[t])
return r[t].exports;
var a = r[t] = {
i: t,
l: !1,
exports: {}
};
return e[t].call(a.exports, a, a.exports, c),
a.l = !0,
a.exports
}
c.e = function(e) {
var t = [];
n[e] ? t.push(n[e]) : 0 !== n[e] && {
1: 1,
3: 1,
4: 1,
5: 1,
6: 1,
7: 1,
8: 1,
9: 1,
10: 1,
11: 1,
12: 1,
13: 1,
14: 1,
15: 1,
16: 1,
17: 1,
18: 1,
19: 1,
20: 1,
22: 1,
23: 1,
24: 1,
25: 1,
26: 1
}[e] && t.push(n[e] = new Promise((function(t, a) {
for (var r = "css/" + ({
1: "common",
3: "album",
4: "albumDetail",
5: "album_mall",
6: "category",
7: "cmtpage",
8: "download_detail",
9: "index",
10: "msg_center",
11: "mv",
12: "mvList",
13: "mv_toplist",
14: "notfound",
15: "player",
16: "player_radio",
17: "playlist",
18: "playlist_edit",
19: "profile",
20: "radio",
22: "search",
23: "singer",
24: "singer_list",
25: "songDetail",
26: "toplist"
}[e] || e) + "." + {
1: "2e3d715e72682303d35b",
3: "5cf0d69eaf29bcab23d2",
4: "798353db5b0eb05d5358",
5: "df4c243f917604263e58",
6: "20d532d798099a44bc88",
7: "e3bedf2b5810f8db0684",
8: "e3bedf2b5810f8db0684",
9: "ea0adb959fef9011fc25",
10: "020422608fe8bfb1719a",
11: "8bdb1df6c5436b790baa",
12: "47ce9300786df1b70584",
13: "4aee33230ba2d6b81dce",
14: "e6f63b0cf57dd029fbd6",
15: "1d2dbefbea113438324a",
16: "d893492de07ce97d8048",
17: "9484fde660fe93d9f9f0",
18: "67fb85e7f96455763c83",
19: "5e8c651e74b13244f7cf",
20: "3befd83c10b19893ec66",
22: "b2d11f89ea6a512a2302",
23: "c7a38353c5f4ebb47491",
24: "df0961952a2d3f022894",
25: "4c080567e394fd45608b",
26: "8edb142553f97482e00f"
}[e] + ".chunk.css?max_age=2592000", o = c.p + r, f = document.getElementsByTagName("link"), d = 0; d < f.length; d++) {
var i = (b = f[d]).getAttribute("data-href") || b.getAttribute("href");
if ("stylesheet" === b.rel && (i === r || i === o))
return t()
}
var l = document.getElementsByTagName("style");
for (d = 0; d < l.length; d++) {
var b;
if ((i = (b = l[d]).getAttribute("data-href")) === r || i === o)
return t()
}
var u = document.createElement("link");
u.rel = "stylesheet",
u.type = "text/css",
u.onload = t,
u.onerror = function(t) {
var r = t && t.target && t.target.src || o
, f = new Error("Loading CSS chunk " + e + " failed.\n(" + r + ")");
f.code = "CSS_CHUNK_LOAD_FAILED",
f.request = r,
delete n[e],
u.parentNode.removeChild(u),
a(f)
}
,
u.href = o,
0 !== u.href.indexOf(window.location.origin + "/") && (u.crossOrigin = "anonymous"),
document.getElementsByTagName("head")[0].appendChild(u)
}
)).then((function() {
n[e] = 0
}
)));
var a = o[e];
if (0 !== a)
if (a)
t.push(a[2]);
else {
var r = new Promise((function(t, r) {
a = o[e] = [t, r]
}
));
t.push(a[2] = r);
var f, d = document.createElement("script");
d.charset = "utf-8",
d.timeout = 120,
c.nc && d.setAttribute("nonce", c.nc),
d.src = function(e) {
return c.p + "js/" + ({
1: "common",
3: "album",
4: "albumDetail",
5: "album_mall",
6: "category",
7: "cmtpage",
8: "download_detail",
9: "index",
10: "msg_center",
11: "mv",
12: "mvList",
13: "mv_toplist",
14: "notfound",
15: "player",
16: "player_radio",
17: "playlist",
18: "playlist_edit",
19: "profile",
20: "radio",
22: "search",
23: "singer",
24: "singer_list",
25: "songDetail",
26: "toplist"
}[e] || e) + ".chunk." + {
1: "a19d13b1f550b0a74773",
3: "4e095ebbcb9e70be04b4",
4: "e3852fd8e0f7280f664b",
5: "b9ad62f6d895d28fac34",
6: "5b0a5766aceabae9cca5",
7: "247a6801f111a0f0248d",
8: "cd8494d8383ad9903094",
9: "a7047eb4c37a6478ed4c",
10: "b92a08f6c8f1e635ceaa",
11: "f1f18259c1c57f8fbcdf",
12: "bb54c66fafb41cb72caa",
13: "ef9d323eabbc2c38bc3e",
14: "aef3af0909ad27a9e18d",
15: "f21aa0b57606c669a045",
16: "a28f3be66a41e0b1a4e2",
17: "b190de0c33f5c6303f60",
18: "39c1a683083b05275679",
19: "e218e34000b7d35e8764",
20: "711790cae7a459dfaea5",
22: "657433415a2330f5d636",
23: "950ad1844b00ee5b48f5",
24: "a99d35055652ba5024bf",
25: "72d76900f95a93d319cf",
26: "18163e92a2b8c2ebe8cf"
}[e] + ".js?max_age=2592000"
}(e),
0 !== d.src.indexOf(window.location.origin + "/") && (d.crossOrigin = "anonymous");
var i = new Error;
f = function(t) {
d.onerror = d.onload = null,
clearTimeout(l);
var a = o[e];
if (0 !== a) {
if (a) {
var r = t && ("load" === t.type ? "missing" : t.type)
, n = t && t.target && t.target.src;
i.message = "Loading chunk " + e + " failed.\n(" + r + ": " + n + ")",
i.name = "ChunkLoadError",
i.type = r,
i.request = n,
a[1](i)
}
o[e] = void 0
}
}
;
var l = setTimeout((function() {
f({
type: "timeout",
target: d
})
}
), 12e4);
d.onerror = d.onload = f,
document.head.appendChild(d)
}
return Promise.all(t)
}
,
c.m = e,
c.c = r,
c.d = function(e, t, a) {
c.o(e, t) || Object.defineProperty(e, t, {
enumerable: !0,
get: a
})
}
,
c.r = function(e) {
"undefined" !== typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, {
value: "Module"
}),
Object.defineProperty(e, "__esModule", {
value: !0
})
}
,
c.t = function(e, t) {
if (1 & t && (e = c(e)),
8 & t)
return e;
if (4 & t && "object" === typeof e && e && e.__esModule)
return e;
var a = Object.create(null);
if (c.r(a),
Object.defineProperty(a, "default", {
enumerable: !0,
value: e
}),
2 & t && "string" != typeof e)
for (var r in e)
c.d(a, r, function(t) {
return e[t]
}
.bind(null, r));
return a
}
,
c.n = function(e) {
var t = e && e.__esModule ? function() {
return e.default
}
: function() {
return e
}
;
return c.d(t, "a", t),
t
}
,
c.o = function(e, t) {
return Object.prototype.hasOwnProperty.call(e, t)
}
,
c.p = "/ryqq/",
c.oe = function(e) {
throw e
}
;
var d = window.webpackJsonp = window.webpackJsonp || []
, i = d.push.bind(d);
d.push = t,
d = d.slice();
for (var l = 0; l < d.length; l++)
t(d[l]);
var b = i;
a()
qqloader = c;
}([]);
function main123(data){
i = qqloader(354).default;
return i(data);
}
还有一个js脚本文件太大,无法上传。
所有爬虫代码已上传网盘,可以关注公众号【站在前沿】,回复qqmusic,获取网盘下载链接。有其它需求也可以在微信公众号留言。
QQ音乐下载代码已分享,但是下载VIP歌曲需要开通QQ音乐会员并替换您的QQ号码,可以尝试进行下载。QQ会员结束前我已经下载所有歌曲和MV文件,文件总大小达2T以上,如果需要可以有偿提供,可以公众号留言提出您的需求。
备份和保存音乐、视频文件和照片文件可以购买移动硬盘或U盘,都是固态硬盘,文件拷贝可以达到每秒1G或500M。使用微信或京东APP扫码下午购买。