1、WebMagic的四大组件
-
Downloader
Downloader负责从互联网上下载页面,以便后续处理。WebMagic默认使用了Apache HttpClient作为下载工具。 -
PageProcessor
PageProcessor负责解析页面,抽取有用信息,以及发现新的链接。WebMagic使用Jsoup作为HTML解析工具,并基于其开发了解析XPath的工具Xsoup。在这四个组件中,PageProcessor对于每个站点每个页面都不一样,是需要使用者定制的部分。 -
Scheduler
Scheduler负责管理待抓取的URL,以及一些去重的工作。WebMagic默认提供了JDK的内存队列来管理URL,并用集合来进行去重。也支持使用Redis进行分布式管理。除非项目有一些特殊的分布式需求,否则无需自己定制Scheduler。 -
Pipeline
Pipeline负责抽取结果的处理,包括计算、持久化到文件、数据库等。WebMagic默认提供了“输出到控制台”和“保存到文件”两种结果处理方案。Pipeline定义了结果保存的方式,如果你要保存到指定数据库,则需要编写对应的Pipeline。对于一类需求一般只需编写一个Pipeline。
2、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>
3、WebMagic的使用
- 第一步,需要先实现PageProcessor接口,并实现process与getSite方法,process方法主要用来选择页面的数据并放入page对象中。具体案例如下:
JobProcessor类,用于抓取并解析网页内容获取需要的数据
@Component
public class JobProcessor implements PageProcessor {
private Logger logger = LoggerFactory.getLogger(this.getClass());
/** 自定以的数据处理类,该类继承了Pipeline接口,并实现了其process方法 */
@Autowired
private SpringDataPipeline springDataPipeline;
/** 需要抓取的url列表页 */
private String url = "https://search.51job.com/list/000000,000000,0000,32%252C01,9,99," +
"Java,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=";
/** 设置请求的页面解析的编码,请求的超时时间,每台服务的请求时间与重试次数 */
private Site site = Site.me()
.setCharset("gbk")
.setTimeOut(10000)
.setRetrySleepTime(3000)
.setRetryTimes(3);
@Override
public void process(Page page) {
//解析页面,获取招聘信息详情页的url地址
List<Selectable> nodes = page.getHtml().css("div#resultList div.el").nodes();
//判断获取到的集合是否weikong,如果为空表示这个招聘详情页,如果不为空则表示这是列表页
if (nodes == null || nodes.size() == 0) {
// 如果为空,则表示这是招聘详情页,解析页面获取招聘详情信息保存数据
this.saveJobInfo(page);
} else {
// 如果不为空,则表示这是列表页解析出详情页的URL地址,放到任务队列中
for (Selectable selectable : nodes) {
// 获取URL地址
String jobInfoUrl = selectable.links().toString();
// 将获取到的URL地址放到任务队列中
page.addTargetRequest(jobInfoUrl);
}
// 获取下一页的url
String nextPageUrl = page.getHtml().css("div.p_in li.bk").nodes().get(1).links().toString();
// 把url放到任务队列中
page.addTargetRequest(nextPageUrl);
}
}
/**
* 功能描述:saveJobInfo,解析页面获取招聘详情信息,保存数据 <br>
* @param page
* @return void
*
* @Author jiangkuobo
* @Date 2020-05-19 00:21
* @Version 1.0.0
*/
private void saveJobInfo(Page page) {
// 创建招聘详情对象
JobInfo jobInfo = new JobInfo();
// 解析页面
Html html = page.getHtml();
// 获取数据封装到对象中
jobInfo.setCompanyName(html.css("div.cn p.cname a", "text").toString());
String companyAddr = Jsoup.parse(html.css("div.bmsg").nodes().get(1).toString()).text();
jobInfo.setCompanyAddr(companyAddr.substring(0, companyAddr.length()-3));
jobInfo.setCompanyInfo(Jsoup.parse(html.css("div.tmsg").toString()).text());
jobInfo.setJobName(html.css("div.cn h1", "text").toString());
String[] jobAndTime = html.css("div.cn p.ltype", "text").toString().split(" ");
jobInfo.setJobAddr(jobAndTime[0]);
jobInfo.setJobInfo(Jsoup.parse(html.css("div.job_msg").toString()).text());
jobInfo.setUrl(page.getUrl().toString());
String text = html.css("div.cn strong", "text").toString();
Integer[] salary = SalatyUtils.getSalary(text);
jobInfo.setSalaryMin(salary[0]);
jobInfo.setSalaryMax(salary[1]);
String time = null;
for (int i=0; i< jobAndTime.length; i++) {
String s = jobAndTime[i];
if (s.contains("发布")) {
time = s.substring(0, s.length()-2);
}
}
jobInfo.setTime(time);
// 保存结果
page.putField("jobInfo", jobInfo);
}
@Override
public Site getSite() {
return site;
}
/**
* 功能描述:initialDelay任务启动后等待多久执行,fixedDelay任务执行的间隔时间 <br>
* @param
* @return void
*
* @Author jiangkuobo
* @Date 2020-05-19 00:21
* @Version 1.0.0
*/
@Scheduled(initialDelay = 1000, fixedDelay = 100000)
public void doProcess() {
logger.info("============ 任务开始执行 ============");
Spider.create(new JobProcessor())
.addUrl(url)
.setScheduler(new QueueScheduler().setDuplicateRemover(new BloomFilterDuplicateRemover(100000)))
.thread(10)
.addPipeline(this.springDataPipeline)
.run();
logger.info("============ 任务执行完成 ============");
}
}
SpringDataPipeline类,用于数据处理后放入数据库中
@Component
public class SpringDataPipeline implements Pipeline {
@Autowired
private JobInfoService jobInfoService;
@Override
public void process(ResultItems resultItems, Task task) {
// 获取封装好的招聘信息对象
JobInfo jobInfo = resultItems.get("jobInfo");
// 判断数据是否为空,如果不为空则将数据保存到数据库中
if (jobInfo != null) {
this.jobInfoService.save(jobInfo);
}
}
}
SalatyUtils类,JobProcessor中调用的方法用于处理网页中的数据
public class SalatyUtils {
public static Integer[] getSalary(String salaryString) {
Integer[] salaryArr = new Integer[2];
String itemSalaty = salaryString.substring(0, salaryString.length()-3);
String itemMath = salaryString.substring(salaryString.length()-3, salaryString.length());
String[] salary = itemSalaty.split("-");
if (itemMath.contains("千")) {
salaryArr[0] = new BigDecimal(salary[0]).multiply(new BigDecimal(1000)).intValue();
salaryArr[1] = new BigDecimal(salary[1]).multiply(new BigDecimal(1000)).intValue();
} else {
if (itemMath.contains("月")) {
salaryArr[0] = new BigDecimal(salary[0]).multiply(new BigDecimal(10000)).intValue();
salaryArr[1] = new BigDecimal(salary[1]).multiply(new BigDecimal(10000)).intValue();
} else {
salaryArr[0] = new BigDecimal(salary[0]).multiply(new BigDecimal(10000/12)).intValue();
salaryArr[1] = new BigDecimal(salary[1]).multiply(new BigDecimal(10000/12)).intValue();
}
}
return salaryArr;
}
}