各位看官可以关注博主个人博客,了解更多信息。
作者:Surpasser
链接地址:https://surpass.org.cn
前言
WebMagic介绍
Java的可伸缩Web搜寻器框架。官方网站: http://webmagic.io/
一款爬虫框架是WebMagic,其底层使用的HttpClient和Jsoup。
WebMagic项目代码分为核心和扩展两部分。核心部分(webmagic-core)是一个精简的、模块化的爬虫实现,而扩展部分则包括一些便利的、实用性的功能。
WebMagic的设计目标是尽量的模块化,并体现爬虫的功能特点。这部分提供非常简单、灵活的API,在基本不改变开发模式的情况下,编写一个爬虫。
结构介绍
WebMagic的结构分为Downloader、PageProcessor、Scheduler、Pipeline四大组件,并由Spider将它们彼此组织起来。这四大组件对应爬虫生命周期中的下载、处理、管理和持久化等功能。WebMagic的设计参考了Scapy,但是实现方式更Java化一些。
Spider则将这几个组件组织起来,让它们可以互相交互,流程化的执行,可以认为Spider是一个大的容器,它也是WebMagic逻辑的核心。
WebMagic四个组件
-
Downloader
Downloader负责从互联网上下载页面,以便后续处理。WebMagic默认使用了Apache HttpClient作为下载工具。
-
PageProcessor
PageProcessor负责解析页面,抽取有用信息,以及发现新的链接。WebMagic使用Jsoup作为HTML解析工具,并基于其开发了解析XPath的工具Xsoup。
在这四个组件中,PageProcessor对于每个站点每个页面都不一样,是需要使用者定制的部分。
-
Scheduler
Scheduler负责管理待抓取的URL,以及一些去重的工作。WebMagic默认提供了JDK的内存队列来管理URL,并用集合来进行去重。也支持使用Redis进行分布式管理。
-
Pipeline
Pipeline负责抽取结果的处理,包括计算、持久化到文件、数据库等。WebMagic默认提供了“输出到控制台”和“保存到文件”两种结果处理方案。
Pipeline定义了结果保存的方式,如果你要保存到指定数据库,则需要编写对应的Pipeline。对于一类需求一般只需编写一个Pipeline。
用于数据流转的对象
-
Request
Request是对URL地址的一层封装,一个Request对应一个URL地址。
它是PageProcessor与Downloader交互的载体,也是PageProcessor控制Downloader唯一方式。
除了URL本身外,它还包含一个Key-Value结构的字段extra。你可以在extra中保存一些特殊的属性,然后在其他地方读取,以完成不同的功能。例如附加上一个页面的一些信息等。
-
Page
Page代表了从Downloader下载到的一个页面——可能是HTML,也可能是JSON或者其他文本格式的内容。
Page是WebMagic抽取过程的核心对象,它提供一些方法可供抽取、结果保存等。
-
ResultItems
ResultItems相当于一个Map,它保存PageProcessor处理的结果,供Pipeline使用。它的API与Map很类似,值得注意的是它有一个字段skip,若设置为true,则不应被Pipeline处理。
正文
添加依赖
-
GitHub拉取WebMagic文件,地址https://github.com/code4craft/webmagic
-
找到webmagic-core,利用mvn安装到本地仓库。
-
添加常规maven坐标。
<dependencies> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.1.3.RELEASE</version> </dependency><!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>2.1.6.RELEASE</version> </dependency><!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.34</version> </dependency><!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-core --> <dependency> <groupId>us.codecraft</groupId> <artifactId>webmagic-core</artifactId> <version>0.6.1</version> </dependency> <!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-extension --> <dependency> <groupId>us.codecraft</groupId> <artifactId>webmagic-extension</artifactId> <version>0.6.1</version> </dependency> <!--WebMagic对布隆过滤器的支持--> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>16.0</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency> </dependencies>
注:博主这里使用得是spring boot+springdatajpa,为什么要在GitHub拉去核心代码,因为现版本对SSL处理不完全,对在爬取只支持SSL v1.2的网站会有SSL的异常抛出。
解决方案:
-
等作者的0.7.4的版本发布
-
直接从github上下载最新的代码,安装到本地仓库
这是开发这自己得解决方法:https://github.com/code4craft/webmagic/issues/701
配置文件
WebMagic使用slf4j-log4j12作为slf4j的实现。
添加log4j.properties配置文件
log4j.rootLogger=INFO,A1
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-[%p] %m%n
添加application.properties配置文件
#DB Configuration:
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/crawler
spring.datasource.username=root
spring.datasource.password=root
#JPA Configuration:
spring.jpa.database=MySQL
spring.jpa.show-sql=true
数据库表
CREATE TABLE `job_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`company_name` varchar(100) DEFAULT NULL COMMENT '公司名称',
`company_addr` varchar(200) DEFAULT NULL COMMENT '公司联系方式',
`company_info` text COMMENT '公司信息',
`job_name` varchar(100) DEFAULT NULL COMMENT '职位名称',
`job_addr` varchar(50) DEFAULT NULL COMMENT '工作地点',
`job_info` text COMMENT '职位信息',
`salary_min` int(10) DEFAULT NULL COMMENT '薪资范围,最小',
`salary_max` int(10) DEFAULT NULL COMMENT '薪资范围,最大',
`url` varchar(150) DEFAULT NULL COMMENT '招聘信息详情页',
`time` varchar(10) DEFAULT NULL COMMENT '职位最近发布时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='招聘信息';
实体类
@Entity
public class JobInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String companyName;
private String companyAddr;
private String companyInfo;
private String jobName;
private String jobAddr;
private String jobInfo;
private Integer salaryMin;
private Integer salaryMax;
private String url;
private String time;
get/set
}
Dao
public interface JobInfoDao extends JpaRepository<JobInfo, Long>{
}
Service
public interface JobInfoService {
/**
* 保存数据
*
* @param jobInfo
*/
public void save(JobInfo jobInfo);
/**
* 根据条件查询数据
*
* @param jobInfo
* @return
*/
public List<JobInfo> findJobInfo(JobInfo jobInfo);
}
ServiceImpl
@Service
public class JobInfoServiceImpl implements JobInfoService {
@Autowired
private JobInfoDao jobInfoDao;
@Override
@Transactional
public void save(JobInfo jobInfo) {
//先从数据库查询数据,根据发布日期查询和url查询
JobInfo param = new JobInfo();
param.setUrl(jobInfo.getUrl());
param.setTime(jobInfo.getTime());
List<JobInfo> list = this.findJobInfo(param);
if (list.size() == 0) {
//没有查询到数据则新增或者修改数据
this.jobInfoDao.saveAndFlush(jobInfo);
}
}
@Override
public List<JobInfo> findJobInfo(JobInfo jobInfo) {
Example example = Example.of(jobInfo);
List<JobInfo> list = this.jobInfoDao.findAll(example);
return list;
}
}
启动类
@SpringBootApplication
@EnableScheduling//开启定时任务
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
功能实现
@Component
public class JobProcessor implements PageProcessor {
@Autowired
private DataPipline dataPipline;
private String url = "https://search.51job.com/list/010000,000000,0000,00,9,99,python,2,1.html?lang=c&stype=&postchannel=0000&workyear=99&cotype=99°reefrom=99&jobterm=99&companysize=99&providesalary=99&lonlat=0%2C0&radius=-1&ord_field=0&confirmdate=9&fromType=&dibiaoid=0&address=&line=&specialarea=00&from=&welfare=";
@Override
public void process(Page page) {
List<Selectable> list = page.getHtml().css("div#resultList div.el").nodes();
if (list.size()==0){
//详情页
this.saveJobInfo(page);
}else {
//列表页
for (Selectable selectable : list) {
//获取url地址
String jobInfoUrl = selectable.links().toString();
//把url放入任务队列中
page.addTargetRequest(jobInfoUrl);
}
//获取下一页url
String s = page.getHtml().css("div.p_in li.bk").nodes().get(1).links().toString();
//下一页放入任务队列
page.addTargetRequest(s);
}
}
//解析招聘详情信息,对数据进行封装
public void saveJobInfo(Page page){
Html html = page.getHtml();
if (html==null){
return;
}
JobInfo jobInfo = new JobInfo();
String companyName = html.css("div.cn p.cname a", "text").toString();
jobInfo.setCompanyName(companyName);
String companyInfo = Jsoup.parse(html.css("div.tmsg").toString()).text();
jobInfo.setCompanyInfo(companyInfo);
String jobName = Jsoup.parse(html.css("div.cn h1").toString()).text();
jobInfo.setJobName(jobName);
String text = Jsoup.parse(html.css("p.ltype").toString()).text();
String address = text.substring(0, 2);
//String jobAddr = Jsoup.parse(html.css("div.bmsg p.fp").nodes().get(1).toString()).text();
jobInfo.setJobAddr(address);
String jobInfos = Jsoup.parse(html.css("div.job_msg").toString()).text();
jobInfo.setJobInfo(jobInfos);
String salary = html.css("div.cn strong", "text").toString();
if (StringUtils.isNotBlank(salary)){
Integer[] texts = MathSalary.getSalary(salary);
jobInfo.setSalaryMin(texts[0]);
jobInfo.setSalaryMax(texts[1]);
}
String url = page.getUrl().toString();
jobInfo.setUrl(url);
String times = Jsoup.parse(html.css("p.ltype").toString()).text();
String[] split = times.split("-");
String time = split[0].substring(split[0].length() - 2) +"-"+ split[1].substring(0, 2);
jobInfo.setTime(time);
//保存(内存)
page.putField("jobInfo",jobInfo);
}
private Site site = Site.me()
.setCharset("gbk") //编码
.setTimeOut(20*1000) //超时
.setRetryTimes(5) //重试次数
.setRetrySleepTime(3000); //重试时间间隔
@Override
public Site getSite() {
return site;
}
//等待一秒后执行 一百秒执行一次
@Scheduled(initialDelay = 1000,fixedDelay = 100000)
public void process(){
Spider.create(new JobProcessor())
.addUrl(url)
.setScheduler(new QueueScheduler().setDuplicateRemover(new BloomFilterDuplicateRemover(100000))) //布隆过滤器
.thread(10) //10线程爬取
.addPipeline(this.dataPipline) //添加Pipeline配置
.run();
}
}
使用和定制Pipline输出
在WebMagic中,Pileline是抽取结束后,进行处理的部分,它主要用于抽取结果的保存,也可以定制Pileline可以实现一些通用的功能。在这里我们会定制Pipeline实现数据导入到数据库中。
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都会得到处理,例如可以使用:
spider.addPipeline(new ConsolePipeline())
.addPipeline(new FilePipeline())
实现输出结果到控制台,并且保存到文件的目标。
类 | 说明 | 备注 |
---|---|---|
ConsolePipeline | 输出结果到控制台 | 抽取结果需要实现toString方法 |
FilePipeline | 保存结果到文件 | 抽取结果需要实现toString方法 |
JsonFilePipeline | JSON格式保存结果到文件 | |
ConsolePageModelPipeline | (注解模式)输出结果到控制台 | |
FilePageModelPipeline | (注解模式)保存结果到文件 | |
JsonFilePageModelPipeline | (注解模式)JSON格式保存结果到文件 | 想持久化的字段需要有getter方法 |
自定义Pipeline导入保存数据
@Component
public class SpringDataPipeline implements Pipeline {
@Autowired
private JobInfoService jobInfoService;
@Override
public void process(ResultItems resultItems, Task task) {
//获取需要保存到MySQL的数据
JobInfo jobInfo = resultItems.get("jobInfo");
//判断获取到的数据不为空
if(jobInfo!=null) {
//如果有值则进行保存
this.jobInfoService.save(jobInfo);
}
}
}
启动启动类
输出结果:
尾言
这里记录了博主学习得一个爬取例子,如果可以,以后直接参考这篇文章即可完成对简单页面得爬取。ok!