WebMagic 基础知识

2023-09-23 15:46:49 浏览数 (1)

webmagic是一个开源的Java垂直爬虫框架,目标是简化爬虫的开发流程,让开发者专注于逻辑功能的开发。webmagic的核心非常简单,但是覆盖爬虫的整个流程,也是很好的学习爬虫开发的材料。 官方文档

WebMagic 初探

WebMagic框架包含四个组件,PageProcessorSchedulerDownloaderPipeline。

这四大组件对应爬虫生命周期中的处理管理下载持久化等功能。

这四个组件都是Spider中的属性,爬虫框架通过Spider启动和管理。

WebMagic总体架构图如下:

Spider

Spider是WebMagic内部流程的核心。Downloader、PageProcessor、Scheduler、Pipeline都是Spider的一个属性,这些属性是可以自由设置的,通过设置这个属性可以实现不同的功能。Spider也是WebMagic操作的入口,它封装了爬虫的创建、启动、停止、多线程等功能。下面是一个设置各个组件,并且设置多线程和启动的例子。

代码语言:javascript复制
public static void main(String[] args) {
    Spider.create(new GithubRepoPageProcessor())
            //从https://github.com/code4craft开始抓    
            .addUrl("https://github.com/code4craft")
            //设置Scheduler,使用Redis来管理URL队列
            .setScheduler(new RedisScheduler("localhost"))
            //设置Pipeline,将结果以json方式保存到文件
            .addPipeline(new JsonFilePipeline("D:\data\webmagic"))
            //开启5个线程同时执行
            .thread(5)
            //启动爬虫
            .run();
}

方法

说明

示例

create(PageProcessor)

创建Spider

Spider.create(new GithubRepoProcessor())

addUrl(String…)

添加初始的URL

spider.addUrl(“http://webmagic.io/docs/”)

thread(n)

开启n个线程

spider.thread(5)

run()

启动,会阻塞当前线程执行

spider.run()

start()/runAsync()

异步启动,当前线程继续执行

spider.start()

stop()

停止爬虫

spider.stop()

addPipeline(Pipeline)

添加一个Pipeline,一个Spider可以有多个Pipeline

spider.addPipeline(new ConsolePipeline())

setScheduler(Scheduler)

设置Scheduler,一个Spider只能有个一个Scheduler

spider.setScheduler(new RedisScheduler())

setDownloader(Downloader)

设置Downloader, 一个Spider只能有个一个Downloader

spider.setDownloader( new SeleniumDownloader())

get(String)

同步调用,并直接取得结果

ResultItems result = spider.get(“http://webmagic.io/docs/”)

getAll(String…)

同步调用,并直接取得一堆结果

List results = spider.getAll(“http://webmagic.io/docs/”, “http://webmagic.io/xxx”)

Site

Site用于定义站点本身的一些配置信息,例如编码、HTTP头、超时时间、重试策略等、代理等,都可以通过设置Site对象来进行配置

方法

说明

示例

setCharset(String)

设置编码

site.setCharset(“utf-8”)

setUserAgent(String)

设置 UserAgent

site.setUserAgent(“Spider”)

setTimeOut(int)

设置超时时间,单位是毫秒

site.setTimeOut(3000)

setRetryTimes(int)

设置重试次数

site.setRetryTimes(3)

setCycleRetryTimes(int)

设置循环重试次数

site.setCycleRetryTimes(3)

addCookie(String,String)

添加一条cookie

site.addCookie(“dotcomt_user”,“code4craft”)

setDomain(String)

设置域名,需设置域名后,addCookie才可生效

site.setDomain(“github.com”)

addHeader(String,String)

添加一条 addHeader

site.addHeader(“Referer”,“https://github.com”)

setHttpProxy(HttpHost)

设置Http代理

site.setHttpProxy(new HttpHost(“127.0.0.1”,8080))

其中循环重试cycleRetry是0.3.0版本加入的机制。

该机制会将下载失败的url重新放入队列尾部重试,直到达到重试次数,以保证不因为某些网络原因漏抓页面。

WebMagic 四大组件

  1. PageProcessor:负责解析页面,抽取有用信息,以及发现新的链接。需要自己定义。
  2. Scheduler:负责管理待抓取的URL,以及一些去重的工作。一般无需自己定制Scheduler。
  3. Pipeline:负责抽取结果的处理,包括计算、持久化到文件、数据库等。
  4. Downloader:负责从互联网上下载页面,以便后续处理。一般无需自己实现,默认使用HttpClient,如果页面是动态数据的,则需要自己实现该接口。

PageProcessor

将PageProcessor的定制分为三个部分,分别是爬虫的配置、页面元素的抽取和链接的发现。

代码语言:javascript复制
public class GithubRepoPageProcessor implements PageProcessor {

    // 部分一:抓取网站的相关配置,包括编码、抓取间隔、重试次数等
    private Site site = Site.me().setRetryTimes(3).setSleepTime(1000);

    @Override
    // process是定制爬虫逻辑的核心接口,在这里编写抽取逻辑
    public void process(Page page) {
        
		// 部分二:定义如何抽取页面信息,并保存下来
        page.putField("author", page.getUrl().regex("https://github\.com/(\w )/.*").toString());
        page.putField("name", page.getHtml().xpath("//h1[@class='entry-title public']/strong/a/text()").toString());
        if (page.getResultItems().get("name") == null) {
            // 不符合规则的,跳过该页面
            page.setSkip(true);
        }
        page.putField("readme", page.getHtml().xpath("//div[@id='readme']/tidyText()"));

        // 部分三:从页面发现后续的url地址来抓取
        page.addTargetRequests(page.getHtml().links().regex("(https://github\.com/[\w\-] /[\w\-] )").all());
    }

    @Override
    public Site getSite() {
        return site;
    }

    public static void main(String[] args) {

        Spider.create(new GithubRepoPageProcessor())
                //从该页面开始抓
                .addUrl("https://github.com/code4craft")
                //开启5个线程抓取
                .thread(5)
                //启动爬虫
                .run();
    }
}

爬虫的配置

第一部分关于爬虫的配置,包括编码、抓取间隔、超时时间、重试次数等,也包括一些模拟的参数,例如User Agent、cookie,以及代理的设置。在这里我们先简单设置一下:重试次数为3次,抓取间隔为一秒。

页面元素的抽取

第二部分是爬虫的核心部分:对于下载到的Html页面,你如何从中抽取到你想要的信息?WebMagic里主要使用了三种抽取技术:XPath、正则表达式和CSS选择器。另外,对于JSON格式的内容,可使用JsonPath进行解析。

链接的发现

有了处理页面的逻辑,我们的爬虫就接近完工了!

但是现在还有一个问题:一个站点的页面是很多的,一开始我们不可能全部列举出来,于是如何发现后续的链接,是一个爬虫不可缺少的一部分。

这段代码的分为两部分,page.getHtml().links().regex("(https://github\.com/\w /\w )").all()用于获取所有满足"(https:/ /github.com/w /w )"这个正则表达式的链接,page.addTargetRequests()则将这些链接加入到待抓取的队列中去。

Scheduler

Scheduler是WebMagic中进行URL管理的组件。一般来说,Scheduler包括两个作用:

  1. 对待抓取的URL队列进行管理。
  2. 对已抓取的URL进行去重。

WebMagic内置了几个常用的Scheduler。如果你只是在本地执行规模比较小的爬虫,那么基本无需定制Scheduler,但是了解一下已经提供的几个Scheduler还是有意义的。

说明

备注

DuplicateRemovedScheduler

抽象基类,提供一些模板方法

继承它可以实现自己的功能

QueueScheduler

使用内存队列保存待抓取URL

PriorityScheduler

使用带有优先级的内存队列保存待抓取URL

耗费内存较QueueScheduler更大,但是当设置了request.priority之后,只能使用PriorityScheduler才可使优先级生效

FileCacheQueueScheduler

使用文件保存抓取URL,可以在关闭程序并下次启动时,从之前抓取到的URL继续抓取

需指定路径,会建立.urls.txt和.cursor.txt两个文件

RedisScheduler

使用Redis保存抓取队列,可进行多台机器同时合作抓取

需要安装并启动redis

在0.5.1版本里,我对Scheduler的内部实现进行了重构,去重部分被单独抽象成了一个接口:DuplicateRemover,从而可以为同一个Scheduler选择不同的去重方式,以适应不同的需要,目前提供了两种去重方式。

说明

HashSetDuplicateRemover

使用HashSet来进行去重,占用内存较大

BloomFilterDuplicateRemover

使用BloomFilter来进行去重,占用内存较小,但是可能漏抓页面

所有默认的Scheduler都使用HashSetDuplicateRemover来进行去重(除开RedisScheduler是使用Redis的set进行去重)。如果你的URL较多,使用HashSetDuplicateRemover会比较占用内存,所以也可以尝试一下BloomFilterDuplicateRemover的方式:

代码语言:javascript复制
spider.setScheduler(new QueueScheduler()
     .setDuplicateRemover(new BloomFilterDuplicateRemover(10000000)) //10000000是估计的页面数量
)

注意:如果使用BloomFilterDuplicateRemover,需要单独引入Guava依赖包。

Pipeline

Pileline是抽取结束后,进行处理的部分,它主要用于抽取结果的保存,也可以定制Pileline可以实现一些通用的功能。

WebMagic中已经提供了将结果输出到控制台、保存到文件和JSON格式保存的几个Pipeline:

说明

备注

ConsolePipeline

输出结果到控制台

抽取结果需要实现toString方法

FilePipeline

保存结果到文件

抽取结果需要实现toString方法

JsonFilePipeline

JSON格式保存结果到文件

ConsolePageModelPipeline

(注解模式)输出结果到控制台

FilePageModelPipeline

(注解模式)保存结果到文件

JsonFilePageModelPipeline

(注解模式)JSON格式保存结果到文件

想要持久化的字段需要有getter方法

PageModelPipeline

(注解模式)持久化页面模型

Pipeline的接口定义如下:

代码语言:javascript复制
public interface Pipeline {

    // ResultItems保存了抽取结果,它是一个Map结构,
    // 在page.putField(key,value)中保存的数据,可以通过ResultItems.get(key)获取
    public void process(ResultItems resultItems, Task task);

}

在WebMagic里,一个Spider可以有多个Pipeline,使用Spider.addPipeline()即可增加一个Pipeline。这些Pipeline都会得到处理,例如你可以使用

代码语言:javascript复制
spider.addPipeline(new ConsolePipeline()).addPipeline(new FilePipeline())

实现输出结果到控制台,并且保存到文件的目标。

将结果输出到控制台

代码语言:javascript复制
public void process(Page page) {
    page.addTargetRequests(page.getHtml().links().regex("(https://github\.com/\w /\w )").all());
    page.addTargetRequests(page.getHtml().links().regex("(https://github\.com/\w )").all());
    //保存结果author,这个结果会最终保存到ResultItems中
    page.putField("author", page.getUrl().regex("https://github\.com/(\w )/.*").toString());
    page.putField("name", page.getHtml().xpath("//h1[@class='entry-title public']/strong/a/text()").toString());
    if (page.getResultItems().get("name")==null){
        //设置skip之后,这个页面的结果不会被Pipeline处理
        page.setSkip(true);
    }
    page.putField("readme", page.getHtml().xpath("//div[@id='readme']/tidyText()"));
}

现在我们想将结果保存到控制台,要怎么做呢?ConsolePipeline可以完成这个工作:

代码语言:javascript复制
public class ConsolePipeline implements Pipeline {

    @Override
    public void process(ResultItems resultItems, Task task) {
        System.out.println("get page: "   resultItems.getRequest().getUrl());
        //遍历所有结果,输出到控制台,上面例子中的"author"、"name"、"readme"都是一个key,其结果则是对应的value
        for (Map.Entry<String, Object> entry : resultItems.getAll().entrySet()) {
            System.out.println(entry.getKey()   ":t"   entry.getValue());
        }
    }
}

参考这个例子,你就可以定制自己的Pipeline了——从ResultItems中取出数据,再按照你希望的方式处理即可。

将结果保存到MySQL

如果我们会使用ORM框架来完成持久化到MySQL的工作,就会面临一个问题:这些框架一般都要求保存的内容是一个定义好结构的对象,而不是一个key-value形式的ResultItems。以MyBatis为例,我们使用MyBatis-Spring可以定义这样一个DAO:

代码语言:javascript复制
public interface JobInfoDAO {

    @Insert("insert into JobInfo (`title`,`salary`,`company`,`description`,`requirement`,`source`,`url`,`urlMd5`) values (#{title},#{salary},#{company},#{description},#{requirement},#{source},#{url},#{urlMd5})")
    public int add(LieTouJobInfo jobInfo);
}

我们要做的,就是实现一个Pipeline,将ResultItems和LieTouJobInfo对象结合起来。

注解模式

注解模式下,WebMagic内置了一个PageModelPipeline:

代码语言:javascript复制
public interface PageModelPipeline<T> {

    //这里传入的是处理好的对象
    public void process(T t, Task task);

}

这时,我们可以很优雅的定义一个JobInfoDaoPipeline,来实现这个功能:

代码语言:javascript复制
@Component("JobInfoDaoPipeline")
public class JobInfoDaoPipeline implements PageModelPipeline<LieTouJobInfo> {

    @Resource
    private JobInfoDAO jobInfoDAO;

    @Override
    public void process(LieTouJobInfo lieTouJobInfo, Task task) {
        //调用MyBatis DAO保存结果
        jobInfoDAO.add(lieTouJobInfo);
    }
}
基本Pipeline模式

至此,结果保存就已经完成了!那么如果我们使用原始的Pipeline接口,要怎么完成呢?其实答案也很简单,如果你要保存一个对象,那么就需要在抽取的时候,将它保存为一个对象:

代码语言:javascript复制
public void process(Page page) {
    page.addTargetRequests(page.getHtml().links().regex("(https://github\.com/\w /\w )").all());
    page.addTargetRequests(page.getHtml().links().regex("(https://github\.com/\w )").all());
    GithubRepo githubRepo = new GithubRepo();
    githubRepo.setAuthor(page.getUrl().regex("https://github\.com/(\w )/.*").toString());
    githubRepo.setName(page.getHtml().xpath("//h1[@class='entry-title public']/strong/a/text()").toString());
    githubRepo.setReadme(page.getHtml().xpath("//div[@id='readme']/tidyText()").toString());
    if (githubRepo.getName() == null) {
        //skip this page
        page.setSkip(true);
    } else {
        page.putField("repo", githubRepo);
    }
}

在Pipeline中,只要使用

代码语言:javascript复制
GithubRepo githubRepo = (GithubRepo)resultItems.get("repo");

就可以获取这个对象了。

Downloader

WebMagic的默认Downloader基于HttpClient。一般来说,你无须自己实现Downloader,不过HttpClientDownloader也预留了几个扩展点,以满足不同场景的需求。

另外,你可能希望通过其他方式来实现页面下载,例如使用SeleniumDownloader来渲染动态页面。

用于数据流转的对象

Request 是对URL地址的一层封装,一个Request对应一个URL地址。它是PageProcessor与Downloader交互的载体,也是PageProcessor控制Downloader唯一方式。

Page 代表了从Downloader下载到的一个页面——可能是HTML,也可能是JSON或者其他文本格式的内容。Page是WebMagic抽取过程的核心对象,它提供一些方法可供抽取、结果保存等。

ResultItems 相当于一个Map,它保存PageProcessor处理的结果,供Pipeline使用。它的API与Map很类似,值得注意的是它有一个字段skip,若设置为true,则不应被Pipeline处理。

使用Selectable抽取元素

Selectable相关的抽取元素链式API是WebMagic的一个核心功能。使用Selectable接口,你可以直接完成页面元素的链式抽取,也无需去关心抽取的细节。

在刚才的例子中可以看到,page.getHtml()返回的是一个Html对象,它实现了Selectable接口。这个接口包含一些重要的方法,我将它分为两类:抽取部分和获取结果部分。

API 说明

方法

说明

示例

xpath(String xpath)

使用XPath选择

page.getHtml().xpath(“//div[@class=’title’]”)

$(String selector)

使用Css选择器选择

page.getHtml().$(“div.title”)

$(String selector,String attr)

使用Css选择器选择,并可以指定属性

page.getHtml().$(“div.title”,”text”)

css(String selector)

功能同$(),使用Css选择器选择

page.getHtml().css(“div.title”)

links()

选择所有链接

page.getHtml().links()

regex(String regex)

使用正则表达式抽取

page.getHtml().regex(“(.*?)”)

regex(String regex,int group)

使用正则表达式抽取,并指定捕获组

page.getHtml().regex(“(.*?)”,1)

replace(String regex, String replacement)

使用正则表达式抽取,并替换内容

page.getHtml().replace(“”,””)

get()

返回一条String类型的结果

page.getHtml().links().get()

toString()

功能同get(),返回一条String类型的结果

page.getHtml().links().toString()

all()

返回所有抽取结果

page.getHtml().links().all()

match()

是否有匹配结果

page.getHtml().links().match()

WebMagic里主要使用了三种抽取技术:XPath、正则表达式和CSS选择器。另外,对于JSON格式的内容,可使用JsonPath进行解析。

XPath

XPath 是一门在 XML 文档中查找信息的语言。XPath 可用来在 XML 文档中对元素和属性进行遍历。用于Html也是比较方便的。例如:

代码语言:javascript复制
page.putField("title", page.getHtml().xpath("//div[@class='blog-heading']/div[@class='blog-title']/text()").toString());

该语句的意思“查找所有Class属性为‘blog-heading’的div,并找它的div子节点(Class属性为‘blog-title’),提取该子节点的文本信息”

参考:XPath 语法

CSS选择器

在 CSS 中,选择器是一种模式,用于选择需要添加样式的元素。

代码语言:javascript复制
page.putField("content", page.getHtml().$("div.outlink").toString()); // $("div.outlink") 等价于 css("div.outlink")

该语句的意思“查找所有Class属性为‘outlink’的div”

正则表达式

正则表达式是一种特殊的字符串模式,用于匹配一组字符串,就好比用模具做产品,而正则就是这个模具,定义一种规则去匹配符合规则的字符。

代码语言:javascript复制
page.addTargetRequests(page.getHtml().links().regex("(http://gank\.io/\d /\d /\d )").all());

参考:正则表达式语法

JsonPath

JsonPath是于XPath很类似的一个语言,它用于从Json中快速定位一条内容。WebMagic中使用的JsonPath格式可以参考这里:https://code.google.com/p/json-path/

官方文档中还介绍了通过注解来实现各种功能,非常简便灵活。

使用xPath时要留意,框架作者自定义了几个函数:

Expression

Description

XPath1.0

text(n)

第n个直接文本子节点,为0表示所有

text() only

allText()

所有的直接和间接文本子节点

not support

tidyText()

所有的直接和间接文本子节点,并将一些标签替换为换行,使纯文本显示更整洁

not support

html()

内部html,不包括标签的html本身

not support

outerHtml()

内部html,包括标签的html本身

not support

regex(@attr,expr,group)

这里@attr和group均可选,默认是group0

not support

依赖

使用 maven

webmagic使用maven管理依赖,在项目中添加对应的依赖即可使用webmagic:

代码语言:javascript复制
<dependency>
    <groupId>us.codecraft</groupId>
    <artifactId>webmagic-core</artifactId>
    <version>0.7.3</version>
</dependency>
<dependency>
    <groupId>us.codecraft</groupId>
    <artifactId>webmagic-extension</artifactId>
    <version>0.7.3</version>
</dependency>

WebMagic 使用slf4j-log4j12作为slf4j的实现.如果你自己定制了slf4j的实现,请在项目中去掉此依赖。

代码语言:javascript复制
<exclusions>
    <exclusion>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
    </exclusion>
</exclusions>

0 人点赞