最近在學習Java爬蟲,發現了webmagic輕量級框架,在網上搜索了一些教程,而後本身嘗試着寫了一個對菜鳥教程的爬蟲,主要功能爲把教程內容html轉換爲markdown文本,方便離線閱讀;
作這個工具的主要緣由是,咱們單位的工做環境通常要求斷網,菜鳥教程上的教學做爲入門通常不錯,爲了方便離線學習,作了這個應用;如今寫了主要爲了分享和本身學習總結;
第一次寫博文,不完善的地方請見諒css
關於 **WebMagic**,我就不做介紹了,主頁傳送門 -> WebMagic
Maven依賴html
<dependency> <groupId>us.codecraft</groupId> <artifactId>webmagic-core</artifactId> <version>0.7.1</version> </dependency> <dependency> <groupId>us.codecraft</groupId> <artifactId>webmagic-extension</artifactId> <version>0.7.1</version> </dependency>
中文文檔 -> http://webmagic.io/docs/zh/ java
由於用到了lambda表達式,jdk版本要求1.8+,IDE使用IDEAnode
----
寫個介紹真辛苦,下面進入項目git
建立項目,導入jar包(略)github
主要內容結構如圖 web
Controller - 控制器,Main方法入口數據庫
MarkdownSavePipeline - 持久化組件-保存爲文件markdown
RunoobPageProcessor - 頁面解析組件多線程
Service - 服務提供組件,至關於Utils,主要用於包裝通用方法
這裏選取的是Scala教程做爲樣板
import us.codecraft.webmagic.Spider; /** * 爬蟲控制器,main方法入口 * Created by bekey on 2017/6/6. */ public class Controller { public static void main(String[] args) { // String url = "http://www.runoob.com/regexp/regexp-tutorial.html"; String url = "http://www.runoob.com/scala/scala-tutorial.html"; //爬蟲控制器 添加頁面解析 添加url(request) 添加持久化組件 建立線程 執行 Spider.create(new RunoobPageProcessor()).addUrl(url).addPipeline(new MarkdownSavePipeline()).thread(1).run(); } }
WebMagic 中主要有四大組件
通常Downloader和Scheduler不須要定製
流程核心控制引擎 -- Spider ,用來自由配置爬蟲,建立/啓動/中止/多線程等
import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import us.codecraft.webmagic.Page; import us.codecraft.webmagic.Site; import us.codecraft.webmagic.processor.PageProcessor; import us.codecraft.webmagic.selector.Html; /** * 菜鳥教程markdown轉換 * Created by bekey on 2017/6/6. */ public class RunoobPageProcessor implements PageProcessor{ private static String name = null; private static String regex = null; // 抓取網站的相關配置,包括編碼、重試次數、抓取間隔、超時時間、請求消息頭、UA信息等 private Site site= Site.me().setRetryTimes(3).setSleepTime(1000).setTimeOut(3000).addHeader("Accept-Encoding", "/") .setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36"); @Override public Site getSite() { return site; } @Override //此處爲處理函數 public void process(Page page) { Html html = page.getHtml(); // String name = page.getUrl().toString().substring(); if(name == null ||regex == null){ String url = page.getRequest().getUrl(); name = url.substring(url.lastIndexOf('/',url.lastIndexOf('/')-1)+1,url.lastIndexOf('/')); regex = "http://www.runoob.com/"+name+"/.*"; } //添加訪問 page.addTargetRequests(html.links().regex(regex).all()); //獲取文章主內容 Document doc = html.getDocument(); Element article = doc.getElementById("content"); //獲取markdown文本 String document = Service.markdown(article); //處理保存操做 String fileName = article.getElementsByTag("h1").get(0).text().replace("/","").replace("\\","") + ".md"; page.putField("fileName",fileName); page.putField("content",document); page.putField("dir",name); } }
通常爬蟲最重要的就是解析,因此必須建立解析器實現PageProcessor接口,
PageProcessor接口有兩個方法
屬性設置有不少,能夠本身嘗試,固然抓取間隔不要過短,不然會給目標網站帶來很大負擔,特別注意
addHeader -- 添加消息頭;最基本的反反爬蟲手段;
Html html = page.getHtml(); // String name = page.getUrl().toString().substring(); if(name == null ||regex == null){ String url = page.getRequest().getUrl(); name = url.substring(url.lastIndexOf('/',url.lastIndexOf('/')-1)+1,url.lastIndexOf('/')); regex = "http://www.runoob.com/"+name+"/.*"; } //添加訪問 page.addTargetRequests(html.links().regex(regex).all());
這段,主要是連接處理;在Controller中,Spider通常有一個入口request,可是不是每發送一個請求就要建立一個Spider(不然要多線程幹什麼囧);
經過page.addTargetRequests 及其餘重載方法能夠很輕鬆地添加請求,請求會放進Scheduler並去重,根據Sleeptime間隔時間訪問
links() 方法是Selectable接口的抽象方法,能夠提取頁面上的連接,由於是要爬取整個教程,因此用正則提取正確的連接,放入Scheduler;
Selectable 相關的抽取元素鏈式API是WebMagic的一個核心功能。使用Selectable接口,能夠直接完成頁面元素的鏈式抽取,也無需去關心抽取的細節。 主要提供 xpath(Xpath選擇器) / $(css選擇器) / regex(正則抽取) /replace(替換)/links(獲取連接) 等方法,不過我不太會用,因此後面頁面解析主要仍是使用Jsoup實現
WebMagic PageProcessor 中解析頁面主要就是使用Jsoup實現的,Jsoup是一款優秀的頁面解析器,具體使用請看官方文檔 http://www.open-open.com/jsoup/
//獲取文章主內容 Document doc = html.getDocument(); Element article = doc.getElementById("content");
page 和 jsoup的轉換 經過getDocument實現,這裏的Document類,import org.jsoup.nodes.Document
經過頁面結構,咱們能夠很輕易地發現,教程主要內容都藏在id爲content的div裏,拿出來
//獲取markdown文本 String document = Service.markdown(article);
經過靜態方法拿到markdown文本,看一下具體實現,Service類
/** * 公有方法,將body解析爲markdown文本 * @param article #content內容 * @return markdown文本 */ public static String markdown(Element article){ StringBuilder markdown = new StringBuilder(""); article.children().forEach(it ->parseEle(markdown, it, 0)); return markdown.toString(); } /** * 私有方法,解析單個元素並向StringBuilder添加 */ private static void parseEle(StringBuilder markdown,Element ele,int level){ //處理相對地址爲絕對地址 ele.getElementsByTag("a").forEach(it -> it.attr("href",it.absUrl("href"))); ele.getElementsByTag("img").forEach(it -> it.attr("src",it.absUrl("src"))); //先判斷class,再斷定nodeName String className = ele.className(); if(className.contains("example_code")){ String code = ele.html().replace(" "," ").replace("<br>",""); markdown.append("```\n").append(code).append("\n```\n"); return; } String nodeName = ele.nodeName(); //獲取到每一個nodes,根據class和標籤進行分類處理,轉化爲markdown文檔 if(nodeName.startsWith("h") && !nodeName.equals("hr")){ int repeat = Integer.parseInt(nodeName.substring(1)) + level; markdown.append(repeat("#", repeat)).append(' ').append(ele.text()); }else if(nodeName.equals("p")){ markdown.append(ele.html()).append(" "); }else if(nodeName.equals("div")){ ele.children().forEach(it -> parseEle(markdown, it, level + 1)); }else if(nodeName.equals("img")) { ele.removeAttr("class").removeAttr("alt"); markdown.append(ele.toString()).append(" "); }else if(nodeName.equals("pre")){ markdown.append("```").append("\n").append(ele.html()).append("\n```"); }else if(nodeName.equals("ul")) { markdown.append("\n"); ele.children().forEach(it -> parseEle(markdown, it, level + 1)); }else if(nodeName.equals("li")) { markdown.append("* ").append(ele.html()); } markdown.append("\n"); } private static String repeat(String chars,int repeat){ String a = ""; if(repeat > 6) repeat = 6; for(int i = 0;i<=repeat;i++){ a += chars; } return a; }
不得不說,java8的lambda表達式太好使了,讓java居然有了腳本的感受(雖然其餘不少語言已經實現好久了)
這裏是具體的業務實現,沒有什麼好特別講解的,就是根據規則一點點作苦力;我這裏主要依靠class 和 nodeName 把html轉爲markdown,處理得不算很完善吧,具體實現能夠慢慢改進~
須要注意的是,這裏的Element對象,都是來自於Jsoup框架,使用起來頗有JavaScript的感受,若是你常使用js,對這些方法名應該都挺了解的,就不詳細講了;若是Element裏屬性有鏈接,經過absUrl(String attrName)能夠很方便得得到絕對連接地址;
回到process函數
//處理保存操做 String fileName = article.getElementsByTag("h1").get(0).text().replace("/","").replace("\\","") + ".md"; page.putField("fileName",fileName); page.putField("content",document); page.putField("dir",name);
再獲得文本後,咱們就能夠對文本進行持久化處理;事實上,咱們能夠不借助Pieline組件進行持久化,可是基於模塊分離,以及更好的複用/擴展,實現一個持久化組件也是有必要的(假如你不只僅須要一個爬蟲)
這裏,page.putField 方法,其實是講內容放入一個 ResultItems 的Map組件中,它負責保存PageProcessor處理的結果,供Pipeline使用. 它的API與Map很相似,但包裝了其餘一些有用的信息,值得注意的是它有一個字段,skip,page中能夠經過page.setSkip(true)方法,使得頁面沒必要持久化
/** * 保存文件功能 * Created by bekey on 2017/6/6. */ public class MarkdownSavePipeline implements Pipeline { @Override public void process(ResultItems resultItems, Task task) { try { String fileName = resultItems.get("fileName"); String document = resultItems.get("content"); String dir = resultItems.get("dir"); Service.saveFile(document,fileName,dir); }catch (IOException e){ e.printStackTrace(); } } }
Pipeline接口,一樣要實現一個
public void process(ResultItems resultItems, Task task) 方法,處理持久化操做
ResultItems 已經介紹過了,裏面除了有你page中保存的內容外,還提供了getRequest()方法,獲取本次操做的Request對象,和一個getAll()的封裝方法,給你迭代;
Task對象提供了兩個方法
沒有使用過,可是看方法名大概能知道是作什麼的;
Serivice.saveFile 是我本身簡單封裝的保存文件方法,在src同級目錄建立以教程命名的文件夾,以每一個頁面標題爲文件名建立.md文件.簡單的IO操做,就不貼出來;
特別注意的是WebMagic框架會在底層catch異常,可是卻不會報錯,因此開發調試的時候,若是要捕獲異常的話,須要本身try catch ,特別是那些RuntimeException
囉囉嗦嗦打了好多,完整代碼下載,個人GitHub
https://github.com/BekeyChao/HelloWorld/tree/master/src
由於沒有用git管理(主要有一些其餘內容),因此是手動同步的,若是運行不起來,就好好研究吧~