博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
webmagic入门-菜鸟教程html to markdown
阅读量:5764 次
发布时间:2019-06-18

本文共 8697 字,大约阅读时间需要 28 分钟。

hot3.png

      最近在学习Java爬虫,发现了webmagic轻量级框架,在网上搜索了一些教程,然后自己尝试着写了一个对的爬虫,主要功能为把教程内容html转换为markdown文本,方便离线阅读;  

      做这个工具的主要原因是,我们单位的工作环境一般要求断网,菜鸟教程上的教学作为入门一般不错,为了方便离线学习,做了这个应用;现在写了主要为了分享和自己学习总结;  
第一次写博文,不完善的地方请见谅

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

Maven依赖

   
us.codecraft
   
webmagic-core
   
0.7.1
   
us.codecraft
   
webmagic-extension
   
0.7.1

中文文档 -> 

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

----

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

----
项目创建

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

  • 主要内容结构如图  

Controller - 控制器,Main方法入口

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

RunoobPageProcessor - 页面解析组件

Service - 服务提供组件,相当于Utils,主要用于包装通用方法

菜鸟教程页面

这里选取的是作为样板

开始上代码

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(获取链接) 等方法,不过我不太会用,所以后面页面解析主要还是使用实现

WebMagic PageProcessor 中解析页面主要就是使用Jsoup实现的,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("
",""); 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

因为没有用git管理(主要有一些其他内容),所以是手动同步的,如果运行不起来,就好好研究吧~

转载于:https://my.oschina.net/u/3491123/blog/917836

你可能感兴趣的文章
20个Linux服务器性能调优技巧
查看>>
多重影分身:一套代码如何生成多个小程序?
查看>>
Oracle将NetBeans交给了Apache基金会
查看>>
填坑记:Uncaught RangeError: Maximum call stack size exceeded
查看>>
SpringCloud之消息总线(Spring Cloud Bus)(八)
查看>>
DLA实现跨地域、跨实例的多AnalyticDB读写访问
查看>>
实时编辑
查看>>
KVO原理分析及使用进阶
查看>>
【348天】每日项目总结系列086(2018.01.19)
查看>>
【JS基础】初谈JS现有的数据类型
查看>>
【294天】我爱刷题系列053(2017.11.26)
查看>>
Microsoft发布了Azure Bot Service和LUIS的GA版
查看>>
Google发布Puppeteer 1.0
查看>>
.NET开源现状
查看>>
可替换元素和非可替换元素
查看>>
2016/08/25 The Secret Assumption of Agile
查看>>
(Portal 开发读书笔记)Portlet间交互-PortletSession
查看>>
搭建vsftpd服务器,使用匿名账户登入
查看>>
AMD改善Linux驱动,支持动态电源管理
查看>>
JAVA中循环删除list中元素的方法总结
查看>>