使用webcollector爬蟲技術獲取網易雲音樂所有歌曲

最近在知乎上看到一個話題,說使用爬蟲技術獲取網易雲音樂上的歌曲,甚至還包括付費的歌曲,哥瞬間心動了,這年頭,好聽的流行音樂或者經典老歌都開始收費了,只能聽不能下載,着實很鬱悶,如今機會來了,因而開始研究爬蟲技術,翻閱各類資料,最終選擇網友們一致認爲比較好用的webcollector框架來實現。css

首先,咱們來認識一下webcollector,webcollector是一個無需配置,便於二次開發的爬蟲框架,它提供精簡的API,只需少許代碼便可實現一個功能強大的爬蟲,webcollector+hadoop是webcollector的hadoop版本,支持分佈式爬取。而且在2.x版本中提供了selenium,能夠處理javaScript生成的數據。咱們邊說便看圖,看圖說話,便於理解:html

 

以上就是webcollector的架構圖,咱們來簡單分析一下:java

  • CrawlDB: 任務數據庫,爬蟲的爬取任務(相似URL列表)是存放在CrawlDB中的,CrawlDB根據DbUpdater和Generator所選插件不一樣,能夠有多種形式,如文件、RedisMySQLMongoDB等。
  • Injector: 種子注入器,負責第一輪爬取時,向CrawlDB中提交爬取任務。在斷點續爬的時候,不須要經過Injector向CrawlDB注入種子,由於CrawlDB中已有爬取任務
  • Generator: 任務生成器,任務生成器從CrawlDB獲取爬取任務,並進行過濾(正則、爬取間隔等),將任務提交給抓取器。
  • Fetcher: 抓取器,Fetcher是爬蟲最核心的模塊,Fetcher負責從Generator中獲取爬取任務,用線程池來執行爬取任務,並對爬取的網頁進行連接解析,將連接信息更新到CrawlDB中,做爲下一輪的爬取任務。在網頁被爬取成功/失敗的時候,Fetcher會將網頁和相關信息以消息的形式,發送到Handler的用戶自定義模塊,讓用戶本身處理網頁內容(抽取、存儲)。
  • DbUpdater: 任務更新器,用來更新任務的狀態和加入新的任務,網頁爬取成功後須要更新CrawlDB中的狀態,對網頁作解析,發現新的鏈接,也須要更新CrawlDB。
  • Handler: 消息發送/處理器,Fetcher利用Handler把網頁信息打包,發送到用戶自定義操做模塊。
  • User Defined Operation: 用戶自定義的對網頁信息進行處理的模塊,例如網頁抽取、存儲。爬蟲二次開發主要就是自定義User Defined Operation這個模塊。實際上User Defined Operation也是在Handler裏定義的。
  • RequestFactory: Http請求生成器,經過RequestFactory來選擇不一樣的插件,來生成Http請求,例如能夠經過httpclient插件來使用httpclient做爲爬蟲的http請求,或者來使用可模擬登錄新浪微博的插件,來發送爬取新浪微博的http請求。 
  • ParserFactory: 用來選擇不一樣的連接分析器(插件)。爬蟲之因此能夠從一個網頁開始,向多個網頁不斷地爬取,就是由於它在不斷的解析已知網頁中的連接,來發現新的未知網頁,而後對新的網頁進行一樣的操做。

爬取邏輯:git

第一層爬取一個網頁,http://www.apache.org/,解析網頁,獲取3個連接,將3個連接保存到CrawlDB中,設置狀態爲未爬取。同時將http://www.apache.org/的爬取狀態設置爲已爬取。結束第一輪。github

第二層找到CrawlDB中狀態爲未爬取的頁面(第一層解析出來的3個連接),分別爬取,並解析網頁,一共得到8個連接。和第一層操做同樣,將解析出的連接放入CrawlDB,設置爲未爬取,並將第二層爬取的三個頁面,狀態設置爲已爬取。web

第三層找到CrawlDB中狀態爲未爬取的頁面(第二層解析出來的8個連接)…………….. 正則表達式

每一層均可以做爲一個獨立的任務去運行,因此能夠將一個大型的廣度遍歷任務,拆分紅一個一個小任務。爬蟲裏有個參數,設置爬取的層數,指的就是這個。redis

插件機制:數據庫

框架圖中的 Injector、Generator、Request(由RequestFactory生成)、Parser(由ParserFactory生成)、DbUpdater、Response都是以插件實現的。製做插件每每只須要自定義一個實現相關接口的類,並在相關Factory內指定便可。apache

WebCollector內置了一套插件(cn.edu.hfut.dmic.webcollector.plugin.redis)。基於這套插件,能夠把WebCollector的任務管理放到redis數據庫上,這使得WebCollector能夠爬取海量的數據(上億級別)。

對於用戶來講,關注的更多的不是爬蟲的爬取流程,而是對每一個網頁要進行什麼樣的操做。對網頁進行抽取、保存仍是其餘操做,應該是由用戶自定義的。

因此咱們使用WebCollector來寫爬蟲不用那麼麻煩,只用集成爬蟲框架裏的BreadthCrawler類並重寫visit方法便可,咱們先來看下官網爬取知乎的例子:

 

  /*visit函數定製訪問每一個頁面時所需進行的操做*/
    @Override
    public void visit(Page page) {
        String question_regex="^http://www.zhihu.com/question/[0-9]+";
        if(Pattern.matches(question_regex, page.getUrl())){
            System.out.println("正在抽取"+page.getUrl());
            /*抽取標題*/
            String title=page.getDoc().title();
            System.out.println(title);
            /*抽取提問內容*/
            String question=page.getDoc().select("div[id=zh-question-detail]").text();
            System.out.println(question);

        }
    }

    /*啓動爬蟲*/
    public static void main(String[] args) throws IOException{  
        ZhihuCrawler crawler=new ZhihuCrawler();
        crawler.addSeed("http://www.zhihu.com/question/21003086");
        crawler.addRegex("http://www.zhihu.com/.*");
        crawler.start(5);  
    }

}

 

咱們來簡單分析一下:

  1. visit()方法

    在整個抓取過程當中,只要抓到一個複合的頁面,wc都會回調該方法,並傳入一個包含了全部頁面信息的page對象。

  2. addSeed()

    添加種子,種子連接會在爬蟲啓動以前加入到上面所說的抓取信息中並標記爲未抓取狀態.這個過程稱爲注入。

  3. addRegex

    爲一個url正則表達式, 過濾沒必要抓取的連接好比.js .jpg .css等,或者指定抓取連接的規則。好比我使用時有個正則爲:http://news.hexun.com/2015-01-16/[0-9]+.html, 那麼個人爬蟲則只會抓取http://news.hexun.com/2015-01-16/172431075.html,http://news.hexun.com/2015-01-16/172429627.html 等news.hexun.com域名下2015-01-16日期的.html結尾的連接。

  4. start()

    表示啓動爬蟲,傳入參數5表示抓取5層(深度爲5),這個深度爲5怎麼理解呢,當只添加了一個種子, 抓這個種子連接爲第1層, 解析種子連接頁面跟據正則過濾想要的連接保存至待抓取記錄. 那麼第2層就是抓取1層保存的記錄並解析保存新記錄,依次類推。

至此,咱們已經對webcollector有了一個大體的瞭解,更深刻的理論知識咱們就再也不往下追究,畢竟高端的東西是須要更恆久的毅力和耐心去不斷挖掘的,而目前咱們只須要掌握簡單的應用便可實現一個爬蟲。

(一)需求分析:

OK,那咱們先來分析一下咱們這次的需求,咱們要使用webcollector爬蟲技術獲取網易雲音樂所有歌曲,咱們先來看下一個網易雲音樂的歌曲頁面連接:http://music.163.com/#/album?id=2884361,咱們會發現這個連接後面帶有參數,傳不一樣的id,能夠獲得不一樣的歌曲,因此,這就是一個模版,咱們能夠遍歷整個網易雲音樂,把其中url與上面相似的網頁提取出來就能夠獲得網易雲音樂的全部歌曲了,對吧?

那麼,第二個問題,咱們如何獲取音樂的真實地址呢?這個一般是要用到抓包工具的,經過抓包工具獲取HTTP請求下的頭信息,從而獲得請求的真實路徑,咱們經過抓包分析獲得網易雲音樂有一個api接口,能夠獲得歌曲的真實地址,api地址:http://music.163.com/api/song/detail,咱們發現這個接口有幾個參數:

  • id 傳入上面獲得的歌曲的id

  • ids ids是由id拼接而成的,ids = '%5B+' + id + '%5D'(這裏的%5B...%5d是js傳參的時候防止亂碼加的,這個在以前的項目裏有遇到過)

而後咱們能夠把上面的API複製進瀏覽器,咱們會獲得一段json,裏面有歌曲的音頻源地址。

 

好了,通過分析,咱們已經準備好了咱們須要的東西,接下來就能夠開始動手操做了。

 

(二)開發

開發就相對很簡單,沒有太多的類,只是普通的Java工程,引入相應的jar包便可。

1.進入WebCollector官方網站下載最新版本所需jar包。最新版本的jar包放在webcollector-version-bin.zip中。

2.打開Eclipse,選擇File->New->Java Project,按照正常步驟新建一個Java項目。

在工程根目錄下新建一個文件夾lib,將剛下載的webcollector-version-bin.zip解壓後獲得的全部jar包放到lib文件夾下。將jar包引入到build path中。

三、新建一個類繼承BreadthCrawler,重寫visit方法進行url的正則匹配,抽取出url,歌曲Id,歌曲名稱,演唱者,url。以及真實路徑。過程很簡單,咱們直接看代碼:

package com.ax.myBug;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.csvreader.CsvWriter;

import cn.edu.hfut.dmic.webcollector.model.CrawlDatums;
import cn.edu.hfut.dmic.webcollector.model.Page;
import cn.edu.hfut.dmic.webcollector.plugin.berkeley.BreadthCrawler;
import org.json.*;  

/**
 * 獲取網易雲音樂全部歌曲寫入csv文件
 * @author AoXiang
 */
public class GetAllSongs extends BreadthCrawler {
    
    private CsvWriter r = null;
    
    public void closeCsv() {
        this.r.close();
    }
    /**
     * 轉換字節流
     * @param instream
     * @return
     * @throws IOException
     */
    public static byte[] readInputStream(InputStream instream) throws IOException {
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();  
        byte[]  buffer = new byte[1204];  
        int len = 0;  
        while ((len = instream.read(buffer)) != -1){  
            outStream.write(buffer,0,len);  
        }  
        instream.close();  
        return outStream.toByteArray(); 
    }
    /**
     * 根據URL得到網頁源碼
     * @param url 傳入的URL
     * @return String
     * @throws IOException 
     */
    public static String getURLSource(URL url) throws IOException {
        HttpURLConnection conn = (HttpURLConnection)url.openConnection();
        conn.setRequestMethod("GET");
        conn.setConnectTimeout(500000);//時間能夠設置的久一點,若是控制檯常常提示read time out
        InputStream inStream = conn.getInputStream();
        byte[] data = readInputStream(inStream);
        String htmlSource = new String(data);
        return htmlSource;
    }
    /**
     * 重寫構造函數
     * @param crawlPath 爬蟲路徑
     * @param autoParse 是否自動解析
     */
    public GetAllSongs(String crawlPath, boolean autoParse) throws FileNotFoundException {
        super(crawlPath, autoParse);
        // 逗號進行分割,字符編碼爲GBK
        this.r = new CsvWriter("songId.csv", ',', Charset.forName("GBK"));        
    }
    @Override
    public void visit(Page page, CrawlDatums next) {
        // 繼承覆蓋visit方法,該方法表示在每一個頁面進行的操做
        // 參數page和next分別表示當前頁面和下個URL對象的地址
        // 生成文件songId.csv,第一列爲歌曲id,第二列爲歌曲名字,第三列爲演唱者,第四列爲歌曲信息的URL
        // 網易雲音樂song頁面URL地址正則
        String song_regex = "^http://music.163.com/song\\?id=[0-9]+";
        // 建立Pattern對象                          http://music.163.com/#/song?id=110411
        Pattern songIdPattern = Pattern.compile("^http://music.163.com/song\\?id=([0-9]+)");
        Pattern songInfoPattern = Pattern.compile("(.*?)-(.*?)-");
        // 對頁面進行正則判斷,若是有的話,將歌曲的id和網頁標題提取出來,不然不進行任何操做
        if (Pattern.matches(song_regex, page.getUrl())) {
            // 將網頁的URL和網頁標題提取出來,網頁標題格式:歌曲名字-歌手-網易雲音樂
            String url = page.getUrl();
            @SuppressWarnings("deprecation")
            String title = page.getDoc().title();
            String songName = null;
            String songSinger = null;
            String songId = null;
            String infoUrl = null;
            String mp3Url = null;
            // 對標題進行歌曲名字、歌手解析
            Matcher infoMatcher = songInfoPattern.matcher(title);
            if (infoMatcher.find()) {
                songName = infoMatcher.group(1);
                songSinger = infoMatcher.group(2); 
            }
            System.out.println("正在抽取:" + url);
            // 建立Matcher對象,使用正則找出歌曲對應id
            Matcher idMatcher = songIdPattern.matcher(url);
            if (idMatcher.find()) {
                songId = idMatcher.group(1);
            }
            System.out.println("歌曲:" + songName);
            System.out.println("演唱者:" + songSinger);
            System.out.println("ID:" + songId);    
            infoUrl = "http://music.163.com/api/song/detail/?id=" + songId + "&ids=%5B+" + songId + "%5D";
            try {
                URL urlObject = new URL(infoUrl);
                // 獲取json源碼
                String urlsource = getURLSource(urlObject);
                JSONObject j = new JSONObject(urlsource);
                JSONArray a = (JSONArray) j.get("songs");
                JSONObject aa = (JSONObject) a.get(0);
                mp3Url = aa.get("mp3Url").toString();                
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            String[] contents = {songId, songName, songSinger, url, mp3Url};
            try {
                this.r.writeRecord(contents);
                this.r.flush();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    /**
     * 歌曲id爬蟲開始
     * @param args
     * @throws Exception 
     */
    public static void main(String[] args) throws Exception {
        URL url = new URL("http://music.163.com/api/song/detail/?id=110411&ids=%5B110411%5D");
        String urlsource = getURLSource(url);
        System.out.println(urlsource);
        JSONObject j = new JSONObject(urlsource);
        JSONArray a = (JSONArray) j.get("songs");
        JSONObject aa = (JSONObject) a.get(0);
        System.out.println(aa.get("mp3Url"));
        GetAllSongs crawler = new GetAllSongs("crawler", true);
        // 添加初始種子頁面http://music.163.com
        crawler.addSeed("http://music.163.com/#/album?id=604667405");
        // 設置採集規則爲全部類型的網頁
        crawler.addRegex("http://music.163.com/.*");
        // 設置爬取URL數量的上限
        crawler.setTopN(500000000);
        // 設置線程數
        crawler.setThreads(30);
        // 設置斷點採集
        crawler.setResumable(false);
        // 設置爬蟲深度
        crawler.start(5);
    }
}

 

(三)測試

直接運行Java程序,查看控制檯

而後去到咱們的workSpace,咱們會發現提取出的歌曲信息已經寫入了csv文件,

 

咱們打開文件,能夠看到裏面已經拿到了咱們想要的數據

 

OK,通過一番折騰,咱們已經大功告成了,是否是很簡單呢?固然,學習的過程也是很曲折的,有了這個技術,咱們不只能夠爬取網易雲音樂,還能夠爬取各種新聞網站,拿到他們的數據來爲咱們本身所用,固然,如今的不少網站在安全方面都作的至關不錯,或者比較摳門,不肯意資源共享,採用了反爬機制,因此,咱們要作的就是更深刻的更全面的瞭解爬蟲技術,掌握其中的要領和精髓,靈活運用,那麼,我相信再密不透風的網站咱們也能爬的進去。由於咱們的目標是星辰大海!

附上項目源碼以及已經爬取的17萬多的網易雲音樂歌曲Excel:https://git.oschina.net/AuSiang/myBug/attach_files

相關文章
相關標籤/搜索