WebMagic项目代码分为核心和扩展
两部分。核心部分(webmagic-core)是一个精简的、模块化的爬虫实现,而扩展部分则包括一些便利的、实用性的功能。
WebMagic的设计目标是尽量的模块化
,并体现爬虫的功能特点。这部分提供非常简单、灵活的API,在基本不改变开发模式的情况下,编写一个爬虫。
扩展部分(webmagic-extension)提供一些便捷的功能,例如注解模式编写爬虫等。同时内置了一些常用的组件,便于爬虫开发。
WebMagic的结构分为Downloader(下载,向Scheduler要下载的地址)、PageProcessor(页面解析)、Scheduler(存放url下载队列)、Pipeline(输出到mysql,文件等)四大组件
,并由Spider
将它们彼此组织起来。这四大组件对应爬虫生命周期中的下载、处理、管理和持久化等功能。WebMagic的设计参考了Scapy,但是实现方式更Java化一些。
而Spider则将这几个组件组织起来,让它们可以互相交互,流程化的执行,可以认为Spider是一个大的容器
,它也是WebMagic逻辑的核心
1.Downloader
Downloader负责从互联网上下载页面,以便后续处理。WebMagic默认使用了Apache HttpClient作为下载工具。
2.PageProcessor
PageProcessor负责解析页面,抽取有用信息,以及发现新的链接。WebMagic使用Jsoup作为HTML解析工具,并基于其开发了解析XPath的工具Xsoup。
在这四个组件中,PageProcessor对于每个站点每个页面都不一样,是需要使用者定制的部分。
3.Scheduler
Scheduler负责管理待抓取的URL,以及一些去重的工作。WebMagic默认提供了JDK的内存队列来管理URL,并用集合来进行去重。也支持使用Redis进行分布式管理。
4.Pipeline
Pipeline负责抽取结果的处理,包括计算、持久化到文件、数据库等。WebMagic默认提供了“输出到控制台”和“保存到文件”两种结果处理方案。
Pipeline定义了结果保存的方式,如果你要保存到指定数据库,则需要编写对应的Pipeline。对于一类需求一般只需编写一个Pipeline。
- Downloader从互联网下载一般用的是http请求,下载之后拿到的是一个html页面,把下载的内容封装为一个page对象
- 1)PageProcessor对page对象进行解析,把需要的数据封装到ResultItems中,传递给Pipeline
2)Scheduler 通过request(对url地址的封装)从PageProcesser中拿到url,Scheduler再通过request分发给downloader- Pipeline拿到ResultItems(相当于一个map),做对应的持久化
PageProcessor组件是实现核心业务逻辑的组件
,在使用WebMagic的使用必须要自定义PageProcessor组件
。需要自定一个类实现PageProcessor接口
。此接口中有两个方法需要实现一个是getSite方法,此方法需要返回一个Site对象。一个是一个是process方法,此方法没有返回值,方法有个参数是Page对象。
<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.0modelVersion>
<groupId>com.itheimagroupId>
<artifactId>crawler_day02_1artifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>us.codecraftgroupId>
<artifactId>webmagic-coreartifactId>
<version>0.7.3version>
dependency>
<dependency>
<groupId>us.codecraftgroupId>
<artifactId>webmagic-extensionartifactId>
<version>0.7.3version>
dependency>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>16.0version>
dependency>
dependencies>
project>
package com.itheima.webmagic;
import org.apache.commons.io.FileUtils;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.pipeline.ConsolePipeline;
import us.codecraft.webmagic.pipeline.FilePipeline;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.scheduler.BloomFilterDuplicateRemover;
import us.codecraft.webmagic.scheduler.QueueScheduler;
import java.util.BitSet;
import java.util.List;
public class MyPageProcessor implements PageProcessor {
public void process(Page page) {
//把数据交给Pipeline进行输出
page.putField("content",page.getHtml().css("div#news_div ul li a",
"text").all());
//可以对爬虫进行一些配置
private Site site = Site.me();
public Site getSite() {
return site;
}
//WebMagic使用的默认下载器是HttpClient
public static void main(String[] args) {
//提供自己定义的PageProcessor
Spider.create(new MyPageProcessor())
//设置初始下载url地址
.addUrl("https://www.jd.com/moreSubject.aspx")
.run();
Site对象,可以使用Site.me()创建。
在此对象中可以对爬虫进行一些配置配置,包括编码、抓取间隔、超时时间、重试次数等。
//可以对爬虫进行一些配置
private Site site = Site.me()
// 单位是秒
.setCharset("UTF-8")//编码
.setSleepTime(1)//抓取间隔时间,可以解决一些反爬限制
.setTimeOut(1000 * 10)//超时时间
.setRetrySleepTime(3000)//重试时间
.setRetryTimes(3);//重试次数
方法 | 说明 | 示例 |
---|---|---|
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”) |
Page对象是PageProcess组件中的核心对象,此对象中包含三个核心操作:
获取Downloader对象下载结果。
当我们需要从page对象中获得下载结果时,可以使用page对象的getHtml()
方法。
此方法的返回结果就是一个Html对象,也可以看做把这个html页面解析之后映射成一个Html对象,Html对象实现了Selectable接口
,是可以直接使用Selectable接口中提供的选择器。
向Scheduler对象中添加Request对象也就是待访问的url
,
使用addTargetRequest或者addTargetRequests方法可以将解析出来的链接添加到url访问队列
,系统会把url封装成Request对象供Scheduler对象使用。
3)向Pipeline对象中设置输出结果
使用putField方法可以将解析的结果添加到ResultItems对象中,将来在Pipeline对象中可以取到这个数据。
Selectable相关的抽取元素链式API是WebMagic的一个核心功能。使用Selectable接口,可以直接完成页面元素的链式抽取,也无需去关心抽取的细节。
XPath
以下是获取属性class=mt的div标签,里面的h2标签的内容
page.getHtml().xpath("//div[@class=mt]/h2/text()")
CSS选择器
CSS选择器是与XPath类似的语言。它比XPath写起来要简单一些,但是如果写复杂一点的抽取规则,就相对要麻烦一点。
div.mt>h1表示class为mt的div标签下的直接子元素h2标签
page.getHtml().css(“div.mt>h2”).toString()。
具体规则见css选择器
正则表达式
正则表达式则是一种通用的文本抽取语言。在这里一般用于获取url地址。正则表达式学习难度要大一些
。
建议不是专门用的话,不需要去专门的学。
但是如果是专业爬虫的话,很多语言是都支持正则的,并且在代码量上来说更简洁。
//links:获取所有连接 regex:使用正则
// addTargetRequests添加多个url到url任务队列中
page.addTargetRequests(page.getHtml()
.css("#news_diva").links()
.regex("https://www.jd.com/news.html.*3$")
.all());
方法 | 说明 | 示例 |
---|---|---|
get() | 返回一条String类型的结果 | String link= html.links().get() |
toString() | 同get(),返回一条String类型的结果 | String link= html.links().toString() |
all() | 返回所有抽取结果 | List links= html.links().all() |
在WebMagic中,Pileline是抽取结束后,进行数据处理的部分,它主要用于抽取结果的保存,也可以定制Pileline可以实现一些通用的功能。
在这里我们可以指定输出的位置,可以是控制台也可以是文件,当然也可以用户自定义Pipeline实现数据导入到数据库中。
类 | 说明 | 备注 |
---|---|---|
ConsolePipeline | 输出结果到控制台 | 抽取结果需要实现toString方法 |
FilePipeline | 保存结果到文件 | 抽取结果需要实现toString方法 |
JsonFilePipeline | JSON格式保存结果到文件 | |
ConsolePageModelPipeline | (注解模式)输出结果到控制台 | |
FilePageModelPipeline | (注解模式)保存结果到文件 | |
JsonFilePageModelPipeline | (注解模式)JSON格式保存结果到文件 想持久化的字段需要有getter方法 |
//提供自己定义的PageProcessor
Spider.create(new MyPageProcessor())
//设置初始下载url地址
.addUrl("https://www.jd.com/moreSubject.aspx")
//添加文件输出的Pipeline
.addPipeline(new FilePipeline("D:\\crawler"))
WebMagic提供了Scheduler可以帮助我们解决下载目标url管理的问题。
Scheduler是WebMagic中进行URL管理的组件。一般来说,Scheduler包括两个作用:
WebMagic内置了几个常用的Scheduler。如果你只是在本地执行规模比较小的爬虫,那么基本无需定制Scheduler
,但是了解一下已经提供的几个Scheduler还是有意义的。
去重部分被单独抽象成了一个接口:DuplicateRemover,从而可以为同一个Scheduler选择不同的去重方式,以适应不同的需要,目前提供了两种去重方式。
RedisScheduler是使用Redis的set进行去重,其他的Scheduler默认都使用HashSetDuplicateRemover来进行去重。
HashSet(小型爬虫)
使用java中的HashSet不能重复的特点去重。优点是容易理解。使用方便。
缺点:占用内存大,性能较低。
Redis去重(超大型爬虫,可以搭集群)
使用Redis的set进行去重。优点是速度快(Redis本身速度就很快),而且去重不会占用爬虫服务器的资源,可以处理更大数据量的数据爬取。
缺点:需要准备Redis服务器,增加开发和使用成本。
布隆过滤器(BloomFilter)(大型爬虫)
使用布隆过滤器也可以实现去重。优点是占用的内存要比使用HashSet要小的多,也适合大量数据的去重操作。
缺点:有误判的可能。没有重复可能会判定重复,但是重复数据一定会判定重复。
布隆过滤器 (Bloom Filter)是由Burton Howard Bloom于1970年提出,它是一种space efficient的概率型数据结构,用于判断一个元素是否在集合中。在垃圾邮件过滤的黑白名单方法、爬虫(Crawler)的网址判重模块中等等经常被用到。
哈希表也能用于判断元素是否在集合中,但是布隆过滤器只需要哈希表的1/8或1/4的空间复杂度就能完成同样的问题。布隆过滤器可以插入元素,但不可以删除已有元素。其中的元素越多,误报率越大,但是漏报是不可能的(重复的一定找得到,但是有可能漏抓)
。原理见算法
Spider是爬虫启动的入口。在启动爬虫之前,我们需要使用一个PageProcessor创建一个Spider对象,然后使用run()进行启动。
同时Spider的其他组件(Downloader、Scheduler、Pipeline)都可以通过set方法来进行设置。
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<groupId>com.ithiema</groupId>
<artifactId>crawler_day02_51job</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--SpringMVC-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--测试组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--SpringData Jpa-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--MySQL连接包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--WebMagic核心包-->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>0.7.3</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--WebMagic扩展-->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>0.7.3</version>
</dependency>
<!--WebMagic对布隆过滤器的支持-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0</version>
</dependency>
</dependencies>
</project>
package com.itheima.wuyijob.crawler;
import com.itheima.wuyijob.pojo.JobInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
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.selector.Html;
import java.util.List;
@Component
public class JobPageProcessor implements PageProcessor {
@Autowired
private JpaPipeline jpaPipeline;
// 测试代码
// String url = "https://www.jd.com/news.html?id=38673";
String url = "https://search.51job.com/list/010000,000000,0000,32%252C38,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=";
@Override
public void process(Page page) {
// 测试代码
// page.putField("content",page.getHtml().css("div.mt h1","text").all());
// 获取列表页的职位详情url
List<String> urlList = page.getHtml().css("div#resultList div.el p.t1").links().all();
// urlList.forEach(e -> System.out.println(e));
// urlList没有值,页面是职位详情页,如果有值,是职位列表页
if (urlList.size()>0){
// 把职位详情url放到url管理列表中
page.addTargetRequests(urlList);
// 获取下一页的地址,到这就一直不会停,会一直下一页,具体原因参考csdn架构图
page.addTargetRequests(page.getHtml().css("li.bk").links().all());
}else {
// 解析页面并存放结果到ResultItems里
parseJobInfo(page);
}
}
private void parseJobInfo(Page page) {
// 创建职位详情对象,用来存放解析的数据
JobInfo jobInfo = new JobInfo();
// 解析页面获取数据
Html html = page.getHtml();
jobInfo.setJobName(html.css("body > div.tCompanyPage > div.tCompany_center.clearfix > div.tHeader.tHjob > div > div.cn > h1","text").get());
jobInfo.setSalary(html.css("body > div.tCompanyPage > div.tCompany_center.clearfix > div.tHeader.tHjob > div > div.cn > strong","text").get());
jobInfo.setCompanyName(html.css("body > div.tCompanyPage > div.tCompany_center.clearfix > div.tHeader.tHjob > div > div.cn > p.cname > a.catn","text").get());
jobInfo.setJobAddr(html.css("body > div.tCompanyPage > div.tCompany_center.clearfix > div.tHeader.tHjob > div > div.cn > p.msg.ltype","text").get());
jobInfo.setJobInfo(html.css("body > div.tCompanyPage > div.tCompany_center.clearfix > div.tCompany_main > div:nth-child(1) > div","text").get());
jobInfo.setUrl(page.getUrl().toString());
// 封装好的职位详情数据存放到resultItems中
page.putField("jobInfo",jobInfo);
}
// 添加定时任务配置
// initialDelay,项目启动成功后,多久执行任务,单位毫秒
// fixedDelay,任务执行完成后,间隔多久下一次任务执行,单位毫秒
@Scheduled(initialDelay = 1000, fixedDelay = 10000)
public void run(){
Spider.create(new JobPageProcessor())
// 使用自定义的PipeLine保存数据
.addPipeline(jpaPipeline)
.addUrl(url)
.thread(20)
.run();
}
private Site site = Site.me()
.setTimeOut(10* 1000); // 超时10s
@Override
public Site getSite() {
return site;
}
}
package com.itheima.wuyijob.crawler;
import com.itheima.wuyijob.pojo.JobInfo;
import com.itheima.wuyijob.service.JobInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;
/**
* 实现PipeLine和使用定时器
*/
@Component
public class JpaPipeline implements Pipeline {
@Autowired
private JobInfoService jobInfoService;
@Override
public void process(ResultItems resultItems, Task task) {
// 获取职位数据
JobInfo jobInfo = resultItems.get("jobInfo");
if (jobInfo!=null){
jobInfoService.save(jobInfo);
}
}
}
有些网站不允许爬虫进行数据爬取,因为会加大服务器的压力。其中一种最有效的方式是通过ip+时间
进行鉴别,因为正常人不可能短时间开启太多的页面,发起太多的请求。
提供两个免费代理ip的服务商网站:
米扑代理
https://proxy.mimvp.com/free.php
西刺免费代理IP
http://www.xicidaili.com/
package com.itheima.day03.job;
import org.springframework.stereotype.Component;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.downloader.HttpClientDownloader;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.proxy.Proxy;
import us.codecraft.webmagic.proxy.SimpleProxyProvider;
public class ProxyTest implements PageProcessor {
@Override
public void process(Page page) {
System.out.println("获取到的自己的ip地址是:");
System.out.println(page.getHtml().css("center", "text").get());
}
private Site site = Site.me();
@Override
public Site getSite() {
return site;
}
public static void main(String[] args) {
//创建下载器
HttpClientDownloader httpClientDownloader = new HttpClientDownloader();
//设置代理服务器
httpClientDownloader.setProxyProvider(SimpleProxyProvider.from(
new Proxy("27.203.165.139",8060 )
));
Spider.create(new ProxyTest())
.addUrl("http://2019.ip138.com/ic.asp")
//把设置好代理服务器的下载器进行使用
.setDownloader(httpClientDownloader)
.run();
}
}
我们可以使用HttpClient模拟浏览器抓取静态html,但是对js的解析部分还是很薄弱。虽然我们可以读取js的运作机制并且找到相关数据,但是这样会耗费大量时间。为了解决这个问题我们可以使用工具来模拟浏览器的运行,直接获取解析结果。这就是使用Selenium+headless浏览器来实现动态爬虫。
例如京东商品页:先加载的一个html没有价格,加载完之后会执行js,js会发起ajax或者类似的远程调用获取价格,然后再写入html页面中的价格去。所以此时页面的价格是一般爬虫无法爬取的,它的价格是在另一个请求中。
Selenium
Selenium是一个用于Web应用程序测试的工具。Selenium可以使用代码控制浏览器,就像真正的用户在操作一样。而对于爬虫来说,使用Selenium操控浏览器来爬取网上的数据那么肯定是爬虫中的杀手武器。Selenium支持多种浏览器可以是chrome、Firefox、PhantomJS等
使用WebDriver在Chrome浏览器上进行测试时或者做页面抓取,需要从http://chromedriver.storage.googleapis.com/index.html网址中下载与本机chrome浏览器对应的驱动程序,驱动程序名为chromedriver。chromedriver的版本需要和本机的chrome浏览器对应,才能正常使用,一般情况下下载最新版就可以了。
headless浏览器(PhantomJS(这个和headless应该是等价的,但是这个已经被弃用了))
一个基于webkit内核的无头浏览器,即没有UI界面,即它就是一个浏览器,只是其内的点击、翻页等人为相关操作需要程序设计实现
京东现在貌似增加了反爬策略,在之后爬取的过程中报sesssion错误的问题,不过不是很确定
要爬取京东商城的完整商品数据,需要使用无头浏览器来进行数据抓取,这样就可以取到搜索结果页面的后半部分数据
。 (这个案例)只能爬取30个详情,具体为什么不太明白。
WebMagic框架默认使用的是HttpClient下载页面,所以我们需要把HttpClient换成无头浏览器,那么就需要定制Downloader组件。
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<groupId>com.itheima</groupId>
<artifactId>crawler_day03_jd</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--SpringData Jpa-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--单元测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--MySQL连接包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--WebMagic核心包-->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>0.7.3</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--WebMagic扩展-->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>0.7.3</version>
</dependency>
<!--selenium依赖-->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.13.0</version>
</dependency>
</dependencies>
</project>
package com.itheima.cralwer.crawler;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.springframework.stereotype.Component;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Request;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.downloader.Downloader;
import us.codecraft.webmagic.selector.PlainText;
@Component
public class JdChromeDownloader implements Downloader {
//声明驱动
private RemoteWebDriver driver;
public JdChromeDownloader() {
//第一个参数是使用哪种浏览器驱动
//第二个参数是浏览器驱动的地址
System.setProperty("webdriver.chrome.driver","C:\\Users\\Administrator\\AppData\\Local\\Google\\Chrome\\Application\\chromedriver\\chromedriver.exe");
//创建浏览器参数对象
ChromeOptions chromeOptions = new ChromeOptions();
// 设置为 headless 模式,上课演示,或者学习不要打开
// chromeOptions.addArguments("--headless");
// 设置浏览器窗口打开大小
chromeOptions.addArguments("--window-size=1280,700");
//创建驱动
this.driver = new ChromeDriver(chromeOptions);
}
@Override
public Page download(Request request, Task task) {
try {
driver.get(request.getUrl());
Thread.sleep(2000);
//无论是搜索页还是详情页,都滚动到页面底部,所有该加载的资源都加载
//需要滚动到页面的底部,获取完整的商品数据
driver.executeScript("window.scrollTo(0, document.body.scrollHeight - 1000)");
Thread.sleep(2000l);
//获取页面对象
Page page = createPage(request.getUrl(), driver.getPageSource());
//判断是否是搜索页
if (request.getUrl().contains("search")) {
//如果请求url包含search,说明是搜索结果页
//在搜索结果页,需要获取下一页的链接地址
//点击下一页按钮,在下一页中获取当前页的url(就是下一页的url),放到任务队列中
WebElement next = driver.findElement(By.cssSelector("a.pn-next"));
//点击
next.click();
//获取当前页面(其实就是下一页)的url地址
String nextUrl = driver.getCurrentUrl();
//使用page对象,把下一页url放到任务列表中
page.addTargetRequest(nextUrl);
}
//关闭浏览器
//driver.close();
return page;
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
@Override
public void setThread(int threadNum) {
}
//构建page返回对象
private Page createPage(String url, String content) {
Page page = new Page();
page.setRawText(content);
page.setUrl(new PlainText(url));
page.setRequest(new Request(url));
page.setDownloadSuccess(true);
return page;
}
}
package com.itheima.cralwer.crawler;
import com.itheima.cralwer.pojo.Item;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.selector.Selectable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class JdPageProcessor implements PageProcessor {
@Override
public void process(Page page) {
//System.out.println(page.getHtml().css("div.mt h1", "text"));
//获取页面中的商品列表数据,只有搜索结果页才有商品列表
List<Selectable> nodes = page.getHtml().css("#J_goodsList li.gl-item").nodes();
//判断nodes是否有值
if (nodes != null && nodes.size() > 0) {
//如果有值表示是搜索结果页
//声明存放商品的集合
List<Item> itemList = new ArrayList<>();
//遍历商品项
for (Selectable node : nodes) {
//获取商品spu
String spu = node.css("li", "data-spu").get();
//获取商品的sku,一个spu有可能有多个sku
List<String> skuList = node.css("li.ps-item img", "data-sku").all();
//遍历sku
for (String sku : skuList) {
//创建对象
Item item = new Item();
//设置数据
item.setSpu(Long.parseLong(spu));
item.setSku(Long.parseLong(sku));
item.setCreated(new Date());
item.setUpdated(item.getCreated());
//放到集合中
itemList.add(item);
//把商品详情页的url放到url任务队列中
page.addTargetRequest("https://item.jd.com/" + sku + ".html");
}
}
//把需要持久化的数据放到ResultItems中
page.putField("itemList", itemList);
} else {
//如果没有值表示是商品详情页
//创建商品对象
Item item = new Item();
String sku = page.getHtml().css("div.left-btns a.J-follow", "data-id").get();
item.setSku(Long.parseLong(sku));
item.setTitle(page.getHtml().css("div.sku-name", "text").get());
item.setPrice(page.getHtml().css("span.p-price span.price", "text").get());
item.setUrl(page.getUrl().toString());
//保存到ResultItems中
page.putField("item", item);
}
}
private Site site = Site.me().setTimeOut(2000);
@Override
public Site getSite() {
return site;
}
}
package com.itheima.cralwer.crawler;
import com.itheima.cralwer.pojo.Item;
import com.itheima.cralwer.service.ItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;
import java.util.List;
@Component
public class JpaPipeline implements Pipeline {
@Autowired
private ItemService itemService;
@Override
public void process(ResultItems resultItems, Task task) {
//获取商品列表页数据
List<Item> itemList = resultItems.get("itemList");
if (itemList != null && itemList.size() > 0) {
itemService.saveItemList(itemList);
}
//获取商品详情页数据
Item item = resultItems.get("item");
if (item != null) {
itemService.saveItem(item);
}
}
}
package com.itheima.cralwer.crawler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import us.codecraft.webmagic.Spider;
@Component
public class StartCrawler {
@Autowired
private JdChromeDownloader downloader;
@Autowired
private JpaPipeline jpaPipeline;
//声明搜索页的初始地址
String url = "https://search.jd.com/Search?keyword=%E6%89%8B%E6%9C%BA&enc=utf-8" +
"&suggest=1.his.0.0&wq=&pvid=72c93b8e6951419f83e22a7daee906d0";
@Scheduled(cron = "0/5 * * * * *")
public void run() {
Spider.create(new JdPageProcessor())
//.addUrl("https://www.jd.com/news.html?id=38673")
.addUrl(url)
//设置下载器
.setDownloader(downloader)
//设置使用jpa的输出
.addPipeline(jpaPipeline)
.run();
}
}