目录
项目只是简单的爬虫,简单的涉及到process、Scheduler和Pipeline
介绍:
WebMagic是一个简单灵活的Java爬虫框架。基于WebMagic,你可以快速开发出一个高效、易维护的爬虫。
特性:
-
简单的API,可快速上手
-
模块化的结构,可轻松扩展
-
提供多线程和分布式支持
详细介绍和相关开发请看
中文开发文档:http://webmagic.io/docs/zh/
开发前,先弄懂webmagic四大组件以及架构图
架构图
四大组件:
1).Downloader:
Downloader负责从互联网上下载页面,以便后续处理。WebMagic默认使用了Apache HttpClient作为下载工具。
2).PageProcessor
PageProcessor负责解析页面,抽取有用信息,以及发现新的链接。WebMagic使用Jsoup作为HTML解析工具,并基于其开发了解析XPath的工具Xsoup。
在这四个组件中,PageProcessor
对于每个站点每个页面都不一样,是需要使用者定制的部分。
3).Scheduler
Scheduler负责管理待抓取的URL,以及一些去重的工作。WebMagic默认提供了JDK的内存队列来管理URL,并用集合来进行去重。也支持使用Redis进行分布式管理。
除非项目有一些特殊的分布式需求,否则无需自己定制Scheduler。
4).Pipeline
Pipeline负责抽取结果的处理,包括计算、持久化到文件、数据库等。WebMagic默认提供了“输出到控制台”和“保存到文件”两种结果处理方案。
Pipeline
定义了结果保存的方式,如果你要保存到指定数据库,则需要编写对应的Pipeline。对于一类需求一般只需编写一个Pipeline
。
以下代码是我自己学习实例,如果有错,请指出。
项目只是简单的爬虫,简单的涉及到process、Scheduler和Pipeline
项目结构如下:
1. pom.xml
引入webmagic坐标
<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>
注意:
此jar包已包含slf4j-log4j12的jar包,故不要再次导入slf4j-log4j12包,引起jar包冲突
如果自己定制了slf4j的实现,请在项目中去掉此依赖。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mzp</groupId>
<artifactId>SpiderCSDN</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<!-- spring版本号 -->
<spring.version>4.3.8.RELEASE</spring.version>
<!-- aspectj版本号 -->
<aspectj.version>1.6.12</aspectj.version>
<!-- jstl标签版本 -->
<jstl.version>1.2</jstl.version>
<!-- mybatis版本号 -->
<mybatis.version>3.4.5</mybatis.version>
<!-- mybatis-spring整合包版本 -->
<mybatis.spring.version>1.3.1</mybatis.spring.version>
<!-- mysql版本号 -->
<mysql.version>5.1.30</mysql.version>
<!-- dbcp数据源连接池jar包 -->
<dbcp.version>1.2.2</dbcp.version>
<!-- log4j日志包版本 -->
<slf4j.version>1.7.7</slf4j.version>
<log4j.version>1.2.17</log4j.version>
<!-- commons-lang版本 -->
<commons.lang.version>2.6</commons.lang.version>
<!-- 文件上传包版本 -->
<fileupload.version>1.3.1</fileupload.version>
<commonsio.version>2.4</commonsio.version>
<commonscodec.version>1.9</commonscodec.version>
<!-- 分页助手版本 -->
<pagehelper.version>4.1.6</pagehelper.version>
<jsqlparser.version>0.9.6</jsqlparser.version>
<!-- 通用mapper版本 -->
<!-- <tk.mybatis.version>3.4.6</tk.mybatis.version> -->
<tk.mybatis.version>4.0.0</tk.mybatis.version>
<!-- json支持,jackson包版本 -->
<jackson.version>2.7.4</jackson.version>
<!-- shiro -->
<shiro.version>1.2.3</shiro.version>
<!-- commons.logging -->
<commons.logging.version>1.2</commons.logging.version>
<!-- 注意,该jar包已包含基础的mybatis,故不需要再次引入mybatisjar包 -->
<mybatis.plus.version>2.1.9</mybatis.plus.version>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>26.0-jre</version>
</dependency>
<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>
<!-- https://mvnrepository.com/artifact/commons-httpclient/commons-httpclient -->
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
<!-- 注意,该jar包已包含基础的mybatis,故不需要再次引入mybatisjar包 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- alibaba json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!-- 分页助手 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>${pagehelper.version}</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>${jsqlparser.version}</version>
</dependency>
<!-- 通用mapper -->
<!-- <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper</artifactId>
<version>${tk.mybatis.version}</version> </dependency> -->
<!-- commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<!-- springmvc依赖包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
<!-- JSTL标签类 -->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
</dependency>
<!-- mybatis核心包 -->
<!-- <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId>
<version>${mybatis.version}</version> </dependency> -->
<!-- mybatis-spring整合包 -->
<!-- <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId>
<version>${mybatis.spring.version}</version> </dependency> -->
<!-- 导入Mysql数据库链接jar包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- 导入dbcp数据源连接池jar包 -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>${dbcp.version}</version>
</dependency>
<!-- 日志文件管理包 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency><!-- <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId> <version>${slf4j.version}</version>
</dependency> -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>${commons.lang.version}</version>
</dependency>
<!-- logging包 -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>${commons.logging.version}</version>
</dependency>
<!-- jsp依赖包,只在编译时需要 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>
<!-- 文件上传组件包 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${fileupload.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commonsio.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commonscodec.version}</version>
</dependency>
<!-- jackson依赖包 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- 资源文件拷贝插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.7</version>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- java编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<!-- 配置Tomcat插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<!-- tomcat 的端口号 -->
<port>8090</port>
<!-- 访问应用的路径 -->
<path>/</path>
<!-- URL按UTF-8进行编码,解决中文参数乱码 -->
<uriEncoding>UTF-8</uriEncoding>
<!-- tomcat名称 -->
<server>tomcat7</server>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<resources>
<!-- 使用Maven部署的时候,xml和properties配置文件也一起部署到Tomcat -->
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<!-- 默认是以下配置 -->
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.*</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
2.常量类 常用的正则表达式
package com.mzp.csdn;
import java.io.Serializable;
public interface SpiderConstant extends Serializable {
// 规则页
final static String CSDN_URL_REGEXP = "https://blog.csdn.net/.+";
// 首页
final static String CSDN_HOMEPAGE = "https://www.csdn.net/";
// 首页分类匹配
final static String CSDN_HOMEPAGE_REGEXP = "https://www.csdn.net/.+/.+";
// 文章article
final static String CSDN_ARTICLE = "https://blog.csdn.net/.+/article/details/\\d+";
// 游戏开发
final static String CSDN_GAME = "https://www.csdn.net/nav/game";
// 数据库
final static String CSDN_DB = "https://www.csdn.net/nav/db";
// 广告
final static String CSDN_AD = "";
// 其他
final static String CSDN_OTHER = "";
}
3.processor
注意该processor的main方法中也有Scheduler
package com.mzp.csdn.spider;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import com.mzp.csdn.SpiderConstant;
import com.mzp.csdn.service.SpiderService;
import com.mzp.csdn.spider.pipeline.impl.CsdnConsolePipeline;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.scheduler.BloomFilterDuplicateRemover;
import us.codecraft.webmagic.scheduler.FileCacheQueueScheduler;
public class CsdnProcessor implements PageProcessor, SpiderConstant {
// 不符合CSDN网址规则的其他连接
public static HashMap<String, String> otherUrlMap = new LinkedHashMap<String, String>();
public static Map<String,Object> map = new LinkedHashMap<String,Object>();
@Autowired
private SpiderService spiderService;
// 部分一:抓取网站的相关配置,包括编码、抓取间隔、重试次数等
// site设置详细参考http://webmagic.io/docs/zh/posts/ch4-basic-page-processor/spider-config.html
// setCharset -- 编码
// setSleepTime -- 休眠时间 单位毫秒
// setCycleRetryTimes -- 设置循环重试次数
// 其中循环重试cycleRetry是0.3.0版本加入的机制。该机制会将下载失败的url重新放入队列尾部重试,直到达到重试次数,以保证不因为某些网络原因漏抓页面。
private Site site = Site.me().setCharset("utf-8").setSleepTime(1000).setCycleRetryTimes(3);
@Override
public Site getSite() {
return site;
}
// process是定制爬虫逻辑的核心接口,在这里编写抽取逻辑
@Override
public void process(Page page) {
// 获取当前页url
String url = page.getUrl().toString();
String title = page.getHtml().xpath("/html/head/title/text()").toString();
// 判断url是否符合规则
if (url.matches(CSDN_HOMEPAGE) || url.matches(CSDN_URL_REGEXP)||url.matches(CSDN_HOMEPAGE_REGEXP)) {
// 判断url类型
// 1.如果是首页,则获取游戏制作和数据库分类
if (url.equals(CSDN_HOMEPAGE)) {
// 获取数据库
String dbString = page.getHtml().xpath("//div[@class='nav_com']/ul/li[8]/a/@href").toString();
// 游戏开发
String gameDevString = page.getHtml().xpath("//div[@class='nav_com']/ul/li[10]/a/@href").toString();
// https://www.csdn.net/nav/db
// 手动拼接完整的url
dbString = CSDN_HOMEPAGE + dbString.substring(1);
gameDevString = CSDN_HOMEPAGE + gameDevString.substring(1);
List<String> list = new ArrayList<String>();
list.add(gameDevString);
list.add(dbString);
page.addTargetRequests(list);
}
// 2. 如果是数据库页面或者游戏开发页面
if (url.equalsIgnoreCase(CSDN_DB) || url.equalsIgnoreCase(CSDN_GAME)) {
List<String> listDBGAME = page.getHtml().xpath("//div[@class='title']/h2/a/@href").all();
page.addTargetRequests(listDBGAME);
}
// 3. 如果是文章的页面,例如:https://blog.csdn.net/xx5533/article/details/81773265,则爬/存储标题,且爬所有的url
if(url.matches(CSDN_ARTICLE)) {
String artTitle = page.getHtml().xpath("//h1[@class='title-article']/text()").toString();
map.put(artTitle, url);
List<String> artList = page.getHtml().links().regex(CSDN_URL_REGEXP).all();
page.addTargetRequests(artList);
page.putField(artTitle,url);
}
} else { // 不符合首页和博客,则存储起来。
otherUrlMap.put(title, url);
}
}
public static void main(String[] args) {
System.out.println("爬虫开始--------------------");
Spider.create(new CsdnProcessor())
.addUrl(CSDN_HOMEPAGE)
// url去重,需指定路径,会建立.urls.txt和.cursor.txt两个文件
// 这里属于Scheduler,scheduler官网http://webmagic.io/docs/zh/posts/ch6-custom-componenet/scheduler.html
.setScheduler(new FileCacheQueueScheduler("/home/mozhipeng/下载/1/").setDuplicateRemover(new BloomFilterDuplicateRemover(10000)))
// 控制台打印
.addPipeline(new CsdnConsolePipeline())
// 文件下载
//.addPipeline(new CsdnFilePipeline())
.thread(2)
.run();
System.out.println("爬虫结束---------------------");
}
}
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会比较占用内存,所以也可以尝试以下BloomFilterDuplicateRemover1,使用方式:
spider.setScheduler(new QueueScheduler()
.setDuplicateRemover(new BloomFilterDuplicateRemover(10000000)) //10000000是估计的页面数量
)
1. 0.6.0版本后,如果使用BloomFilterDuplicateRemover,需要单独引入Guava依赖包。 ↩
3. pipeline
3.1
package com.mzp.csdn.spider.pipeline;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;
/**
* @brief Pileline是抽取结束后,进行处理的部分,它主要用于抽取结果的保存,也可以定制Pileline可以实现一些通用的功能。
* @author: mozhipeng
* @date: 2018年9月25日 上午9:18:29
* @note:
*/
public interface CsdnPipeline extends Pipeline{
public void process(ResultItems resultItems,Task task);
}
3.2 控制台输出
package com.mzp.csdn.spider.pipeline.impl;
import java.util.Map;
import com.mzp.csdn.spider.pipeline.CsdnPipeline;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
public class CsdnConsolePipeline implements CsdnPipeline {
@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());
}
}
}
3.3 文件输出
package com.mzp.csdn.spider.pipeline.impl;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Map;
import org.apache.commons.codec.digest.DigestUtils;
import com.mzp.csdn.spider.pipeline.CsdnPipeline;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.FilePipeline;
public class CsdnFilePipeline extends FilePipeline implements CsdnPipeline {
public CsdnFilePipeline() {
setPath("/home/mozhipeng/下载/1");
}
public CsdnFilePipeline(String path) {
setPath(path);
}
@Override
public void process(ResultItems resultItems, Task task) {
String path = this.path + PATH_SEPERATOR + task.getUUID() + PATH_SEPERATOR;
try {
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(
new FileOutputStream(getFile(path + resultItems.getRequest().getUrl().toString() + ".html")),
"UTF-8"));
printWriter.println("url:\t" + resultItems.getRequest().getUrl());
for (Map.Entry<String, Object> entry : resultItems.getAll().entrySet()) {
if (entry.getValue() instanceof Iterable) {
Iterable value = (Iterable) entry.getValue();
printWriter.println(entry.getKey() + ":");
for (Object o : value) {
printWriter.println(o);
}
} else {
printWriter.println(entry.getKey() + ":\t" + entry.getValue());
}
}
printWriter.close();
} catch (IOException e) {
}
}
}