背景 博客首页的QQ音乐链接又双叒叕失效了,很烦。加上最近碰到了一个很有意思的网站-刘志进实验室。有个想法,我能不能自己实现这样一个类似的功能,通过关键词从QQ音乐获取歌曲信息,然后直接在生成页面。
心路历程
没啥难度。
– 直接扒网站接口,然后通过js直接请求调用来获取信息,这是我的一开始的想法,没啥难度
However 捣鼓了一段时间,发现单纯的前端静态页面没法实现跨域请求,而QQ音乐那边的配置又没法有配合,这个想法废弃掉了。。。
——————-
换个方式So easy!
– 这样不行的话,那么客户端直接爬网页吧,在打开QQ音乐主页进行搜索,然后爬页面结果。语言的话,想用python和java都来一遍,先用java吧。
– 在网上看了很多的开源项目,感觉掌握起来很麻烦诶,我也没那么多需求。我想吧,这只是一个入门级的爬虫,其实更多的只是需要html的解析功能。找了半天,感觉jsoup刚刚够用。如果只是返回xml或json的http请求的话,甚至Java自带的java.net.HttpURLConnection也不是不能考虑。就这样决定了,jsoup!!!
Document doc = Jsoup.connect(url).timeout(2000).get();
参考
通过java.net.URLConnection发送HTTP请求的方法
——————-
WTF什么鬼?
愉快的打开QQ音乐的网页,然后搜索关键词 - 战 排骨教主
最后发现地址如下:
https://y.qq.com/portal/search.html#page=1&searchid=1&remoteplace=txt.yqq.top&t=song&w=%E6%88%98%20%E6%8E%92%E9%AA%A8%E6%95%99%E4%B8%BB
这个地址显然很容易解析,极具规律,那么接下来的工作就简单了…简单了…单了…了…
WTF?! tm爬出来的html和浏览器里面取出来的压根匹配不上,一点有用信息也没有
——————-
再来!!
两种思路:
一种不再爬页面html了,直接人工监听获取数据请求的接口,使用http请求获取数据,刚刚好QQ音乐返回的是json格式,很好解决
-java爬虫获取动态网页的数据的一种思路
-浏览器进行js调试
-java json解析
一种模拟加载html后js的各种行为,然后再爬,这里想要采用的是HtmlUnit + Jsoup框架
使用HtmlUnit + Jsoup解析js动态生成的网页
本文选用的是第一种,第二种有时间再尝试
——————-
具体实现
根据关键词 搜索得到歌曲信息
进行以下过程可以获取歌曲信息。
—- 打开QQ音乐搜索页,搜索关键词 - 战 排骨教主
按F12,打开Network进行查看,刷新网页复现过程
我们可以根据Response来Check是否是我们想要的请求
点击Headers可以发现我们想要的Request URL。
具体实现如下:
- 生成RequestUrl
其中p=1 代表分页第一页,n=3代表每页3个数据,w代表关键词private String genSongLstUrl( String keyWord ){ try { StringBuffer sbUrl = new StringBuffer(); sbUrl.append("https://c.y.qq.com/soso/fcgi-bin/client_search_cp?ct=24&qqmusic_ver=1298&new_json=1&t=0&aggr=1&cr=1&p=1&n=3&w="); sbUrl.append(java.net.URLEncoder.encode(keyWord,"utf-8")); return sbUrl.toString(); } catch (Exception e) { System.err.printf("Method - querySongLstUrl : keyWord to Url error"); return ""; } }
- 发送Http请求
private String getJsonStr( String url ){ Response res; try { res = Jsoup.connect(url) .header("Accept", "*/*") .header("Accept-Encoding", "gzip, deflate, sdch, br") .header("Accept-Language","zh-CN,zh;q=0.8") .header("Content-Type", "application/json;charset=UTF-8") .userAgent("Mozilla/5.0 (Windows NT 10.0; WOW64)") .referrer("https://y.qq.com/portal/player.html") .ignoreContentType(true).execute(); String jsonStr = res.body(); //System.out.println(jsonStr); return jsonStr; } catch (IOException e) { System.err.printf("Connect to url: %s response error!\n",url); return ""; } }
- json解析如下:
private List<Song> jsonToSongList( String jsonStr ){ jsonStr = jsonStr.replace("callback(", ""); jsonStr = jsonStr.substring(0, jsonStr.length()-1); //System.out.println(jsonStr); List<Song> lstSong = new ArrayList<>(); JSONArray jsonArr = new JSONObject(jsonStr) .getJSONObject("data") .getJSONObject("song") .getJSONArray("list"); //System.out.printf("共有%d个搜索结果:\n------------\n",jsonArr.length()); for (int i = 0; i < jsonArr.length(); i++){ //System.out.printf(" 当前第一个歌曲%d信息:\n",i+1); Song song = new Song(); JSONObject json = jsonArr.getJSONObject(i);//第i首歌曲 song.mid = json.getString("mid");//title song.name = json.getString("name");//title song.singer = json.getJSONArray("singer") .getJSONObject(0).getString("name");//title song.singermid = json.getJSONArray("singer") .getJSONObject(0).getString("mid"); song.album = json.getJSONObject("album").getString("name"); song.albumMid = json.getJSONObject("album").getString("mid"); //System.out.printf(" 歌曲mid: %s\n",song.mid); //System.out.printf(" 曲名: %s\n",song.name); //System.out.printf(" 演唱者: %s ,对应mid: %s\n",song.singer,song.mid); //System.out.printf(" 专辑: %s ,对应mid: %s\n",song.album,song.albumMid); //System.out.printf("------------\n"); lstSong.add(song); } return lstSong; }
根据songMid得到 歌曲外链
—- 打开QQ音乐搜索页,搜索关键词 - 战 排骨教主,点击播放其中一首歌曲,会弹出另一个页面
和以上类似,不过监控的是弹出的另一个页面
private String genSongLinkUrl( String songMid){
try {
final String urlHeader = "https://u.y.qq.com/cgi-bin/musicu.fcg?data=";
String urlData = "{\"req_0\":{\"module\":\"vkey.GetVkeyServer\",\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"2857186708\",\"songmid\":[\"%s\"],\"songtype\":[0],\"uin\":\"0\",\"loginflag\":1,\"platform\":\"20\"}},\"comm\":{\"uin\":0,\"format\":\"json\",\"ct\":20,\"cv\":0}}";
urlData = String.format(urlData, songMid);
urlData = java.net.URLEncoder.encode(urlData,"utf-8");
return urlHeader + urlData;
} catch (UnsupportedEncodingException e) {
System.err.printf("Method - querySongLstUrl : keyWord to Url error");
return "";
}
}
根据songMid得到 歌词
—- 打开QQ音乐搜索页,搜索关键词 - 战 排骨教主,点击播放其中一首歌曲,会弹出另一个页面
和以上类似,不过监控的是弹出的另一个页面
需要注意的是,Refer需要设置https://y.qq.com,否则无法成功
Jsoup.connect(url).referrer("https://y.qq.com/portal/player.html")
RequestUrl如下:
String dstUrl = String.format("https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg?callback=MusicJsonCallback_lrc&pcachetime=1540437434727&songmid=%s&g_tk=5381&jsonpCallback=MusicJsonCallback_lrc&loginUin=0&hostUin=0&format=jsonp&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0",songMid);
解析需要用到Base64:
private String jsonToLyric(String jsonStr){
try {
jsonStr = jsonStr.replace("MusicJsonCallback_lrc(", "");
jsonStr = jsonStr.replace("})", "}");
String lyricOrg = new JSONObject(jsonStr)
.getString("lyric");
final Base64.Decoder decoder = Base64.getDecoder();
//解码
String lyric = new String(decoder.decode(lyricOrg), "UTF-8");
//System.out.printf("歌词\n%s\n\n",lyric);
return lyric;
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
根据albumMid得到 专辑图片链接
这个可以直接生成url,没法通过监听http请求实现,具体方法可以参考:
-浏览器进行js调试
在跑的时候看到有如下js代码:
getMidPic: function(t) {
t = t || {};
var e = "//y.gtimg.cn/mediastyle/macmusic_v4/extra/default_cover.png?max_age=31536000"
, o = t.page
, n = t.type
, i = t.mid;
return window.devicePixelRatio && parseInt(window.devicePixelRatio) > 1 && (150 == n && (n = 300),
(68 == n || 90 == n) && (n = 150)),
"string" == typeof i && i.length >= 14 ? (o = "album" == o ? "T002" : "singer" == o ? "T001" : o,
e = "//y.gtimg.cn/music/photo_new/" + o + "R" + (n || 68) + "x" + (n || 68) + "M000" + i + ".jpg?max_age=2592000") : i > 0 && (e = "//y.gtimg.cn/music/photo/" + o + "_" + (n || 68) + "/" + i % 100 + "/" + (n || 68) + "_" + o + "pic_" + i + "_0.jpg?max_age=2592000"),
e
}
所以能够直接生成:
String dstUrl = String.format("https://y.gtimg.cn/music/photo_new/T001R300x300M000%s.jpg?max_age=2592000",albumMid);
##最后的话
阿西吧,多做多会,以后不掉坑额(⊙﹏⊙)
附上代码