webmagic入门-菜鸟教程html to markdown

      最近在学习Java爬虫,发现了webmagic轻量级框架,在网上搜索了一些教程,然后自己尝试着写了一个对菜鸟教程的爬虫,主要功能为把教程内容html转换为markdown文本,方便离线阅读;  
      做这个工具的主要原因是,我们单位的工作环境一般要求断网,菜鸟教程上的教学作为入门一般不错,为了方便离线学习,做了这个应用;现在写了主要为了分享和自己学习总结;  
第一次写博文,不完善的地方请见谅

关于 **WebMagic**,我就不作介绍了,主页传送门 -> WebMagic
Maven依赖

<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/  


因为用到了lambda表达式,jdk版本要求1.8+,IDE使用IDEA

----
写个介绍真辛苦,下面进入项目

----
项目创建

  • 创建项目,导入jar包(略)

  • 主要内容结构如图  

Controller - 控制器,Main方法入口

MarkdownSavePipeline - 持久化组件-保存为文件

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管理(主要有一些其他内容),所以是手动同步的,如果运行不起来,就好好研究吧~

猜你喜欢

转载自my.oschina.net/u/3491123/blog/917836