NiceLeeのBlog 用爱发电 bilibili~

Java 一个入门的网络爬虫用例

2018-10-25
nIceLee

阅读:


背景 博客首页的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&notice=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);

##最后的话
阿西吧,多做多会,以后不掉坑额(⊙﹏⊙)
附上代码


内容
隐藏