使用java爬取網易雲音樂
- 目的:抓取網易雲音樂熱門歌手及其歌曲、專輯等信息保存到數據庫
- 技術點:
- 使用HttpClient和Jsoup進行模擬請求並對網頁進行解析
- 使用springBoot+maven構建管理項目
- 使用mybatis作爲數據訪問
- 數據庫設計
- song : id,name,comment_thread_id,mv_id,record_id,mp3Url
- singer: id,name,intro,picUrl
- record: id,name,picUrl,singer_id,tags,type,intro,company,comment_thread_id
- singer_song: singer_id,song_id,intro
這裏考慮到一首歌曲可對應多個歌手,故建立一箇中間表保存雙方的id (多對多)
爬取網站的分析
1.歌曲頁面的分析
目標網站:https://music.163.com/#/artist?id=6452
我們先以該網頁進行分析,在對網頁抓包中我們發現有個名爲artist?id=2116的請求,在該請求返回的數據中我們發現就是該網頁歌手對應的頁面(即html)
在該返回的html中,在標籤<textarea id="song-list-pre-data" style="display:none;">...</textarea>
中就存在該歌手的50首歌曲的json數據。對於所要做的系統來說,50首歌曲數據已經夠了,爲此我們拿到該數據即可。
但是在其中我們並沒有發現歌曲的url。當對歌曲進行點擊後,抓包(url?csrf_token=)發現
瀏覽器向https://music.163.com/weapi/song/enhance/player/url?csrf_token= 發送了post請求,返回的就是帶有當前歌曲url的json數據。
其中有兩個參數
params:IvwCQv++yCDpnlC+Tog13k9WVWYgou3LauQ60jh9YPGSL1AkcUxma7r1Hs8yTaXBGYluJV7tl0xrTmtgl3qxQ6TZYNBIxfFvkaqqDxRKJvgsBOYV2SpD8mRfxrgbesqH
encSecKey:2b29c882a53743986c4f5aa279f1f2353a84a8d773071877252dbfc805c34b2065de3738945ea1bbdb29602b85d7ef4382d4d77c44c9eed6cb0c88ce7d3e37883b222b77381929a367b2fc062c3499ebfc7135c9d3a51b2fb8bb316f4c8006d2e30141b9be9de6bd017096fdeaf645e4450c88999febad081f9b6cc0e83fbaaa
這兩個參數是加密過的。
找到https://s3.music.126.net/web/s/core.js?acad6e83fe6f991104f40576c001942a
在裏面搜索encSecKey,找到加密函數,經過了兩次加密。。
加密技術不太好搞,可以換種別的方法。
在對音樂生成外鏈的抓包中,意外地發現個url:http://music.163.com/song/media/outer/url?id=25730757
該url可直接拿到歌曲的url,這樣我們只需要記住“http://music.163.com/song/media/outer/url?id=” 鏈接,最後加上歌曲id就可以拿到歌曲mp3的url了。
歌曲url:”https://music.163.com/artist?id=“+singerId;
歌曲mp3Url:“http://music.163.com/song/media/outer/url?id=” +songId;
2.歌手頁面的分析
目標網站:https://music.163.com/#/artist/desc?id=2116
以所需要的圖片數據爲例,F12打開開發者工具,找到圖片所在標籤,右鍵複製元素的selector,得到#auto-id-herlzRnQZM09uulQ > div.g-bd4.f-cb > div.g-mn4 > div > div > div.n-artist.f-cb > img。其它所需數據也是通過此方法拿到對應的selector,之後用Jsoup解析即可得到對應的元素。
歌手url:”https://music.163.com/artist/desc?id=“+singerId;
3.專輯頁面的分析
目標網站:https://music.163.com/#/artist/album?id=2116
我們發現,專輯頁面進行了分頁,在進行下一頁過程的抓包中,發現專輯分頁的規律是以limit和offset兩個參數決定的。在對幾個熱門歌手分析中可以發現歌手的專輯數是沒有超過兩位數的,所以我們可以簡單地假設一個歌手的專輯數不會超過100。因爲要儘可能不跳轉頁面發請求,所以得到一個
url: https://music.163.com/artist/album?id=“+singerId+”&limit=100&offset=0
我們以這個url去請求頁面,以歌手爲例的拿取元素位置即可得到專輯所需要的信息。
代碼邏輯分析
貼幾個核心代碼:
爬蟲基類:
package com.netMusic.spider;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.netMusic.entity.Record;
import com.netMusic.entity.Singer;
import com.netMusic.entity.custom2.SongMsg;
import com.netMusic.utils.CharacterUtils;
import com.netMusic.utils.HttpClientUtil;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.lang.reflect.Type;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 歌曲爬取類
*/
public class NetMusicGrab {
private final static Logger logger = LoggerFactory.getLogger(NetMusicGrab.class);
public static List<SongMsg> getSongList(String url,String charest) {
Document document = getDocument(url, charest);
if(document!=null){
Elements elements = document.select("#song-list-pre-data");
System.out.println("json數據如下");
String resJson = elements.text();
// logger.info(resJson);
Gson gson = new Gson();
Type listType = new TypeToken<List<SongMsg>>() {}.getType();
if(resJson!=null && !resJson.contains("html"));
List<SongMsg> msgList = gson.fromJson(resJson, listType);
return msgList;
}
return null;
}
public static Singer getSinger(String url,String charest){
Document document = getDocument(url,charest);
if(document!=null){
Elements singerName = document.select("#artist-name");
System.out.println("歌手名字:"+singerName.text());
Elements singerAlias = document.select("#artist-alias");
String name="";
if(singerAlias!=null && !"".equals(singerAlias.text())){
System.out.println("歌手別名:"+singerAlias.text());
name = singerName.text()+"/"+singerAlias.text();
}else{
name = singerName.text();
}
Elements desc = document.select("body > div.g-bd4.f-cb > div.g-mn4 > div > div > div:nth-child(3) > div > p:nth-child(2)");
Elements image = document.select("body > div.g-bd4.f-cb > div.g-mn4 > div > div > div.n-artist.f-cb > img");
String intro = desc.text();
String picUrl = image.attr("src");
Singer singer = new Singer();
singer.setName(name);
if(intro.length()>=255 || "".equals(intro)){
intro = "暫無介紹";
}
singer.setIntro(intro);
singer.setPicUrl(picUrl);
return singer;
}
return null;
}
public static List<Record> getRecordList(String url,String charest){
List<Record> recordList = new ArrayList<>();
String publishTime = null;
String company = null;
Document document = getDocument(url,charest);
if(document!=null){
Elements elements = document.select("#m-song-module");
for(Element element :elements){
//專輯鏈接
Elements urlElement = element.select("#m-song-module > li:nth-child(1) > div > a.msk");
String albumUrl = urlElement.select(".msk").attr("href");
if(!albumUrl.contains("https://")){
albumUrl = "https://music.163.com"+albumUrl;
}
Elements elementsAlbumId = element.select("#m-song-module > li:nth-child(1) > div > a.icon-play.f-alpha");
String albumId = elementsAlbumId.attr("data-res-id");
try {
Document albumDec = Jsoup.connect(albumUrl).
userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36")
.get();
Elements main = albumDec.select("body > div.g-bd4.f-cb.p-share > div.g-mn4 > div > div > div.m-info.f-cb > div.cnt > div > div.topblk");
Elements elementsName = main.select("div > div > h2");
String albumName = elementsName.text();
Elements elementsPublishTime = main.select("p:nth-child(3)");
if(elementsPublishTime!=null && !"".equals(elementsPublishTime.text()) ){
publishTime = elementsPublishTime.text().substring(5);
}
Elements elementsCompany = main.select("p:nth-child(4)");
if(elementsCompany!=null && !"".equals(elementsCompany.text()) ){
company = elementsCompany.text().substring(5);
}
//如果包含中文,則說明該字段爲company
if(CharacterUtils.isContainChinese(publishTime)){
company = publishTime;
publishTime = null;
}
Elements elementsCommentId = albumDec.select("#cnt_comment_count");
String commentId = elementsCommentId.text();
if(CharacterUtils.isContainChinese(commentId)){
commentId = null;
}
Elements elementsImg = albumDec.select("body > div.g-bd4.f-cb.p-share > div.g-mn4 > div > div > div.m-info.f-cb > div.cover.u-cover.u-cover-alb > img");
String picUrl = elementsImg.attr("data-src");
Elements elementsIntro = albumDec.select("#album-desc-dot");
String intro = elementsIntro.text();
if(intro.length()>=255 || intro.length()<=1){
intro = "暫無介紹";
}
Record record = new Record();
record.setId(Integer.valueOf(albumId));
record.setIntro(intro);
record.setPicUrl(picUrl);
record.setName(albumName);
record.setCompany(company);
record.setCommentThreadId(commentId);
if(publishTime!=null){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse(publishTime);
record.setPublishTime(date);
}else{
record.setPublishTime(null);
}
recordList.add(record);
} catch (IOException e) {
e.printStackTrace();
}catch (Exception e){
e.printStackTrace();
}
}
}
return recordList;
}
private static Document getDocument(String url, String charest) {
List<Header> headerList = new ArrayList<>();
headerList.add(new BasicHeader("Host", "music.163.com"));
headerList.add(new BasicHeader("Referer", "https://music.163.com/"));
headerList.add(new BasicHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36"));
String result = HttpClientUtil.doGet(url, headerList, charest);
if(result!=null && !result.contains("n-for404")){
return Jsoup.parse(result);
}
return null;
}
public static void main(String[] args) throws ParseException {
// 測試獲取歌手信息
// List<Integer> listId = BaseUtil.getRandomNumber(1000,10000,
// }
//測試專輯信息100);
//// for(Integer id:listId){
//// Singer singer = getSinger("https://music.163.com/artist/desc?id="+id,"utf-8");
//// System.out.println(singer);
// getRecordList("https://music.163.com/artist/album?id=3684&limit=100&offset=0","utf-8");
//測試字符串轉時間
// SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// Date date = sdf.parse("2017-12-12");
// System.out.println(date);
}
}
因爲考慮到速度問題,採用簡單的多線程開啓方式,這裏設置了三個子線程,分別爬取專輯,歌曲,歌手。同時,爲使項目啓動時即開始爬,添加了監聽類,用來啓動線程。
SpiderSingerRunnable類:
SpiderRecordRunnable類:
SpiderSongRunnnable類:
SaveDataListener類:
具體的代碼可以在我的github找到,剩餘的邏輯和一些自己封裝的工具包就不貼了也相對簡單。
學習總結
經過測試,20分鐘左右爬取了大約14萬的歌曲數據,期間有幾次被封ip了,直接404,爲解決這個問題得用上ip連接池不斷地更換ip地址發出請求,不過目前來講已經夠了,之後有時間再去學學。還有個問題是,雖然採用了多線程,但我發現並沒有將多線程用得很好,希望有時間去學下Redis緩存隊列再對代碼進行更新吧。