本系列文章使用WebCollector开发爬虫功能,虽然官方也有很多相关的文档和示例,但是代码的解释实在太少,因此本系列会对代码进行详细解释。

先解释一下爬虫最基本的原理:

  1. 给定一个URL,对该URL发起http请求,可以获取到一个html文档,也可能是一个文件,图片等资源。
  2. 如果是html文档,则提取其中的链接,然后对每一个链接执行第一步,同时提取dom中的有用文本,存数据库;如果是文件,则将数据保存到文件。

如此反复,就可以从一个网站上爬取文本信息和文件。

下面从实战开始进行详细解析:

  1. 首先创建maven项目,引入下面的依赖,之后的文章如果没有特别说明均是如此。
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>3.4.0</version>
        </dependency>
  1. 手动模式
    首先什么是手动模式?我们已经知道URL会存在某个标签上,然后通过css选择器获取该标签上的URL的方式就是手动模式。首先举个例子,我们从img标签中提取到一个URL,这就是手动模式提取URL,你觉得这个URL是什么?图片对吧?那么我们就可以在代码中进行标识,如果是从img中提取的URL就给他一个img的标识,如果明确知道从某个地方提取到的URL是html文档则给他一个html标识,这里的标识可以随便改,不同的标识决定对该链接发起请求后走不同的逻辑。之后我们再对html类型的URL进行解析,提取里面的其他URL,如此反复;而对img标识的URL直接保存为图片。

  2. 代码实战

import cn.edu.hfut.dmic.webcollector.model.CrawlDatums;
import cn.edu.hfut.dmic.webcollector.model.Links;
import cn.edu.hfut.dmic.webcollector.model.Page;
import cn.edu.hfut.dmic.webcollector.plugin.rocks.BreadthCrawler;
import xin.jiangqiang.utils.ImgUtils;

/**
 * extends BreadthCrawler 内置数据库,适合处理长期和大量级的任务,并具有断点爬取功能,不会因为宕机、关闭导致数据丢失。
 * extends RamCrawler 不支持断点爬取
 *
 * @author jiangqiang
 * @date 2020/12/9 16:03
 */
public class MyCrawler extends BreadthCrawler {
    /**
     * 构造一个基于RocksDB的爬虫
     * RocksDB文件夹为crawlPath,crawlPath中维护了历史URL等信息
     * 不同任务不要使用相同的crawlPath
     * 两个使用相同crawlPath的爬虫并行爬取会产生错误
     *
     * @param crawlPath RocksDB使用的文件夹
     * @param autoParse 是否根据设置的正则自动探测新URL
     */
    public MyCrawler(String crawlPath, boolean autoParse) {
        super(crawlPath, autoParse);
        //种子URL,手动给定标识为home
        this.addSeed("https://blog.jiangqiang.xin/", "home");
    }

    @Override
    public void visit(Page page, CrawlDatums next) {
        if (page.matchType("home")) {
            Links links = page.links(".post-thumb>a");
            //首页里面提取文章内容的URL,给定标识为content
            next.add(links).type("content");
        } else if (page.matchType("content")) {
            //文章内容页面提取标题,内容。
            System.out.println("title:" + page.select(".entry-header>.entry-title").first().text());
            System.out.println("content:" + page.select("article .entry-content").first().text());
            //提取插图的URL
            next.add(page.attrs("img[src]", "abs:src"), "img");
        } else if (page.matchType("img")) {
            //如果是图片则保存到指定目录
            ImgUtils.save(page, "crawl/images");
        }
    }

    public static void main(String[] args) throws Exception {
        //此demo采用的是内置数据库,会将爬虫的一些信息存储到文件系统,支持断点续爬,第一个参数是存储路径,第二个参数表示是否自动分类
        MyCrawler myCrawler = new MyCrawler("crawl", false);
        myCrawler.start(4);
    }
}

import cn.edu.hfut.dmic.webcollector.model.Page;
import cn.edu.hfut.dmic.webcollector.util.ExceptionUtils;
import cn.edu.hfut.dmic.webcollector.util.FileUtils;
import cn.edu.hfut.dmic.webcollector.util.MD5Utils;

import java.io.File;

/**
 * 保存图片的工具类
 * @author JiangQiang
 * @date 2020/1/12 11:07
 */
public class ImgUtils {
    public static void save(Page page, String baseDir) {
        //根据http头中的Content-Type信息来判断当前资源是网页还是图片
        String contentType = page.contentType();
        //根据Content-Type判断是否为图片
        if (contentType != null && contentType.startsWith("image")) {
            //从Content-Type中获取图片扩展名
            String extensionName = contentType.split("/")[1];
            try {
                byte[] image = page.content();
                //根据图片MD5生成文件名
                String fileName = String.format("%s.%s", MD5Utils.md5(image), extensionName);
                File imageFile = new File(baseDir, fileName);
                FileUtils.write(imageFile, image);
                System.out.println("保存图片 " + page.url() + " 到 " + imageFile.getAbsolutePath());
            } catch (Exception e) {
                ExceptionUtils.fail(e);
            }
        }
    }
}

关于代码中的难点都已经注释的明明白白,官方还提供了另一种使用注解替代if语句的写法。
代码如下:

import cn.edu.hfut.dmic.webcollector.model.CrawlDatums;
import cn.edu.hfut.dmic.webcollector.model.Links;
import cn.edu.hfut.dmic.webcollector.model.Page;
import cn.edu.hfut.dmic.webcollector.plugin.rocks.BreadthCrawler;
import xin.jiangqiang.utils.ImgUtils;

/**
 * extends BreadthCrawler 内置数据库,适合处理长期和大量级的任务,并具有断点爬取功能,不会因为宕机、关闭导致数据丢失。
 * extends RamCrawler 不支持断点爬取
 *
 * @author jiangqiang
 * @date 2020/12/9 16:03
 */
public class MyCrawlerAnno extends BreadthCrawler {
    /**
     * 构造一个基于RocksDB的爬虫
     * RocksDB文件夹为crawlPath,crawlPath中维护了历史URL等信息
     * 不同任务不要使用相同的crawlPath
     * 两个使用相同crawlPath的爬虫并行爬取会产生错误
     *
     * @param crawlPath RocksDB使用的文件夹
     * @param autoParse 是否根据设置的正则自动探测新URL
     */
    public MyCrawlerAnno(String crawlPath, boolean autoParse) {
        super(crawlPath, autoParse);
        //种子URL,手动给定标识为home
        this.addSeed("https://blog.jiangqiang.xin/", "home");
    }


    @MatchType(types = "home")
    public void visitHome(Page page, CrawlDatums next) {
        Links links = page.links(".post-thumb>a");
        //首页里面提取文章内容的URL,给定标识为content
        next.add(links).type("content");
    }

    @MatchType(types = "content")
    public void visitContent(Page page, CrawlDatums next) {
        //文章内容页面提取标题,内容。
        System.out.println("title:" + page.select(".entry-header>.entry-title").first().text());
        System.out.println("content:" + page.select("article .entry-content").first().text());
        //提取插图的URL
        next.add(page.attrs("img[src]", "abs:src"), "img");
    }

    @MatchType(types = "img")
    public void visitImg(Page page, CrawlDatums next) {
        //如果是图片则保存到指定目录
        ImgUtils.save(page, "crawl/images");
    }

    public static void main(String[] args) throws Exception {
        //此demo采用的是内置数据库,会将爬虫的一些信息存储到文件系统,支持断点续爬,第一个参数是存储路径,第二个参数表示是否自动分类
        MyCrawlerAnno myCrawler = new MyCrawlerAnno("crawl", false);
        myCrawler.start(4);
    }

    @Override
    public void visit(Page page, CrawlDatums next) {

    }
}

注意:提取网页正文内容可以使用下面的方式:

           News news = ContentExtractor.getNewsByUrl(page.url());
                String content = ContentExtractor.getContentByUrl(page.url());
                Element contentElement = ContentExtractor.getContentElementByUrl(page.url());

Q.E.D.


擅长前端的Java程序员