最近在知乎上看到一個話題,說使用爬蟲技術獲取網易雲音樂上的歌曲,甚至還包括付費的歌曲,哥瞬間心動了,這年頭,好聽的流行音樂或者經典老歌都開始收費了,只能聽不能下載,着實很郁悶,現在機會來了,於是開始研究爬蟲技術,翻閱各種資料,最終選擇網友們一致認為比較好用的webcollector框架來實現。
首先,我們來認識一下webcollector,webcollector是一個無需配置,便於二次開發的爬蟲框架,它提供精簡的API,只需少量代碼即可實現一個功能強大的爬蟲,webcollector+hadoop是webcollector的hadoop版本,支持分布式爬取。並且在2.x版本中提供了selenium,可以處理javaScript生成的數據。我們邊說便看圖,看圖說話,便於理解:

以上就是webcollector的架構圖,我們來簡單分析一下:
- CrawlDB: 任務數據庫,爬蟲的爬取任務(類似URL列表)是存放在CrawlDB中的,CrawlDB根據DbUpdater和Generator所選插件不同,可以有多種形式,如文件、Redis、MySQL、MongoDB等。
- 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: 用來選擇不同的鏈接分析器(插件)。爬蟲之所以可以從一個網頁開始,向多個網頁不斷地爬取,就是因為它在不斷的解析已知網頁中的鏈接,來發現新的未知網頁,然后對新的網頁進行同樣的操作。
爬取邏輯:

第一層:爬取一個網頁,http://www.apache.org/,解析網頁,獲取3個鏈接,將3個鏈接保存到CrawlDB中,設置狀態為未爬取。同時將http://www.apache.org/的爬取狀態設置為已爬取。結束第一輪。
第二層,找到CrawlDB中狀態為未爬取的頁面(第一層解析出來的3個鏈接),分別爬取,並解析網頁,一共獲得8個鏈接。和第一層操作一樣,將解析出的鏈接放入CrawlDB,設置為未爬取,並將第二層爬取的三個頁面,狀態設置為已爬取。
第三層,找到CrawlDB中狀態為未爬取的頁面(第二層解析出來的8個鏈接)……………..
每一層都可以作為一個獨立的任務去運行,所以可以將一個大型的廣度遍歷任務,拆分成一個一個小任務。爬蟲里有個參數,設置爬取的層數,指的就是這個。
插件機制:
框架圖中的 Injector、Generator、Request(由RequestFactory生成)、Parser(由ParserFactory生成)、DbUpdater、Response都是以插件實現的。制作插件往往只需要自定義一個實現相關接口的類,並在相關Factory內指定即可。
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); } }
我們來簡單分析一下:
-
visit()方法
在整個抓取過程中,只要抓到一個復合的頁面,wc都會回調該方法,並傳入一個包含了所有頁面信息的page對象。
-
addSeed()
添加種子,種子鏈接會在爬蟲啟動之前加入到上面所說的抓取信息中並標記為未抓取狀態.這個過程稱為注入。
-
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結尾的鏈接。
-
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中。
3、新建一個類繼承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
