网易云音乐java爬虫_用Java实现网易云音乐爬虫
起因
前兩天在知乎上看到一個(gè)帖子《網(wǎng)易云音樂(lè)有哪些評(píng)論過(guò)萬(wàn)的歌曲?》,一時(shí)技癢,用Java實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的爬蟲(chóng),這里簡(jiǎn)單記錄一下。
最終的結(jié)果開(kāi)放出來(lái)了,大家可以隨意訪問(wèn),請(qǐng)戳這里>>>>>> 網(wǎng)易云音樂(lè)爬蟲(chóng)結(jié)果。
爬蟲(chóng)簡(jiǎn)介
網(wǎng)絡(luò)爬蟲(chóng)是一種按照一定的規(guī)則,自動(dòng)地抓取萬(wàn)維網(wǎng)信息的程序或者腳本,一個(gè)通用的網(wǎng)絡(luò)爬蟲(chóng)大致包含以下幾個(gè)步驟:
網(wǎng)絡(luò)爬蟲(chóng)的大致流程如上圖所示,無(wú)論你是做什么樣的爬蟲(chóng)應(yīng)用,整體流程都是大同小異。現(xiàn)在,我們就根據(jù)網(wǎng)易云音樂(lè)來(lái)定制一個(gè)專門(mén)爬取音樂(lè)評(píng)論數(shù)量的特定網(wǎng)絡(luò)爬蟲(chóng)。
前期準(zhǔn)備
網(wǎng)頁(yè)類型分析
首先,我們需要對(duì)網(wǎng)易云音樂(lè)整個(gè)網(wǎng)站有個(gè)大致的了解,進(jìn)入網(wǎng)易云音樂(lè)首頁(yè),瀏覽后發(fā)現(xiàn)其大概有這么幾種類型的URL:推薦頁(yè)面
排行榜列表以及排行榜頁(yè)面
歌單列表以及歌單頁(yè)面
主播電臺(tái)列表以及主播電臺(tái)頁(yè)面
歌手列表以及歌手頁(yè)面
專輯列表(新碟上架)以及專輯頁(yè)面
歌曲頁(yè)面
我們最終需要爬取的數(shù)據(jù)在歌曲頁(yè)面中,該頁(yè)面里包含了歌曲的名稱以及歌曲的評(píng)論數(shù)量。
另外,我們還需要盡可能多的獲取歌曲頁(yè)面,這些信息我們可以從前面6種類型的頁(yè)面拿到。其中,歌單列表以及歌單頁(yè)面結(jié)構(gòu)最簡(jiǎn)單,歌單列表直接分頁(yè)就可以拿到。因此,我們選擇歌單頁(yè)面作為我們的初始頁(yè)面,然后歌單列表--歌單--歌曲一路爬下去即可。
設(shè)計(jì)數(shù)據(jù)模型
通過(guò)上述分析,我們可以知道我們要做兩件事情,一是爬取頁(yè)面歌單列表--歌單--歌曲,二是將最終的結(jié)果存儲(chǔ)起來(lái)。因此,我們只需要兩個(gè)對(duì)象,一個(gè)用來(lái)存儲(chǔ)頁(yè)面相關(guān)的信息,url、頁(yè)面類型、是否被爬過(guò)(html和title作為臨時(shí)數(shù)據(jù)存儲(chǔ)),另外一個(gè)用來(lái)存儲(chǔ)歌曲相關(guān)信息,url、歌曲名,評(píng)論數(shù)。因此,model類如下:
public class WebPage {
public enum PageType {
song, playlist, playlists;
}
public enum Status {
crawled, uncrawl;
}
private String url;
private String title;
private PageType type;
private Status status;
private String html;
...
}
public class Song {
private String url;
private String title;
private Long commentCount;
...
}
獲取網(wǎng)頁(yè)內(nèi)容并解析
根據(jù)之前的分析,我們需要爬的頁(yè)面有三種:歌單列表、歌單以及歌曲。為了驗(yàn)證想法的可行性,我們先用代碼來(lái)解析這三種類型的網(wǎng)頁(yè),我們將網(wǎng)頁(yè)內(nèi)容獲取以及解析的代碼都放入CrawlerThread當(dāng)中。
獲取html
無(wú)論想要從什么網(wǎng)站中拿到數(shù)據(jù),獲取其html代碼都是最最基礎(chǔ)的一步,這里我們使用jsoup來(lái)獲取頁(yè)面信息,在CrawlerThread中添加如下代碼:
private boolean fetchHtml(WebPage webPage) throws IOException {
Connection.Response response = Jsoup.connect(webPage.getUrl()).timeout(3000).execute();
webPage.setHtml(response.body());
return response.statusCode() / 100 == 2 ? true : false;
}
public static void main(String[] args) throws Exception {
WebPage playlists = new WebPage("http://music.163.com/#/discover/playlist/?order=hot&cat=%E5%85%A8%E9%83%A8&limit=35&offset=0", PageType.playlists);
CrawlerThread crawlerThread = new CrawlerThread();
crawlerThread.fetchHtml(playlists);
System.out.println(playlists.getHtml());
}
運(yùn)行后即可看到html文本的輸出
解析歌單列表頁(yè)面
得到html后,我們來(lái)解析歌單列表,拿到頁(yè)面中的所有歌單,Jsoup包含了html解析相關(guān)的功能,我們無(wú)需添加其他依賴,直接在CrawlerThread中添加如下代碼:
private List parsePlaylist(WebPage webPage) {
Elements songs = Jsoup.parse(webPage.getHtml()).select("ul.f-hide li a");
return songs.stream().map(e -> new WebPage(BASE_URL + e.attr("href"), PageType.song, e.html())).collect(Collectors.toList());
}
public static void main(String[] args) throws Exception {
WebPage playlists = new WebPage("http://music.163.com/discover/playlist/?order=hot&cat=%E5%85%A8%E9%83%A8&limit=35&offset=0", PageType.playlists);
CrawlerThread crawlerThread = new CrawlerThread();
crawlerThread.fetchHtml(playlists);
System.out.println(crawlerThread.parsePlaylists(playlists));
}
解析歌單頁(yè)面
和歌單列表頁(yè)面類似,只需要將歌曲相關(guān)的元素找出來(lái)即可:
private List parsePlaylist(WebPage webPage) {
Elements songs = Jsoup.parse(webPage.getHtml()).select("ul.f-hide li a");
return songs.stream().map(e -> new WebPage(BASE_URL + e.attr("href"), PageType.song, e.html())).collect(Collectors.toList());
}
public static void main(String[] args) throws Exception {
WebPage playlist = new WebPage("http://music.163.com/playlist?id=454016843", PageType.playlist);
CrawlerThread crawlerThread = new CrawlerThread();
crawlerThread.fetchHtml(playlist);
System.out.println(crawlerThread.parsePlaylist(playlist));
}
注意,這里為了方便,我們將歌曲的名稱也拿到了,這樣后面我們就不需要再次獲取歌曲名稱了。
解析歌曲頁(yè)面
終于到歌曲頁(yè)面了,這里網(wǎng)易云音樂(lè)做了反爬處理,獲取數(shù)據(jù)時(shí)的參數(shù)需要經(jīng)過(guò)加密處理,這里我們不糾結(jié)于具體算法,如果有興趣的直接看參考代碼,我們只看關(guān)鍵代碼:
private Song parseSong(WebPage webPage) throws Exception {
return new Song(webPage.getUrl(), webPage.getTitle(), getCommentCount(webPage.getUrl().split("=")[1]));
}
public static void main(String[] args) throws Exception {
WebPage song = new WebPage("http://music.163.com/song?id=29999506", PageType.song, "test");
CrawlerThread crawlerThread = new CrawlerThread();
crawlerThread.fetchHtml(song);
System.out.println(crawlerThread.parseSong(song));
}
好吧,獲取過(guò)程確實(shí)比較曲折,經(jīng)過(guò)了多次的加密,不過(guò)不管怎么樣,最終我們還是拿到了我們想要的數(shù)據(jù)。接下來(lái),就是使用爬蟲(chóng)將整套機(jī)制run起來(lái)了。
實(shí)現(xiàn)爬蟲(chóng)
重新回顧一下流程圖,我們發(fā)現(xiàn)其中有很重要的一個(gè)對(duì)象是爬蟲(chóng)隊(duì)列,爬蟲(chóng)隊(duì)列的實(shí)現(xiàn)方法有很多種,自己實(shí)現(xiàn),mysql、redis、MongoDB等等都可以滿足我們的需求,不同的選擇會(huì)導(dǎo)致我們實(shí)現(xiàn)的不一致。
綜合考慮,我們使用Mysql+ Spring Data JPA + Spring MVC來(lái)跑我們的整套框架,最終還可以將爬下來(lái)的數(shù)據(jù)通過(guò)web服務(wù)展現(xiàn)出來(lái)。更深入地學(xué)習(xí)Spring MVC,請(qǐng)大家參考Spring MVC實(shí)戰(zhàn)入門(mén)訓(xùn)練。
確定好之后,我們就可以開(kāi)始一步步實(shí)現(xiàn)了。這里Spring Data JPA的代碼就不展示了。了解Spring Data JPA,請(qǐng)參考Spring Data JPA實(shí)戰(zhàn)入門(mén)訓(xùn)練。直接上核心代碼,所有和爬蟲(chóng)整體流程相關(guān)的代碼我們都放進(jìn)CrawlerService中。
初始網(wǎng)址
第一步建立一個(gè)初始網(wǎng)址,我們可以根據(jù)歌單列表分頁(yè)的特征得到:
private void init(String catalog) {
List webPages = Lists.newArrayList();
for(int i = 0; i < 43; i++) {
webPages.add(new WebPage("http://music.163.com/discover/playlist/?order=hot&cat=" + catalog + "&limit=35&offset=" + (i * 35), PageType.playlists));
}
webPageRepository.save(webPages);
}
public void init() {
webPageRepository.deleteAll();
init("全部");
init("華語(yǔ)");
init("歐美");
init("日語(yǔ)");
init("韓語(yǔ)");
init("粵語(yǔ)");
init("小語(yǔ)種");
init("流行");
init("搖滾");
init("民謠");
init("電子");
init("舞曲");
init("說(shuō)唱");
init("輕音樂(lè)");
init("爵士");
init("鄉(xiāng)村");
init("R&B/Soul");
init("古典");
init("民族");
init("英倫");
init("金屬");
init("朋克");
init("藍(lán)調(diào)");
init("雷鬼");
init("世界音樂(lè)");
init("拉丁");
init("另類/獨(dú)立");
init("New Age");
init("古風(fēng)");
init("后搖");
init("Bossa Nova");
init("清晨");
init("夜晚");
init("學(xué)習(xí)");
init("工作");
init("午休");
init("下午茶");
init("地鐵");
init("駕車");
init("運(yùn)動(dòng)");
init("旅行");
init("散步");
init("酒吧");
init("懷舊");
init("清新");
init("浪漫");
init("性感");
init("傷感");
init("治愈");
init("放松");
init("孤獨(dú)");
init("感動(dòng)");
init("興奮");
init("快樂(lè)");
init("安靜");
init("思念");
init("影視原聲");
init("ACG");
init("校園");
init("游戲");
init("70后");
init("80后");
init("90后");
init("網(wǎng)絡(luò)歌曲");
init("KTV");
init("經(jīng)典");
init("翻唱");
init("吉他");
init("鋼琴");
init("器樂(lè)");
init("兒童");
init("榜單");
init("00后");
}
這里,我們初始化了歌單所有分類的列表,通過(guò)這些列表,我們就能拿到網(wǎng)易云音樂(lè)大部分的歌曲。
從爬蟲(chóng)隊(duì)列中拿到一個(gè)URL
這里的邏輯非常簡(jiǎn)單,從mysql中獲取一個(gè)狀態(tài)為未爬的網(wǎng)頁(yè)即可,但是由于我們需要爬的網(wǎng)址非常的多,肯定要用到多線程,因此需要考慮異步的情況:
public synchronized WebPage getUnCrawlPage() {
WebPage webPage = webPageRepository.findTopByStatus(Status.uncrawl);
webPage.setStatus(Status.crawled);
return webPageRepository.save(webPage);
}
爬取頁(yè)面
剛剛說(shuō)到,我們需要爬取的頁(yè)面很多,因此我們使用多線程的方式來(lái)運(yùn)行我們的代碼,首先我們來(lái)將CrawlThread改寫(xiě)成線程的方式,核心代碼如下:
public class CrawlerThread implements Runnable {
@Override
public void run() {
while (true) {
WebPage webPage = crawlerService.getUnCrawlPage(); // TODO: 更好的退出機(jī)制 if (webPage == null)
return; // 拿不到url,說(shuō)明沒(méi)有需要爬的url,直接退出 try {
if (fetchHtml(webPage))
parse(webPage);
} catch (Exception e) {}
}
}
}
在CrawlerService中,我們還需要提供一個(gè)啟動(dòng)爬蟲(chóng)的入口:
public void crawl() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(MAX_THREADS);
for(int i = 0; i < MAX_THREADS; i++) {
executorService.execute(new CrawlerThread(this));
}
executorService.shutdown();
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
Ehcache ehcache = cacheManager.getEhcache(cacheName);
ehcache.removeAll();
}
這樣,爬蟲(chóng)的所有核心代碼就搞定了,先運(yùn)行CrawlerService.init()方法初始化爬蟲(chóng)隊(duì)列,之后運(yùn)行CrawlerService.crawl()就能讓我們的爬蟲(chóng)跑起來(lái)啦。
提供WEB應(yīng)用
之前我們提到,我們還要使用Spring MVC,通過(guò)Spring MVC,我們就能很方便的提供爬蟲(chóng)管理的API啦。更深入地學(xué)習(xí)Spring MVC,請(qǐng)大家參考Spring MVC實(shí)戰(zhàn)入門(mén)訓(xùn)練。
@RestController
public class CrawlerController {
@Autowired
private CrawlerService crawlerService;
@Value("${auth.key}")
private String key;
@ModelAttribute
public void AuthConfig(@RequestParam String auth) throws AccessException {
if(!key.equals(auth)) {
throw new AccessException("auth failed");
}
}
@GetMapping("/init")
public void init() {
crawlerService.init();
}
@GetMapping("/crawl")
public void crawl() throws InterruptedException {
crawlerService.crawl();
}
}
最后,我們將所有爬取到的音樂(lè)通過(guò)頁(yè)面展示出來(lái):
@Controller
public class SongController {
@Autowired SongRepository songRepository;
@GetMapping("/songs")
public String songs(Model model,
@PageableDefault(size = 100, sort = "commentCount", direction = Sort.Direction.DESC) Pageable pageable) {
model.addAttribute("songs", songRepository.findAll(pageable));
return "songs";
}
}
這樣,我們的整個(gè)爬蟲(chóng)就完成了,整個(gè)應(yīng)用是通過(guò)Spring Boot運(yùn)行的,感興趣的話可以參考Spring Boot——開(kāi)發(fā)新一代Spring Java應(yīng)用。
后續(xù)
爬取效率
爬蟲(chóng)爬了兩天后,一共爬到了573945條數(shù)據(jù),此時(shí)數(shù)據(jù)庫(kù)訪問(wèn)速度已經(jīng)變成龜速... 事實(shí)證明,對(duì)于大型爬蟲(chóng)而言,這樣簡(jiǎn)單粗暴的將數(shù)據(jù)庫(kù)作為爬蟲(chóng)隊(duì)列是不科學(xué)的,簡(jiǎn)單想了一下,我們可以用下列方式來(lái)優(yōu)化爬蟲(chóng)的效率:將webpage表分拆成playlist、album、song三張表,按照數(shù)據(jù)順序先爬playlist,再爬album,最后再爬song(甚至將song拆成多張表)
由于網(wǎng)易云音樂(lè)的各種對(duì)象都有id,將id作為索引,提高mysql的效率
獲取url的時(shí)候按照id從小到大獲取,獲取完一條刪除一條
既然mysql達(dá)不到我們的要求,可以考慮直接將mysql替換掉,使用redis作為爬蟲(chóng)隊(duì)列
優(yōu)化的方式有很多種,有些可以借助工具來(lái)實(shí)現(xiàn),有些需要考慮具體的業(yè)務(wù)邏輯。這里我們不具體實(shí)現(xiàn),感興趣的同學(xué)可以自行實(shí)現(xiàn),看看如何優(yōu)化可以達(dá)到最大的效率。
音樂(lè)頁(yè)面訪問(wèn)效率
數(shù)據(jù)量大了之后,影響的不僅僅是爬蟲(chóng)爬的效率,當(dāng)然還有訪問(wèn)音樂(lè)列表的速度,隨意訪問(wèn)一個(gè)頁(yè)面都需要4秒左右。最后,我通過(guò)緩存解決了這個(gè)問(wèn)題,具體實(shí)現(xiàn)我們也不多講了,可以參考文章基于Spring的緩存。加上緩存之后頁(yè)面訪問(wèn)速度達(dá)到了100ms左右。
數(shù)據(jù)更新
除了爬蟲(chóng)的爬取效率外,還有一個(gè)很重要環(huán)節(jié),就是數(shù)據(jù)的更新,評(píng)論數(shù)據(jù)是每天都會(huì)變化的,我們的數(shù)據(jù)當(dāng)然也要每天更新。這里,我們使用最簡(jiǎn)單粗暴的方式,建立一個(gè)定時(shí)任務(wù)(有關(guān)定時(shí)任務(wù)可以參考基于Spring Boot的定時(shí)任務(wù)),在每天的凌晨1點(diǎn),找到評(píng)論數(shù)量大于5000的歌曲,將其狀態(tài)設(shè)置為uncrawl(未爬),啟動(dòng)爬蟲(chóng)即可:
@GetMapping("/update")
@Scheduled(cron = "0 1 0 * * ?")
public void update() throws InterruptedException {
crawlerService.update();
}
@Async
public void update() throws InterruptedException {
List webPages = songRepository.findByCommentCountGreaterThan(5000L);
webPages.forEach(s -> {
WebPage p = webPageRepository.findOne(s.getUrl());
p.setStatus(Status.uncrawl);
webPageRepository.save(p);
});
crawl();
}
整個(gè)站點(diǎn)是用Spring MVC假設(shè)的,學(xué)習(xí)Spring MVC,請(qǐng)大家參考和Spring MVC實(shí)戰(zhàn)入門(mén)訓(xùn)練和Spring MVC的入門(mén)實(shí)例。
希望進(jìn)一步深入了解的同學(xué)請(qǐng)參考一起來(lái)寫(xiě)網(wǎng)易云音樂(lè)Java爬蟲(chóng)
進(jìn)一步閱讀
更深入地學(xué)習(xí)Spring MVC,請(qǐng)大家參考Spring MVC實(shí)戰(zhàn)入門(mén)訓(xùn)練。
歡迎關(guān)注天碼營(yíng)微信公眾號(hào): TMY-EDU
小編重點(diǎn)推薦:
更多精彩內(nèi)容請(qǐng)?jiān)L問(wèn)天碼營(yíng)網(wǎng)站
總結(jié)
以上是生活随笔為你收集整理的网易云音乐java爬虫_用Java实现网易云音乐爬虫的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: web文件加密
- 下一篇: java美元兑换,(Java实现) 美元