webmagic入門-菜鳥教程html to markdown

      最近在學習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 負責下載頁面
  • PageProcessor 負責解析頁面
  • Scheduler 調度URL
  • Pipeline 持久化到文件/數據庫等

通常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接口有兩個方法

  • public Site getSite()   Site 抓取網站的配置,通常能夠設爲靜態屬性
  • public void process(Page page) 頁面處理函數 , 其中Page 表明了從Downloader下載到的一個頁面——多是HTML,也多是JSON或者其餘文本格式的內容。

屬性設置有不少,能夠本身嘗試,固然抓取間隔不要過短,不然會給目標網站帶來很大負擔,特別注意

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("&nbsp;"," ").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對象提供了兩個方法

  • getSite()
  • getUUID()

沒有使用過,可是看方法名大概能知道是作什麼的;

Serivice.saveFile 是我本身簡單封裝的保存文件方法,在src同級目錄建立以教程命名的文件夾,以每一個頁面標題爲文件名建立.md文件.簡單的IO操做,就不貼出來;

特別注意的是WebMagic框架會在底層catch異常,可是卻不會報錯,因此開發調試的時候,若是要捕獲異常的話,須要本身try catch ,特別是那些RuntimeException

 

囉囉嗦嗦打了好多,完整代碼下載,個人GitHub

https://github.com/BekeyChao/HelloWorld/tree/master/src

由於沒有用git管理(主要有一些其餘內容),因此是手動同步的,若是運行不起來,就好好研究吧~

相關文章
相關標籤/搜索