网易云音乐java爬虫_用Java实现网易云音乐爬虫
起因
前兩天在知乎上看到一個帖子《網易云音樂有哪些評論過萬的歌曲?》,一時技癢,用Java實現了一個簡單的爬蟲,這里簡單記錄一下。
最終的結果開放出來了,大家可以隨意訪問,請戳這里>>>>>> 網易云音樂爬蟲結果。
爬蟲簡介
網絡爬蟲是一種按照一定的規則,自動地抓取萬維網信息的程序或者腳本,一個通用的網絡爬蟲大致包含以下幾個步驟:
網絡爬蟲的大致流程如上圖所示,無論你是做什么樣的爬蟲應用,整體流程都是大同小異?,F在,我們就根據網易云音樂來定制一個專門爬取音樂評論數量的特定網絡爬蟲。
前期準備
網頁類型分析
首先,我們需要對網易云音樂整個網站有個大致的了解,進入網易云音樂首頁,瀏覽后發現其大概有這么幾種類型的URL:推薦頁面
排行榜列表以及排行榜頁面
歌單列表以及歌單頁面
主播電臺列表以及主播電臺頁面
歌手列表以及歌手頁面
專輯列表(新碟上架)以及專輯頁面
歌曲頁面
我們最終需要爬取的數據在歌曲頁面中,該頁面里包含了歌曲的名稱以及歌曲的評論數量。
另外,我們還需要盡可能多的獲取歌曲頁面,這些信息我們可以從前面6種類型的頁面拿到。其中,歌單列表以及歌單頁面結構最簡單,歌單列表直接分頁就可以拿到。因此,我們選擇歌單頁面作為我們的初始頁面,然后歌單列表--歌單--歌曲一路爬下去即可。
設計數據模型
通過上述分析,我們可以知道我們要做兩件事情,一是爬取頁面歌單列表--歌單--歌曲,二是將最終的結果存儲起來。因此,我們只需要兩個對象,一個用來存儲頁面相關的信息,url、頁面類型、是否被爬過(html和title作為臨時數據存儲),另外一個用來存儲歌曲相關信息,url、歌曲名,評論數。因此,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;
...
}
獲取網頁內容并解析
根據之前的分析,我們需要爬的頁面有三種:歌單列表、歌單以及歌曲。為了驗證想法的可行性,我們先用代碼來解析這三種類型的網頁,我們將網頁內容獲取以及解析的代碼都放入CrawlerThread當中。
獲取html
無論想要從什么網站中拿到數據,獲取其html代碼都是最最基礎的一步,這里我們使用jsoup來獲取頁面信息,在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());
}
運行后即可看到html文本的輸出
解析歌單列表頁面
得到html后,我們來解析歌單列表,拿到頁面中的所有歌單,Jsoup包含了html解析相關的功能,我們無需添加其他依賴,直接在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));
}
解析歌單頁面
和歌單列表頁面類似,只需要將歌曲相關的元素找出來即可:
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));
}
注意,這里為了方便,我們將歌曲的名稱也拿到了,這樣后面我們就不需要再次獲取歌曲名稱了。
解析歌曲頁面
終于到歌曲頁面了,這里網易云音樂做了反爬處理,獲取數據時的參數需要經過加密處理,這里我們不糾結于具體算法,如果有興趣的直接看參考代碼,我們只看關鍵代碼:
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));
}
好吧,獲取過程確實比較曲折,經過了多次的加密,不過不管怎么樣,最終我們還是拿到了我們想要的數據。接下來,就是使用爬蟲將整套機制run起來了。
實現爬蟲
重新回顧一下流程圖,我們發現其中有很重要的一個對象是爬蟲隊列,爬蟲隊列的實現方法有很多種,自己實現,mysql、redis、MongoDB等等都可以滿足我們的需求,不同的選擇會導致我們實現的不一致。
綜合考慮,我們使用Mysql+ Spring Data JPA + Spring MVC來跑我們的整套框架,最終還可以將爬下來的數據通過web服務展現出來。更深入地學習Spring MVC,請大家參考Spring MVC實戰入門訓練。
確定好之后,我們就可以開始一步步實現了。這里Spring Data JPA的代碼就不展示了。了解Spring Data JPA,請參考Spring Data JPA實戰入門訓練。直接上核心代碼,所有和爬蟲整體流程相關的代碼我們都放進CrawlerService中。
初始網址
第一步建立一個初始網址,我們可以根據歌單列表分頁的特征得到:
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("華語");
init("歐美");
init("日語");
init("韓語");
init("粵語");
init("小語種");
init("流行");
init("搖滾");
init("民謠");
init("電子");
init("舞曲");
init("說唱");
init("輕音樂");
init("爵士");
init("鄉村");
init("R&B/Soul");
init("古典");
init("民族");
init("英倫");
init("金屬");
init("朋克");
init("藍調");
init("雷鬼");
init("世界音樂");
init("拉丁");
init("另類/獨立");
init("New Age");
init("古風");
init("后搖");
init("Bossa Nova");
init("清晨");
init("夜晚");
init("學習");
init("工作");
init("午休");
init("下午茶");
init("地鐵");
init("駕車");
init("運動");
init("旅行");
init("散步");
init("酒吧");
init("懷舊");
init("清新");
init("浪漫");
init("性感");
init("傷感");
init("治愈");
init("放松");
init("孤獨");
init("感動");
init("興奮");
init("快樂");
init("安靜");
init("思念");
init("影視原聲");
init("ACG");
init("校園");
init("游戲");
init("70后");
init("80后");
init("90后");
init("網絡歌曲");
init("KTV");
init("經典");
init("翻唱");
init("吉他");
init("鋼琴");
init("器樂");
init("兒童");
init("榜單");
init("00后");
}
這里,我們初始化了歌單所有分類的列表,通過這些列表,我們就能拿到網易云音樂大部分的歌曲。
從爬蟲隊列中拿到一個URL
這里的邏輯非常簡單,從mysql中獲取一個狀態為未爬的網頁即可,但是由于我們需要爬的網址非常的多,肯定要用到多線程,因此需要考慮異步的情況:
public synchronized WebPage getUnCrawlPage() {
WebPage webPage = webPageRepository.findTopByStatus(Status.uncrawl);
webPage.setStatus(Status.crawled);
return webPageRepository.save(webPage);
}
爬取頁面
剛剛說到,我們需要爬取的頁面很多,因此我們使用多線程的方式來運行我們的代碼,首先我們來將CrawlThread改寫成線程的方式,核心代碼如下:
public class CrawlerThread implements Runnable {
@Override
public void run() {
while (true) {
WebPage webPage = crawlerService.getUnCrawlPage(); // TODO: 更好的退出機制 if (webPage == null)
return; // 拿不到url,說明沒有需要爬的url,直接退出 try {
if (fetchHtml(webPage))
parse(webPage);
} catch (Exception e) {}
}
}
}
在CrawlerService中,我們還需要提供一個啟動爬蟲的入口:
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();
}
這樣,爬蟲的所有核心代碼就搞定了,先運行CrawlerService.init()方法初始化爬蟲隊列,之后運行CrawlerService.crawl()就能讓我們的爬蟲跑起來啦。
提供WEB應用
之前我們提到,我們還要使用Spring MVC,通過Spring MVC,我們就能很方便的提供爬蟲管理的API啦。更深入地學習Spring MVC,請大家參考Spring MVC實戰入門訓練。
@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();
}
}
最后,我們將所有爬取到的音樂通過頁面展示出來:
@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";
}
}
這樣,我們的整個爬蟲就完成了,整個應用是通過Spring Boot運行的,感興趣的話可以參考Spring Boot——開發新一代Spring Java應用。
后續
爬取效率
爬蟲爬了兩天后,一共爬到了573945條數據,此時數據庫訪問速度已經變成龜速... 事實證明,對于大型爬蟲而言,這樣簡單粗暴的將數據庫作為爬蟲隊列是不科學的,簡單想了一下,我們可以用下列方式來優化爬蟲的效率:將webpage表分拆成playlist、album、song三張表,按照數據順序先爬playlist,再爬album,最后再爬song(甚至將song拆成多張表)
由于網易云音樂的各種對象都有id,將id作為索引,提高mysql的效率
獲取url的時候按照id從小到大獲取,獲取完一條刪除一條
既然mysql達不到我們的要求,可以考慮直接將mysql替換掉,使用redis作為爬蟲隊列
優化的方式有很多種,有些可以借助工具來實現,有些需要考慮具體的業務邏輯。這里我們不具體實現,感興趣的同學可以自行實現,看看如何優化可以達到最大的效率。
音樂頁面訪問效率
數據量大了之后,影響的不僅僅是爬蟲爬的效率,當然還有訪問音樂列表的速度,隨意訪問一個頁面都需要4秒左右。最后,我通過緩存解決了這個問題,具體實現我們也不多講了,可以參考文章基于Spring的緩存。加上緩存之后頁面訪問速度達到了100ms左右。
數據更新
除了爬蟲的爬取效率外,還有一個很重要環節,就是數據的更新,評論數據是每天都會變化的,我們的數據當然也要每天更新。這里,我們使用最簡單粗暴的方式,建立一個定時任務(有關定時任務可以參考基于Spring Boot的定時任務),在每天的凌晨1點,找到評論數量大于5000的歌曲,將其狀態設置為uncrawl(未爬),啟動爬蟲即可:
@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();
}
整個站點是用Spring MVC假設的,學習Spring MVC,請大家參考和Spring MVC實戰入門訓練和Spring MVC的入門實例。
希望進一步深入了解的同學請參考一起來寫網易云音樂Java爬蟲
進一步閱讀
更深入地學習Spring MVC,請大家參考Spring MVC實戰入門訓練。
歡迎關注天碼營微信公眾號: TMY-EDU
小編重點推薦:
更多精彩內容請訪問天碼營網站
總結
以上是生活随笔為你收集整理的网易云音乐java爬虫_用Java实现网易云音乐爬虫的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: web文件加密
- 下一篇: Sql Server 的基本增删改查语句