之前在公司实习的时候,做过一个QQ音乐歌词爬虫的项目,期间网上找了不少参考资料,自己也研究过QQ音乐的js解析,这里简单做个记录,以供以后参考。
单曲搜索接口
访问链接
1 | http://c.y.qq.com/soso/fcgi-bin/search_cp?t=0&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=1&w=#{1}&n=#{2}&g_tk=938407465&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0 |
- w表示的是搜索关键词
- n表示的是结果返回的个数
- format表示返回结果的格式,QQ原本的format方式是jsonp,这里改成json,使其返回的结果变成json格式的数据
返回结果示例
目前使用的几个主要属性如下:
- data-song-list是搜索的结果
- albumid 专辑id
- albummid 专辑mid
- albumname 专辑名称
- grp QQ音乐搜索结果页面上有些结果是折腾显示的,折叠的数据就在这里面,其基本结构跟list的一个元素是一样的
- singer 歌手列表,每个歌手是singer下的一个元素
- id 歌手id
- mid 歌手mid
- name 歌手名
- songid 歌曲id
- songmid 歌曲mid
- songname 歌曲名称
1 | { |
歌曲详情页
访问链接
1 | http://y.qq.com/portal/song/#{1}.html |
#{1}
处填写的是歌曲的mid
这是通用情况,绝大部分歌曲都是用的这个链接来访问
但是,存在一小部分歌曲,songid等一系列基本数据都是0,后经部分观察发现,使用上面那个url来获得歌曲详情的歌曲,type都是0
对于songid为0的这部分歌曲,目前来看是type=2,应该还有其他类型,但是没有发现对应的歌曲。这部分歌曲,他的访问链接和正常歌曲是不一样的。1
2http://y.qq.com/portal/song2/#{1}/#{2}.html
http://y.qq.com/portal/song2/46/16783190164570565401.html
其中,#{2}对应于搜索json的docid,#{1}没弄清楚是什么东西,song2后面接的都是46.至于song2,这是js中生成的,目前猜测是对应于type=2中的2。其他的因为没有找到对应的歌曲,所以也就没有深入去分析QQ那边的js。对于绝大部分歌曲来说,QQ那边的songid都不会是0【目前获得的51049条记录中,仅1468条记录的songid是0】所以这种情况应该可以不予考虑了。
这里记录一下QQ中对应的js解析部分,以备后面需要深入研究时再继续处理。
构造规则在http://imgcache.gtimg.cn/music/portal/js/common/pkg/common_61970c5.js?max_age=31536000
请求获得的music.js
文件中1
2
3
4
5
6
7
8
9gotoSongdetail: function(a) {
// 默认是 根据songmid为构造歌曲详情的url
var b = "//y.qq.com/portal/song/" + a.mid + ".html";
a.songtype && 1 != a.songtype && 11 != a.songtype && 13 != a.songtype && 3 != a.songtype && (b = "//y.qq.com/portal/song2/" + a.songtype + "/" + a.id + ".html");
a.songtype && (111 == a.songtype || 112 == a.songtype || 113 == a.songtype) && (b = "//y.qq.com/portal/song2/" + a.songtype + "/" + a.id + ".html");
a.disstid && a.songtype && (b = "//y.qq.com/portal/song3/" + a.songtype + "/" + a.disstid + "/" + a.id + ".html");
window.open(b, f.util.getPageTarget())
}
歌词提取
一开始是打算直接提取html页面的内容,但是查看网页源文件后发现歌曲详情页的实际数据是异步构造的。简单尝试直接请求返回歌词的url后发现,这边的url做了防范,直接请求返回的是{"retcode":-1310,"code":-1310,"subcode":-1310}
,心想还是看看有没有办法获得js加载完毕后的html吧。
Google+各种尝试后发现,可以使用phantomjs来获取js加载完成后的网页,于是折腾了一会儿把本机配起来运行了。乍一看,似乎还可以,但是等到大量歌曲运行起来后,发现还是有一定几率存在没等js加载完就解析网页的,感觉命中率不能忍受。
没办法,只能继续去啃原生的js请求了。chrome调试后,找到请求歌词的实际url1
http://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric.fcg?nobase64=1&musicid=7109361&callback=jsonp1&g_tk=938407465&jsonpCallback=jsonp1&loginUin=0&hostUin=0&format=jsonp&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0
针对请求返回的是错误代码,仔细研究了一下request header,发现其中有个Referer:http://y.qq.com/portal/song/002OrhQA0bNYFg.html
请求头,是QQ用来防盗链的(咋就这一个js请求要防盗链呢,其他都不防,醉了。难道是被人爬歌词爬怕了hhhh)。在HTTPClient的get请求中附带上Referer,指向实际歌曲详情的页面,终于发现返回的不是错误代码了。。。。
可是问题的关键是,返回的结果是一堆乱码。一开始以为是编码问题,尝试了各种字符编码,还是乱码。仔细看了下Response header,发现其中有个Content-Encoding:gzip
,Google了一下发现其实真正“乱码”原因是被压缩了,需要java这边手动解压一下。解压之后发现,就是这段了,他返回了当前歌曲的动态歌词。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//获取消息头Content-Encoding判断数据流是否gzip压缩过
Header[] contentEncodings = httpResponse.getHeaders("Content-Encoding");
Header contentEncoding = null;
if (contentEncodings.length > 0) {
contentEncoding = httpResponse.getHeaders("Content-Encoding")[0];
}
HttpEntity entity = httpResponse.getEntity();
if ((contentEncoding != null) &&contentEncoding.getValue().equalsIgnoreCase("gzip")) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPInputStream gzip = new GZIPInputStream(entity.getContent());
byte[] buffer = new byte[bufferSize];
int n = 0;
while ((n = gzip.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
html = out.toString("UTF-8");
} else {
html = EntityUtils.toString(entity, "UTF-8"); //获得html源代码
}
还是梳理一下QQ那边的流程吧。
设置好相应的请求头,通过上面的URL获得动态歌词,然后前端js根据需要,对获得的内容进行清洗获得文本歌词
具体的js操作在http://imgcache.gtimg.cn/music/portal/js/v4/song_detail_37c8119.js?max_age=31536000
请求获得的song_detail.js
中,关键的就是下面三行代码1
2
3var t = s.lyric.unescapeHTML(), //html反转义
i = t.replace(/\[[^\[\]]*\]/g, "<p>").replace(/\\n/g, "</p>").trim(); //html展示的文本标签
lyricStr = t.replace(/\[[^\[\]]*\]/g, "").replace(/\\n/g, "\r\n").trim(); //文本歌词
动态歌词
上面我们提到了,在想方设法获得文本歌词的过程中,发现了QQ实际上是将动态歌词转成文本歌词的,所以动态歌词也就理所当然的一起得到了。
但实际上在一开始的时候,不是这么获得动态歌词的。虽然那个接口数据量可能没有目前使用的接口数据量多,但是还是记录一下吧。
访问链接
1 | http://music.qq.com/miniportal/static/lyric/songid%100/songid.xml |
- songid就是QQ中音乐id
这边返回的是一个xml文件,并且注意编码是GB2312的,但如果java中使用GB2312来解析这个xml文件,会出现繁体字不能识别的情况。这是因为GB2312只收录了6k多个汉字和符号,GBK在兼容GB2312的基础上,扩展了GB13000,收录了2万多汉字及符号。所以我们这边使用GBK来解析这个xml文件。
值的注意的是,部分文件,动态歌词的接口返回的其实就是文本歌词,所以需要过滤一遍,不能跟QQ那边一样把文本歌词设置在动态歌词中。