webmagic是一个开源的Java垂直爬虫框架,目标是简化爬虫的开发流程,让开发者专注于逻辑功能的开发。webmagic的核心非常简单,但是覆盖爬虫的整个流程,也是很好的学习爬虫开发的材料。 官方文档
WebMagic 初探
WebMagic框架包含四个组件,PageProcessor、Scheduler、Downloader和Pipeline。
这四大组件对应爬虫生命周期中的处理、管理、下载和持久化等功能。
这四个组件都是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 四大组件
- PageProcessor:负责解析页面,抽取有用信息,以及发现新的链接。需要自己定义。
- Scheduler:负责管理待抓取的URL,以及一些去重的工作。一般无需自己定制Scheduler。
- Pipeline:负责抽取结果的处理,包括计算、持久化到文件、数据库等。
- 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包括两个作用:
- 对待抓取的URL队列进行管理。
- 对已抓取的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>