一个可配置的爬虫采集系统的方案实现

2,921 阅读11分钟

记录两年前写的一个采集系统,包括需求,分析,设计,实现,遇到的问题及系统的成效,系统最主要功能就是可以通过对每个网站进行不同的采集规则配置对每个网站爬取数据,两年前离职的时候已爬取的数据量大概就在千万级左右,每天采集的数据增量在一万左右,配置采集的网站1200多个,现记录一下系统实现,在提供一些简单的爬虫demo供大家学习下如何爬数据

需求

数据采集系统:一个可以通过配置规则采集不同网站的系统 主要实现目标:

  1. 针对不同的网站通过配置不同的采集规则实现网页数据的爬取
  2. 针对每篇内容可以实现对特征数据的提取
  3. 定时去爬取所有网站的数据
  4. 采集配置规则可维护
  5. 采集入库数据可维护

架构图

数据采集系统架构图

分析

第一步当然要先分析需求,所以在抽取一下系统的主要需求:

  1. 针对不同的网站可以通过不同的采集规则实现数据的爬取
  2. 针对每篇内容可以实现对特征数据的提取,特征数据就是指标题,作者,发布时间这种信息
  3. 定时任务关联任务或者任务组去爬取网站的数据

再分析一下网站的结构,无非就是两种;

  1. 一个是列表页,这里的列表页代表的就是那种需要在当前页面获取到更多别的详情页的网页链接,像一般的查询列表,可以通过列表获取到更多的详情页链接。
  2. 一个是详情页,这种就比较好理解,这种页面不需要在这个页面再去获得别的网页链接了,直接在当前页面就可以提取数据。

基本所有爬取的网站都可以抽象成这样。

设计

针对分析的结果设计实现:

  1. 任务表

    每个网站可以当做一个任务,去执行采集

  2. 两张规则表

    每个网站对应自己的采集规则,根据上面分析的网站结构,采集规则又可以细分为两个表,一个是包含网站链接,获取详情页列表的列表采集规则表,一个针对是网站详情页的特征数据采集的规则表 详情采集规则表

  3. url表

    负责记录采集目标网站详情页的url

  4. 定时任务表

    根据定时任务去定时执行某些任务 (可以采用定时任务和多个任务进行关联,也可以考虑新增一个任务组表,定时任务跟任务组关联,任务组跟任务关联)

  5. 数据存储表

    这个由于我们采集的数据主要是招标和中标两种数据,分别建了两张表进行数据存储,中标信息表,招标信息表

实现

框架

基础架构就是:ssm+redis+htmlunit+jsoup+es+mq+quartz java中可以实现爬虫的框架有很多,htmlunit,WebMagic,jsoup等等还有很多优秀的开源框架,当然httpclient也可以实现。

为什么用htmlunit? htmlunit 是一款开源的java 页面分析工具,读取页面后,可以有效的使用htmlunit分析页面上的内容。项目可以模拟浏览器运行,被誉为java浏览器的开源实现

简单说下我对htmlunit的理解:

  1. 一个是htmlunit提供了通过xpath去定位页面元素的功能,利用xpath就可以实现对页面特征数据进行提取;
  2. 第二个就在于对js的支持,支持js意味着你真的可以把它当做一个浏览器,你可以用它模拟点击,输入,登录等操作,而且对于采集而言,支持js就可以解决页面使用ajax获取数据的问题
  3. 当然除此之外,htmlunit还支持代理ip,https,通过配置可以实现模拟谷歌,火狐等浏览器,Referer,user-agent,是否加载js,css,是否支持ajax等。

XPath语法即为XML路径语言(XML Path Language),它是一种用来确定XML文档中某部分位置的语言。

为什么用jsoup? jsoup相较于htmlunit,就在于它提供了一种类似于jquery选择器的定位页面元素的功能,两者可以互补使用。

采集

采集数据逻辑分为两个部分:url采集器,详情页采集器

url采集器:

  • 只负责采集目标网站的详情页url

详情页采集器:

  • 根据url去采集目标url的详情页数据

  • 使用htmlunit的xpath,jsoup的select语法,和正则表达式进行特征数据的采集。

    这样设计目的主要是将url采集和详情页的采集流程分开,后续如果需要拆分服务的话就可以将url采集和详情页的采集分成两个服务。

    url采集器与详情页采集器之间使用mq进行交互,url采集器采集到url做完处理之后把消息冷到mq队列,详情页采集器去获取数据进行详情页数据的采集。

遇到的问题

数据去重:

  1. 在采集url的时候进行去重
  2. 同过url进行去重,通过在redis存储key为url,缓存时间为3天,这种方式是为了防止对同一个url进行重复采集。
  3. 通过标题进行去重,通过在redis中存储key为采集到的标题 ,缓存时间为3天,这种方式就是为了防止一篇文章被不同网站发布,重复采集情况的发生。

数据质量:

​ 由于每个网站的页面都不一样,尤其是有的同一个网站的详情页结构也不一样,这样就给特征数据的提取增加了难度,所以使用了htmlunit+jsoup+正则三种方式结合使用去采集特征数据。

采集效率:

​ 由于采集的网站较多,假设每个任务的执行都打开一个列表页,十个详情页,那一千个任务一次执行就需要采集11000个页面,所以采用url与详情页分开采集,通过mq实现异步操作,url和详情页的采集通过多线程实现。

被封ip:

​ 对于一个网站,假设每半小时执行一次,那每天就会对网站进行48次的扫描,也是假设一次采集会打开11个页面,一天也是528次,所以被封是一个很常见的问题。解决办法,htmlunit提供了代理ip的实现,使用代理ip就可以解决被封ip的问题,代理ip的来源:一个是现在网上有很多卖代理ip的网站,可以直接去买他们的代理ip,另一种就是爬,这些卖代理ip的网站都提供了一些免费的代理ip,可以将这些ip都爬回来,然后使用httpclient或者别的方式去验证一下代理ip的可用性,如果可以就直接入库,构建一个自己的代理ip库,由于代理ip具有时效性,所以可以建个定时任务去刷这个ip库,将无效ip剔除。

网站失效:

​ 网站失效也有两种,一种是网站该域名了,原网址直接打不开,第二种就是网站改版,原来配置的所有规则都失效了,无法采集到有效数据。针对这个问题的解决办法就是每天发送采集数据和日志的邮件提醒,将那些没采到数据和没打开网页的数据汇总,以邮件的方式发送给相关人员。

验证码:

​ 当时对一个网站采集历史数据采集,方式也是先通过他们的列表页去采集详情页,采集了几十万的数据之后发现,这个网站采不到数据了,看页面之后发现在列表页加了一个验证码,这个验证码还是属于比较简单的就数字加字母,当时就想列表页加验证码?,然后想解决办法吧,搜到了一个开源的orc文字识别项目tess4j(怎么使用可以看这),用了一下还可以,识别率在百分之二十左右,因为htmlunit可以模拟在浏览器的操作,所以在代码中的操作就是先通过htmlunit的xpath获取到验证码元素,获取到验证码图片,然后利用tess4j进行验证码识别,之后将识别的验证码在填入到验证码的输入框,点击翻页,如果验证码通过就翻页进行后续采集,如果失败就重复上述识别验证码操作,知道成功为止,将验证码输入到输入框和点击翻页都可用htmlunit去实现

ajax加载数据:

​ 有些网站使用的是ajax加载数据,这种网站在使用htmlunit采集的时候需要在获取到HtmlPage对象之后给页面一个加载ajax的时间,之后就可以通过HtmlPage拿到ajax加载之后的数据。

代码:webClient.waitForBackgroundJavaScript(time); 可以看后面提供的demo

系统整体的架构图,我们这里说就是数据采集系统这部分

demo

爬虫的实现:

@GetMapping("/getData")
    public List<String> article_(String url,String xpath){
        WebClient webClient = WebClientUtils.getWebClientLoadJs();
        List<String> datas = new ArrayList<>();
        try {
            HtmlPage page = webClient.getPage(url);
            if(page!=null){
                List<?> lists = page.getByXPath(xpath);
                lists.stream().forEach(i->{
                    DomNode domNode = (DomNode)i;
                    datas.add(domNode.asText());
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            webClient.close();
        }
        return datas;
    }

上面的代码就实现了采集一个列表页

  • url就是目标网址
  • xpath就要采集的数据的xpath了

爬一下博客园

请求这个url:http://localhost:9001/getData?url=https://www.cnblogs.com/&xpath=//*[@id="post_list"]/div/div[2]/h3/a

  • url:传的是博客园首页的地址;
  • xpath:传的是获取博客园首页的博客列表的标题

网页页面:

采集回的数据:

再爬一下csdn

再次请求:http://localhost:9001/getData?url=https://blog.csdn.net/&xpath=//*[@id="feedlist_id"]/li/div/div[1]/h2/a

  • url:这次传是csdn的首页;
  • xpath:传的是获取csdn首页的博客列表的标题

网页页面:

采集回的数据:

采集步骤

通过一个方法去采集两个网站,通过不同url和xpath规则去采集不同的网站,这个demo展示的就是htmlunit采集数据的过程。
每个采集任务都是执行相同的步骤
- 获取client -> 打开页面 -> 提取特征数据(或详情页链接) -> 关闭cline
不同的地方就在于提取特征数据

优化:利用模板方法设计模式,将功能部分抽取出来

上述代码可以抽取为:一个采集执行者,一个自定义采集数据的实现

/**
 * @Description: 执行者 man
 * @author: chenmingyu
 * @date: 2018/6/24 17:29
 */
public class Crawler {

    private Gatherer gatherer;

    public Object execute(String url,Long time){
        // 获取 webClient对象
        WebClient webClient = WebClientUtils.getWebClientLoadJs();
        try {
            HtmlPage page = webClient.getPage(url);
            if(null != time){
                webClient.waitForBackgroundJavaScript(time);
            }
            return gatherer.crawl(page);
        }catch (Exception e){

            e.printStackTrace();
        }finally {
            webClient.close();
        }
        return null;
    }

   public Crawler(Gatherer gatherer) {
        this.gatherer = gatherer;
    }
}

在Crawler 中注入一个接口,这个接口只有一个方法crawl(),不同的实现类去实现这个接口,然后自定义取特征数据的实现

/**
 * @Description: 自定义实现
 * @author: chenmingyu
 * @date: 2018/6/24 17:36
 */
public interface Gatherer {

    Object crawl(HtmlPage page) throws Exception;
}

优化后的代码:

	@GetMapping("/getData")
    public List<String> article_(String url,String xpath){

        Gatherer gatherer = (page)->{
            List<String> datas = new ArrayList<>();
            List<?> lists = page.getByXPath(xpath);
            lists.stream().forEach(i->{
                DomNode domNode = (DomNode)i;
                datas.add(domNode.asText());
            });
            return datas;
        };

        Crawler crawler = new Crawler(gatherer);
        List<String> datas = (List<String>)crawler.execute(url,null);
        return datas;
    }

不同的实现,只需要去修改接口实现的这部分就可以了

数据

最后看一下利用采集系统采集的数据。

效果

效果还是不错的,最主要是系统运行稳定:

  1. 采集的历史数据在600-700万量级之间
  2. 每天新采集的数据增量在一万左右
  3. 系统目前配置了大约1200多个任务(一次定时的实现会去采集这些网站)

数据

系统配置采集的网站主要针对全国各省市县招投标网站(目前大约配置了1200多个采集站点)的标讯信息。 采集的数据主要做公司标讯的数据中心,为一个pc端网站和2微信个公众号提供数据

欢迎关注,掌握一手标讯信息

以pc端展示的一篇采集的中标的数据为例,看下采集效果:

本文只是大概记录下这个采集系统从零到整的过程,当然其中还遇到了很多本文没提到的问题。