基于WebMagic的新闻抓取工具小结          返回主页

github地址EducationNewsHunterSpider

本文是WebHunter爬虫系统中自动化采集模块的小结,该自动化模块是基于WebMagic实现的。

什么是WebMagic

WebMagic是一个简单灵活的爬虫框架。基于WebMagic,你可以快速开发出一个高效、易维护的爬虫。作者是一位黄亿华的国人。

之所以采用Webmagic,一方面是它在开源社区的口碑很好,另一方面它有着丰富的中英文文档。提升了学习的速度。从开始接触它,到开发出1.0版本的爬虫系统,只用了三天的时间。

WebMagic在我的项目中主要运用在了自动化页面采集和数据持久化部分。它的优雅的Xpath和CSS选择器大大减轻了我解析页面的负担。感谢作者的贡献。

模块简介

文件结构

    --com.hunter.auto.bean          实体类包
        --EducationNews.java                    新闻实体类,封装了需要保存的新闻信息,与数据库中的字段对应
        --TencentNewsProcessorSingleton.java新闻解析器单例类,通过单例模式返回一个解析器实体
    --com.hunter.auto.pipeline      管道类包
        --DBPersistencePipeline.java            实现管道接口,自定义数据库持久化逻辑,将抓取的数据持久化到数据库
    --com.hunter.auto.processor     解析器类包
        --TencentNewsProcessor.java         腾讯新闻解析器,实现PageProcessor接口,定义了主要的爬取逻辑
    --com.hunter.auto.serviceEngine 应用引擎包
        --Startup.java                      自动化爬取的开关模块,通过接受来自前端的参数决定启动或者结束爬取
    --com.hunter.auto.serviceEngine.client 客户端启动类包
        --Bootstrap.java                    JavaSE的应用启动器,能够使应用运行在CLI命令行接口下,并可扩展为桌面应用。提供了另一种启动方式。
    --com.hunter.auto.utils         工具类包
        --StringUtils.java                  封装了一些字符串操作。主要是对字符串的格式化输出。
    --com.hunter.dao                数据访问层类包
        --HunterDao.java                    数据访问接口
        --HunterDaoImpl.java                数据访问接口实现类,实现了数据持久化逻辑

应用流程介绍

  1. Startup.java在接受前端输入的线程数之后会根据此数量确定运行时线程数量。
    1. Spider开启抓取实例。这里有三种方式,同步方式抓取的run();异步方式的
    2. start()/runAsync();我使用了异步的方式,这种方式使得抓取效率提高了10倍以上
    3. 输入线程数之后通过单选框确定开启或者关闭操作。如果是on,则触发抓取逻辑。off则关闭逻辑
    4. 在web端能够可视化的看到抓取的情况,具体扫描了哪一个URL
  2. DBPersistencePipeline.java对抓取的内容进行持久化,这样就可以在展示层看到效果
    1. 该类实现了Pipeline接口,在原有的控制台、文件、json等实现的基础上添加了数据持久化的功能。
  3. 在之前持久化的基础上,运行另一个展示层应用,可查看到具体的效果。

关键文件解析

1. TencentNewsProcessor.java 腾讯新闻解析器,实现PageProcessor接口,定义了主要的爬取逻辑

public class TencentNewsProcessor implements PageProcessor {

    /**
     * 抓取网站的相关配置,编码,抓取间隔,重试次数等
     */
    private Site site = Site.me()
            .setRetryTimes(3)
            .setSleepTime(100)
            .setUserAgent("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0")
            .setTimeOut(400);

    @Override
    public Site getSite() {
        return site;
    }
    /**
     * 核心逻辑,在此处编写抽取的核心业务
     */
    @Override
    public void process(Page page) {
        // 部分二:定义如何抽取页面信息,并保存下来
        //题目
        String title = page.getHtml().xpath("//*[@id='C-Main-Article-QQ']/div[1]/h1").toString();
        //正文
        String content = page.getHtml().xpath("//*[@id='Cnt-Main-Article-QQ']").toString();
        page.putField("title", title);
        page.putField("content", content);
        /**
         * 保存数据到对象中
         */
        EducationNews educationNews = new EducationNews();
        educationNews.setTitle(page.getHtml().xpath("//*[@id='C-Main-Article-QQ']/div[1]/h1").toString());
        educationNews.setContent(page.getHtml().xpath("//*[@id='Cnt-Main-Article-QQ']").toString());
        //空内容则跳过
        if (educationNews.getContent() == null) {
             page.setSkip(true);
        } else {
            //不为空则设值
            page.putField("educationNews", educationNews);
        }

        // 部分三:从页面发现后续的url地址来抓取
        //获取所有满足正则表达式的连接并添加到队列中去        http://edu\\.qq\\.com/a
        page.addTargetRequests(page.getHtml().links().regex("(http://edu\\.qq\\.com/\\a/\\w+/\\w+/\\.htm)").all());
    }

}

解释: 1. 实现了PageProcessor接口,并实现其回调方法getSite(),process(Page page),定义具体的抓取策略。
2. 在process(Page page)中使用XPATH表达式定位要抓取的内容,获取XPATH表达式最实用的方法是在chrome浏览器的开发者工具中选取对应内容的XPATH表达式定义。
1. 比如://*[@id='C-Main-Article-QQ']/div[1]/h1,它的意思是选取id为C-Main-Article-QQ的第一个div中的h1标签的html 3. EducationNews educationNews = new EducationNews();定义并实例化一个新闻实体,通过setter将抓取到的具体的内容封装到其中,其中需要通过toString()方法将抓取到的内容转换为字符串。
4. 接着通过page.putField("educationNews", educationNews);通过K-V方式将组装好的实体放入page中,便于后续持久化的操作。
5. page.addTargetRequests(page.getHtml().links().regex("(http://edu\.qq\.com/\a/\w+/\w+/\.htm)").all());制定进一步抓取到策略。能够匹配正则的超链接会添加到队列中进一步深度抓取。


2.DBPersistencePipeline.java 实现管道接口,自定义数据库持久化逻辑,将抓取的数据持久化到数据库

public class DBPersistencePipeline implements Pipeline {

    private EducationNews educationNews;

    private HunterDao hunterDao;

    @Override
    public void process(ResultItems resultItems, Task task) {
        //获取实例
        educationNews = (EducationNews) resultItems.get("educationNews");
        //拆分实例并持久化
        String title = educationNews.getTitle();
        String content = educationNews.getContent();
        //获取持久层
        hunterDao = new HunterDaoImpl();
        boolean flag = hunterDao.saveData(title,
                new Date(System.currentTimeMillis()),
                content);
        if (flag) {
            System.out.println("持久化成功");
        } else {
            System.out.println("持久化失败");
        }
    }

}

解释: 1. 实现管道接口Pipeline,实现其process方法
2. process(ResultItems resultItems, Task task)通过resultItems使用get("key")将之前在TencentNewsProcessor组装的获取了爬取数据的实体类拆分,取出实体中封装的属性,并通过HunterDao的实现类HunterDaoImpl中的saveData方法将数据持久化到数据库中。

==============================

到这里基本上就是整个应用的核心流程,但是在其它地方也有值得研究之处。

=============================

3.Startup.java 自动化爬取的开关模块,通过接受来自前端的参数决定启动或者结束爬取

/**
 * 服务启动器
 * 由于要更新主线程中的信息,
 * 因此启动爬虫逻辑写在子线程中,发送消息到主线程
 * 更新主线程视图
 * 使用AsyncRun()异步方式提升抓取速度
 * @author snowalker
 * 16.7.26
 */
public class Startup extends HttpServlet {

    .......

    private RunningEngine runningEngine;

    ........

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");

        runningEngine = new RunningEngine();
        //获取标志位
        String flag = request.getParameter("flag");
        //System.out.println(flag);
        if ("off".equals(flag)) {
            runningEngine.currentThread().interrupt();
            request.getRequestDispatcher("HunterIndex").forward(request, response);
        }
        if ("on".equals(flag)) {
            //获取参数
            String sThreadnum = request.getParameter("threadnum");
            threadnum = Integer.valueOf(sThreadnum);
            //初始时间
            out = response.getWriter();
            out.println("<br/><br/><br/><br/><br/><br/><br/><br/>"
                    + "<br/><br/><br/><br/><br/><br/><br/><br/><br/>");
            out.println("<center><h2>spider now is running:"
                    + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date())
                    + "</center></h2>");
            out.println("<br/><center><a href='HunterIndex'><h3><font color='blue'>返回首页<font></h3></a></center>");
            //建立运行引擎实例
            runningEngine = new RunningEngine();
            //开启线程运行抓取逻辑
            runningEngine.start();
            //获取输出实例
            out.println("<center><h2>" + "<font color='red'>正在抓取分析:</font>" + urlTemp + "</h2></center>");
            //设定1s自动刷新
            response.setIntHeader("Refresh", 1);
        }


    }

    ...省略doPost()....

    /**
     * 抓取核心逻辑
     * @param response
     * @param threadnum
     * @throws IOException
     */
    public void start(HttpServletResponse response, int threadnum) throws IOException {
        //测试逻辑
        testData();
        //运行逻辑
        for (int i = 0; i < 4; i++) {
            for (int j = 700; j <= 9999; j++) {
                //url定义
                String url = "http://edu.qq.com/a/"
                        + new SimpleDateFormat("yyyyMMdd").format(System.currentTimeMillis())
                        + "/0"
                        + i
                        + StringUtils.IntegerFomatterUtil(j)
                        +".htm";
                //url中间变量
                urlTemp = url;
                //控制台输出
                System.out.println(url);
                System.out.println("===================================");
                try {
                    Spider.create(tencentNewsProcessor)
                    .addUrl(url)
                    .addPipeline(new DBPersistencePipeline())
                    .thread(threadnum)
                    .runAsync();    //异步抓取
                } catch (Exception e) {
                }

            }
        }
    }
    .......

    /**
     * 运行逻辑,内部类实现
     * @author snowalker
     * 16.7.27
     * 内部类,继承Thread
     * 开启多线程方式抓取
     */
    class RunningEngine extends Thread {

        @Override
        public void run() {
            try {
                if (this.interrupted()) {
                    System.out.println("已经停止");
                    throw new InterruptedException();
                }
                Startup.this.start(response, threadnum);
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}

解释: 1. 省略了一些无关紧要的代码
2. 这一段代码中最主要的知识点是使用了多线程的知识;在代码最下端定义了一个内部类RunningEngine继承Thread类,实现其run()回调方法。在这里运用了线程停止的知识,通过interrupted()进行一次判断。这个知识点我的另一篇博客专门进行了讲解【多线程】停止线程小结
3. 这里之所以用了多线程是为了数据的展示所考虑。之前的代码是在主线程中进行了爬取逻辑的调用,在控制台能够正常看到状态数据。我的初衷是想要实现在网页端通过定时数据刷新实现抓取链接的动态扫描呈现。如果在主线程中这么做就会出现如果没有结束扫描逻辑则页面一直加载的情况。通过定义实现Thread的内部类,异步执行爬取逻辑,并将扫描过的链接保存在成员变量中,在主线程中以1s为单位获取该成员变量,并显示在前台界面。实现了后台抓取,前台查看抓取状态的业务。


小结

本文是对新闻抓取爬虫的技术点的总结。这个系统主要基于WebMagic实现,让我对爬虫的基本概念及使用有了一个初步但全方位的认识。WebMagic作为一个“教科书式”的爬虫实现确实有其独到之处,在基于java技术的众多爬虫及搜索引擎之中占据了一席之地。再次感谢作者对开源社区的贡献。 PS:如果想要实现进一步的定制和精确化页面爬取,一定要加强对正则表达式的学习。

                                                                                        16.7.27