网络爬虫——WebMagic详解(二)

网络爬虫——WebMagic详解(二)这里我们使用的是正则表达式来规定 URL 范围

大家好,欢迎来到IT知识分享网。

5、使用注解编写爬虫

WebMagic支持使用独有的注解风格编写一个爬虫,引入webmagic-extension包即可使用此功能。

在注解模式下,使用一个简单对象加上注解,可以用极少的代码量就完成一个爬虫的编写。对于简单的爬虫,这样写既简单又容易理解,并且管理起来也很方便。这也是WebMagic的一大特色,我戏称它为OEM(Object/Extraction Mapping)。

注解模式的开发方式是这样的:

  1. 首先定义你需要抽取的数据,并编写类。
  2. 在类上写明@TargetUrl注解,定义对哪些URL进行下载和抽取。
  3. 在类的字段上加上@ExtractBy注解,定义这个字段使用什么方式进行抽取。
  4. 定义结果的存储方式。

下面我们仍然以第四章中github的例子,来编写一个同样功能的爬虫,来讲解注解功能的使用。最终编写好的爬虫是这样子的,是不是更加简单?

@TargetUrl("https://github.com/\\w+/\\w+") @HelpUrl("https://github.com/\\w+") public class GithubRepo { 
    @ExtractBy(value = "//h1[@class='entry-title public']/strong/a/text()", notNull = true) private String name; @ExtractByUrl("https://github\\.com/(\\w+)/.*") private String author; @ExtractBy("//div[@id='readme']/tidyText()") private String readme; public static void main(String[] args) { 
    OOSpider.create(Site.me().setSleepTime(1000) , new ConsolePageModelPipeline(), GithubRepo.class) .addUrl("https://github.com/code4craft").thread(5).run(); } } 

5.1、编写Model类

同第四章的例子一样,我们这里抽取一个github项目的名称、作者和简介三个信息,所以我们定义了一个Model类。

public class GithubRepo { 
    private String name; private String author; private String readme; } 

这里省略了getter和setter方法。

在抽取最后,我们会得到这个类的一个或者多个实例,这就是爬虫的结果。

5.2、5.2 TargetUrl与HelpUrl

在第二步,我们仍然要定义如何发现URL。这里我们要先引入两个概念:@TargetUrl和@HelpUrl。

5.2.1、 TargetUrl与HelpUrl

HelpUrl/TargetUrl是一个非常有效的爬虫开发模式,TargetUrl是我们最终要抓取的URL,最终想要的数据都来自这里;而HelpUrl则是为了发现这个最终URL,我们需要访问的页面。几乎所有垂直爬虫的需求,都可以归结为对这两类URL的处理:

  1. 对于博客页,HelpUrl是列表页,TargetUrl是文章页。
  2. 对于论坛,HelpUrl是帖子列表,TargetUrl是帖子详情。
  3. 对于电商网站,HelpUrl是分类列表,TargetUrl是商品详情。

在这个例子中,TargetUrl是最终的项目页,而HelpUrl则是项目搜索页,它会展示所有项目的链接。

有了这些知识,我们就为这个例子定义URL格式:

@TargetUrl("https://github.com/\\w+/\\w+") @HelpUrl("https://github.com/\\w+") public class GithubRepo { 
    …… } 
5.2.2、TargetUrl中的自定义正则表达式

这里我们使用的是正则表达式来规定URL范围。可能细心的朋友,会知道.是正则表达式的保留字符,那么这里是不是写错了呢?其实是这里为了方便,WebMagic自己定制的适合URL的正则表达式,主要由两点改动:

在WebMagic中,从TargetUrl页面得到的URL,只要符合TargetUrl的格式,也是会被下载的。所以即使不指定HelpUrl也是可以的——例如某些博客页总会有“下一篇”链接,这种情况下无需指定HelpUrl。

5.2.3、sourceRegion

TargetUrl还支持定义sourceRegion,这个参数是一个XPath表达式,指定了这个URL从哪里得到——不在sourceRegion的URL不会被抽取。

5.3、使用ExtractBy进行抽取

@ExtractBy是一个用于抽取元素的注解,它描述了一种抽取规则。

5.3.1、初识ExtractBy注解

@ExtractBy注解主要作用于字段,它表示“使用这个抽取规则,将抽取到的结果保存到这个字段中”。例如:

@ExtractBy("//div[@id='readme']/text()") private String readme; 

这里”//div[@id='readme']/text()“是一个XPath表示的抽取规则,而抽取到的结果则会保存到readme字段中

5.3.2、使用其他抽取方式

除了XPath,我们还可以使用其他抽取方式来进行抽取,包括CSS选择器、正则表达式和JsonPath,在注解中指明type之后即可。

@ExtractBy(value = "div.BlogContent", type = ExtractBy.Type.Css) private String content; 
5.3.3、notnull

@ExtractBy包含一个notNull属性,如果熟悉mysql的同学一定能明白它的意思:此字段不允许为空。如果为空,这条抽取到的结果会被丢弃。对于一些页面的关键性属性(例如文章的标题等),设置notnull为true,可以有效的过滤掉无用的页面。

notNull默认为false。

5.3.4、multi(已废弃)

multi是一个boolean属性,它表示这条抽取规则是对应多条记录还是单条记录。对应的,这个字段必须为java.util.List类型。在0.4.3之后,当字段为List类型时,这个属性会自动为true,无须再设置。

  • 0.4.3以前
 @ExtractBy(value = "//div[@class='BlogTags']/a/text()", multi = true) private List<String> tags; 
  • 0.4.3及以后
 @ExtractBy("//div[@class='BlogTags']/a/text()") private List<String> tags; 
5.3.5、ComboExtract(已废弃)

@ComboExtract是一个比较复杂的注解,它可以将多个抽取规则进行组合,组合方式包括”AND/OR”两种方式。

在WebMagic 0.4.3版本中使用了Xsoup 0.2.0版本。在这个版本,XPath支持的语法大大加强了,不但支持XPath和正则表达式组合使用,还支持“|”进行或运算。所以作者认为,ComboExtract这种复杂的组合方式,已经不再需要了。

  • XPath与正则表达式组合
 @ExtractBy("//div[@class='BlogStat']/regex('\\d+-\\d+-\\d+\\s+\\d+:\\d+')") private Date date; 
  • XPath的取或
 @ExtractBy("//div[@id='title']/text() | //title/text()") private String title; 
5.3.6、ExtractByUrl

@ExtractByUrl是一个单独的注解,它的意思是“从URL中进行抽取”。它只支持正则表达式作为抽取规则。

5.4、在类上使用ExtractBy

在之前的注解模式中,我们一个页面只对应一条结果。如果一个页面有多个抽取的记录呢?例如在“美食”的列表页面http://meishi..com/beijing/c/all,我想要抽取所有商户名和优惠信息,该怎么办呢?

在类上使用@ExtractBy注解可以解决这个问题。

在类上使用这个注解的意思很简单:使用这个结果抽取一个区域,让这块区域对应一个结果。

@ExtractBy(value = "//ul[@id=\"promos_list2\"]/li",multi = true) public class Meishi { 
    …… } 

对应的,在这个类中的字段上再使用@ExtractBy的话,则是从这个区域而不是整个页面进行抽取。如果这个时候仍想要从整个页面抽取,则可以设置source = RawHtml。

@TargetUrl("http://meishi..com/beijing/c/all[\\-p2]*") @ExtractBy(value = "//ul[@id=\"promos_list2\"]/li",multi = true) public class Meishi { 
    @ExtractBy("//div[@class=info]/a[@class=title]/h4/text()") private String shopName; @ExtractBy("//div[@class=info]/a[@class=title]/text()") private String promo; public static void main(String[] args) { 
    OOSpider.create(Site.me(), new ConsolePageModelPipeline(), Meishi.class).addUrl("http://meishi..com/beijing/c/all").thread(4).run(); } } 

5.5、结果的类型转换

类型转换(Formatter机制)是WebMagic 0.3.2增加的功能。因为抽取到的内容总是String,而我们想要的内容则可能是其他类型。Formatter可以将抽取到的内容,自动转换成一些基本类型,而无需手动使用代码进行转换。

例如:

@ExtractBy("//ul[@class='pagehead-actions']/li[1]//a[@class='social-count js-social-count']/text()") private int star; 
5.5.1、自动转换支持的类型

自动转换支持所有基本类型和装箱类型。

基本类型 装箱类型
int Integer
long Long
double Double
float Float
short Short
char Character
byte Byte
boolean Boolean

另外,还支持java.util.Date类型的转换。但是在转换时,需要指定Date的格式。格式按照JDK的标准来定义

@Formatter("yyyy-MM-dd HH:mm") @ExtractBy("//div[@class='BlogStat']/regex('\\d+-\\d+-\\d+\\s+\\d+:\\d+')") private Date date; 
5.5.2、显式指定转换类型

一般情况下,Formatter会根据字段类型进行转换,但是特殊情况下,我们会需要手动指定类型。这主要发生在字段是List类型的时候。

@Formatter(value = "",subClazz = Integer.class) @ExtractBy(value = "//div[@class='id']/text()", multi = true) private List<Integer> ids; 
5.5.3、自定义Formatter(TODO)

实际上,除了自动类型转换之外,Formatter还可以做一些结果的后处理的事情。例如,我们有一种需求场景,需要将抽取的结果作为结果的一部分,拼接上一部分字符串来使用。在这里,我们定义了一个StringTemplateFormatter。

public class StringTemplateFormatter implements ObjectFormatter<String> { 
    private String template; @Override public String format(String raw) throws Exception { 
    return String.format(template, raw); } @Override public Class<String> clazz() { 
    return String.class; } @Override public void initParam(String[] extra) { 
    template = extra[0]; } } 

那么,我们就能在抽取之后,做一些简单的操作了!

@Formatter(value = "author is %s",formatter = StringTemplateFormatter.class) @ExtractByUrl("https://github\\.com/(\\w+)/.*") private String author; 

5.6、一个完整的流程

到之前为止,我们了解了URL和抽取相关API,一个爬虫已经基本编写完成了。

@TargetUrl("https://github.com/\\w+/\\w+") @HelpUrl("https://github.com/\\w+") public class GithubRepo { 
    @ExtractBy(value = "//h1[@class='entry-title public']/strong/a/text()", notNull = true) private String name; @ExtractByUrl("https://github\\.com/(\\w+)/.*") private String author; @ExtractBy("//div[@id='readme']/tidyText()") private String readme; } 
5.6.1、爬虫的创建和启动

注解模式的入口是OOSpider,它继承了Spider类,提供了特殊的创建方法,其他的方法是类似的。创建一个注解模式的爬虫需要一个或者多个Model类,以及一个或者多个PageModelPipeline——定义处理结果的方式。

public static OOSpider create(Site site, PageModelPipeline pageModelPipeline, Class... pageModels); 
5.6.2、 PageModelPipeline

注解模式下,处理结果的类叫做PageModelPipeline,通过实现它,你可以自定义自己的结果处理方式。

public interface PageModelPipeline<T> { 
    public void process(T t, Task task); } 

PageModelPipeline与Model类是对应的,多个Model可以对应一个PageModelPipeline。除了创建时,你还可以通过

public OOSpider addPageModel(PageModelPipeline pageModelPipeline, Class... pageModels) 

方法,在添加一个Model的同时,可以添加一个PageModelPipeline。

5.6.3、结语

好了,现在我们来完成这个例子:

@TargetUrl("https://github.com/\\w+/\\w+") @HelpUrl("https://github.com/\\w+") public class GithubRepo { 
    @ExtractBy(value = "//h1[@class='entry-title public']/strong/a/text()", notNull = true) private String name; @ExtractByUrl("https://github\\.com/(\\w+)/.*") private String author; @ExtractBy("//div[@id='readme']/tidyText()") private String readme; public static void main(String[] args) { 
    OOSpider.create(Site.me().setSleepTime(1000) , new ConsolePageModelPipeline(), GithubRepo.class) .addUrl("https://github.com/code4craft").thread(5).run(); } } 

5.7、AfterExtractor

有的时候,注解模式无法满足所有需求,我们可能还需要写代码完成一些事情,这个时候就要用到AfterExtractor接口了。

public interface AfterExtractor { 
    public void afterProcess(Page page); } 

afterProcess方法会在抽取结束,字段都初始化完毕之后被调用,可以处理一些特殊的逻辑。

//TargetUrl的意思是只有以下格式的URL才会被抽取出生成model对象 //这里对正则做了一点改动,'.'默认是不需要转义的,而'*'则会自动被替换成'.*',因为这样描述URL看着舒服一点... //继承jfinal中的Model //实现AfterExtractor接口可以在填充属性后进行其他操作 @TargetUrl("http://my.oschina.net/flashsword/blog/*") public class OschinaBlog extends Model<OschinaBlog> implements AfterExtractor { 
    //用ExtractBy注解的字段会被自动抽取并填充 //默认是xpath语法 @ExtractBy("//title") private String title; //可以定义抽取语法为Css、Regex等 @ExtractBy(value = "div.BlogContent", type = ExtractBy.Type.Css) private String content; //multi标注的抽取结果可以是一个List @ExtractBy(value = "//div[@class='BlogTags']/a/text()", multi = true) private List<String> tags; @Override public void afterProcess(Page page) { 
    //jfinal的属性其实是一个Map而不是字段,没关系,填充进去就是了 this.set("title", title); this.set("content", content); this.set("tags", StringUtils.join(tags, ",")); //保存 save(); } public static void main(String[] args) { 
    C3p0Plugin c3p0Plugin = new C3p0Plugin("jdbc:mysql://127.0.0.1/blog?characterEncoding=utf-8", "blog", "password"); c3p0Plugin.start(); ActiveRecordPlugin activeRecordPlugin = new ActiveRecordPlugin(c3p0Plugin); activeRecordPlugin.addMapping("blog", OschinaBlog.class); activeRecordPlugin.start(); //启动webmagic OOSpider.create(Site.me().addStartUrl("http://my.oschina.net/flashsword/blog/"), OschinaBlog.class).run(); } } 

6、组件的使用和定制

在第一章里,我们提到了WebMagic的组件。WebMagic的一大特色就是可以灵活的定制组件功能,实现你自己想要的功能。

在Spider类里,PageProcessor、Downloader、Scheduler和Pipeline四个组件都是Spider的字段。除了PageProcessor是在Spider创建的时候已经指定,Downloader、Scheduler和Pipeline都可以通过Spider的setter方法来进行配置和更改。

方法 说明 示例
setScheduler() 设置Scheduler spipder.setScheduler(new FileCacheQueueScheduler(“D:\data\webmagic”))
setDownloader() 设置Downloader spipder.setDownloader(new SeleniumDownloader()))
addPipeline() 设置Pipeline,一个Spider可以有多个Pipeline spipder.addPipeline(new FilePipeline())

6.1、使用和定制Pipeline

Pileline是抽取结束后,进行处理的部分,它主要用于抽取结果的保存,也可以定制Pileline可以实现一些通用的功能。在这一节中,我们会对Pipeline进行介绍,并用两个例子来讲解如何定制Pipeline。

6.1.1、Pipeline介绍

Pipeline的接口定义如下:

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

可以看到,Pipeline其实就是将PageProcessor抽取的结果,继续进行了处理的,其实在Pipeline中完成的功能,你基本上也可以直接在PageProcessor实现,那么为什么会有Pipeline?有几个原因:

  1. 为了模块分离。“页面抽取”和“后处理、持久化”是爬虫的两个阶段,将其分离开来,一个是代码结构比较清晰,另一个是以后也可能将其处理过程分开,分开在独立的线程以至于不同的机器执行。
  2. Pipeline的功能比较固定,更容易做成通用组件。每个页面的抽取方式千变万化,但是后续处理方式则比较固定,例如保存到文件、保存到数据库这种操作,这些对所有页面都是通用的。WebMagic中就已经提供了控制台输出、保存到文件、保存为JSON格式的文件几种通用的Pipeline。

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

spider.addPipeline(new ConsolePipeline()).addPipeline(new FilePipeline()) 

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

6.1.2、将结果输出到控制台

在介绍PageProcessor时,我们使用了GithubRepoPageProcessor作为例子,其中某一段代码中,我们将结果进行了保存:

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可以完成这个工作:

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()); } } } 
6.1.3、将结果保存到MySQL

这里先介绍一个demo项目:jobhunter。它是一个集成了Spring,使用WebMagic抓取招聘信息,并且使用Mybatis持久化到Mysql的例子。我们会用这个项目来介绍如果持久化到Mysql。

Java里,我们有很多方式将数据保存到MySQL,例如jdbc、dbutils、spring-jdbc、MyBatis等工具。这些工具都可以完成同样的事情,只不过功能和使用复杂程度不一样。如果使用jdbc,那么我们只需要从ResultItems取出数据,进行保存即可。

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

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:

public interface PageModelPipeline<T> { 
    //这里传入的是处理好的对象 public void process(T t, Task task); } 

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

@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接口,要怎么完成呢?其实答案也很简单,如果你要保存一个对象,那么就需要在抽取的时候,将它保存为一个对象:

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中,只要使用

GithubRepo githubRepo = (GithubRepo)resultItems.get("repo"); 

就可以获取这个对象了。

6.1.4、WebMagic已经提供的几个Pipeline

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

说明 备注
ConsolePipeline 输出结果到控制台 抽取结果需要实现toString方法
FilePipeline 保存结果到文件 抽取结果需要实现toString方法
JsonFilePipeline JSON格式保存结果到文件
ConsolePageModelPipeline (注解模式)输出结果到控制台
FilePageModelPipeline (注解模式)保存结果到文件
JsonFilePageModelPipeline (注解模式)JSON格式保存结果到文件 想要持久化的字段需要有getter方法

6.2、使用和定制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会比较占用内存,所以也可以尝试以下BloomFilterDuplicateRemover1,使用方式:

spider.setScheduler(new QueueScheduler() .setDuplicateRemover(new BloomFilterDuplicateRemover()) //是估计的页面数量 ) 

6.3、使用和定制Downloader

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

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

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/128878.html

(0)
上一篇 2025-08-28 16:20
下一篇 2025-08-28 16:26

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信