# JavaCrawler教程
**Repository Path**: baijincode/java-crawler-tutorial
## Basic Information
- **Project Name**: JavaCrawler教程
- **Description**: Java爬虫【一篇文章精通系列-案例开发-巨细】HttpClient5 + jsoup + WebMagic + spider
- **Primary Language**: Unknown
- **License**: Artistic-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 3
- **Created**: 2022-12-10
- **Last Updated**: 2022-12-10
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
网络爬虫 ( web crawler),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本,在java的世界里,我们经常用**HttpClient** ,**jsoup** ,**WebMagic**,**spider-flow** 这四种技术来实现爬虫。
@[TOC](Java之爬虫【一篇文章精通系列】HttpClient + jsoup + WebMagic + ElasticSearch导入数据检索数据)
## 一、入门程序
### 1、环境准备
- JDK1.8
- lntelliJ IDEA
- IDEA自带的Maven
### 2、环境搭建
创建Maven工程并给pom.xml加入依赖


在Maven当中搜索对应的依赖
[https://mvnrepository.com/](https://mvnrepository.com/)
搜索HttpClient

我们选择使用量最多的


将依赖引入工程当中

```xml
org.apache.httpcomponents.client5
httpclient5
5.1.3
```
搜索slf4j




```xml
org.slf4j
slf4j-log4j12
1.7.25
test
```
完善日子配置文件



```xml
log4j.rootLogger=DEBUG,A1
1og4j.logger.cn.itbluebox = DEBUG
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-d{yyy-MM-dd HH:mm:ss,SSS}[%t][%c]-[%p] %m%n
```
### 3、使用httpclient爬取数据


这里我们爬取菜鸟教程的内容

完善:
CrawlerFirst

```java
public class CrawlerFirst {
public static void main(String[] args) throws IOException, ParseException {
//1、打开浏览器,创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//2、输入网址,创建发起get请求HttpGet对象
HttpGet httpGet = new HttpGet("https: // www.runoob.com/");
//3、按回车,发起请求,返回响应,使用HttpClient对象发起请求
CloseableHttpResponse response = httpClient.execute(httpGet);
//4、解析响应,响应数据
//判断状态码是否是200
if(response.getCode() == 200){
System.out.println("响应成功");
HttpEntity entity = response.getEntity();
String content = EntityUtils.toString(entity, "UTF-8");
System.out.println(content);
}
}
}
```
运行测试

获得响应的内容

## 二、网络爬虫介绍
在大数据时代,信息的采集是一项重要的工作,而互联网中的数据是海量的,如果单纯靠人力进行信息采集,不仅低效繁琐,搜集的成本也会提高。
如何自动高效地获取互联网中我们感兴趣的信息并为我们所用是一个重要的问题,而爬虫技术就是为了解决这些问题而生的。
网络爬虫 ( web crawler)也叫做网络机器人,可以代替人们自动地在互联网中进行数据信息的采集与整理。
它是一种按照一定的规则,自动地抓取万维网信息的程序或者脚木,可以自动采集所有其能够访问到的页面内容,以获取相关数据。
从功能上来讲,爬虫一般分为数据采集,处理,储存三个部分。爬虫从一个或若干初始网页的URL开始,获得初始网页上的URL,在抓取网页的过程中,不断从当前页面上抽取新的URL放入队列,直到满足系统的一定停止条件。
## 三、HttpClient
网络爬虫就是用程序帮助我们访问网络上的资源,我们一直以来都是使用HTTP协议访问互联网的网页,网络爬虫需要编写程序,在这里使用同样的HTTP协议访问网页。这里我们使用Java的 HTT协议客户端 HttpClient这个技术,来实现抓取网页数据。
### 1、GET请求(无参/有参)
创建HttpGetTest类


```java
public class HttpGetTest {
public static void main(String[] args) throws IOException, ParseException {
// 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建HttpGet对象,设置URL地址
HttpGet httpGet = new HttpGet("https: // www.runoob.com/");
//使用HttpClient发起请求,获取response
CloseableHttpResponse response = httpClient.execute(httpGet);
//解析响应
if(response.getCode() == 200){
String content = EntityUtils.toString(response.getEntity(), "utf-8");
System.out.println(content.length());
}
//关闭response
response.close();
httpClient.close();
}
}
```
运行测试

我们看到只输出了长度没有日志信息
我们需要配置一下pom.xml
将test注掉

输出了对应的日志信息

复制HttpGetTest创建HttpGetParamTest,设置带参数的请求


```java
public class HttpGetParamTest {
public static void main(String[] args) throws Exception {
// 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//设置请求地址是:菜鸟教程网站地址/?s=Java
//创建URIBuilder
URIBuilder uriBuilder = new URIBuilder("https: // www.runoob.com/");
//设置参数
uriBuilder.setParameter("s","Java");
//创建HttpGet对象,设置URL地址
HttpGet httpGet = new HttpGet(uriBuilder.build());
System.out.println("发起请求的信息:"+httpGet.getUri());
//使用HttpClient发起请求,获取response
CloseableHttpResponse response = httpClient.execute(httpGet);
//解析响应
if(response.getCode() == 200){
String content = EntityUtils.toString(response.getEntity(), "utf-8");
System.out.println(content.length());
}
//关闭response
response.close();
httpClient.close();
}
}
```
运行测试



### 2、POST请求(无参/有参)
创建HttpPostTest类

```java
public class HttpPostTest {
public static void main(String[] args) throws IOException, ParseException {
// 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建HttpPost对象,设置URL地址
HttpPost httpPost = new HttpPost("https: // www.runoob.com/");
//使用HttpClient发起请求,获取response
CloseableHttpResponse response = httpClient.execute(httpPost);
//解析响应
if(response.getCode() == 200){
String content = EntityUtils.toString(response.getEntity(), "utf-8");
System.out.println(content.length());
}
//关闭response
response.close();
httpClient.close();
}
}
```
运行测试

POST请求带参数的

```java
public class HttpPostParamTest {
public static void main(String[] args) throws Exception {
// 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建HttpPost对象,设置URL地址
HttpPost httpPost = new HttpPost("https: // www.runoob.com");
//声明List集合,封装表单中的请求参数
List params = new ArrayList();
params.add(new BasicNameValuePair("s","java"));
//添加多个内容
//params.add(new BasicNameValuePair("s","java"));
//params.add(new BasicNameValuePair("s","java"));
//params.add(new BasicNameValuePair("s","java"));
//创建表单的Entity对象
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(params, Charset.defaultCharset());
//设置表单的Entity对象到POST请求当中
httpPost.setEntity(formEntity);
System.out.println(httpPost.getUri());
System.out.println(httpPost.getMethod());
System.out.println(httpPost.getEntity());
//使用HttpClient发起请求,获取response
CloseableHttpResponse response = httpClient.execute(httpPost);
//解析响应
if(response.getCode() == 200){
String content = EntityUtils.toString(response.getEntity(), "utf-8");
System.out.println(content.length());
}
//关闭response
response.close();
httpClient.close();
}
}
```
运行测试
运行成功

参数


### 3、连接池
如果每次请求都要创建HttpClient,会有频繁创建和销毁的问题,可以使用连接池来解决这个问题。测试以下代码,并断点查看每次获取的HttpClient都是不一样的。
创建HttpClientPoolTest类

编辑代码并打上断点

```java
public class HttpClientPoolTest {
public static void main(String[] args) throws Exception {
//创建连接池管理器
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
//使用连接池管理器发起请求
doGet(cm);
doGet(cm);
}
private static void doGet(PoolingHttpClientConnectionManager cm) throws Exception {
//不是每次创建新的HttpClient,而是从连接池当中获取HttpClient
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
HttpGet httpGet = new HttpGet("https: // www.runoob.com/");
CloseableHttpResponse response = httpClient.execute(httpGet);
if( response.getCode() == 200){
String content = EntityUtils.toString(response.getEntity(), "utf8");
System.out.println(content.length());
}
//不能关闭httpClient,这是是由连接池管理
//httpClient.close();
response.close();
}
}
```

Debug后我们看到 HttpClient 的地址值为1587

我们放行

我们发现HttpClient 的地址值为1954,证明连接是由连接池统一创建多个进行管理

如果不使用连接池那么创建多个连接使用的时候会是同一个地址值
我们Debug之前HttpPostParamTest的内容,HttpGetTest 地址 1588

再次debug,HttpPostParamTest ,HttpGetTest 地址 也是1588

### 4、连接池-设置连接数
设置每个主机的最大连接数,,,设置Host对应ip地址的连接数量,
因为在一个连接池当中可以有多个ip地址发起请求,
设置每一个地址的最大连接数,不然会照成ip地址分配不均匀的情况出现

```java
public class HttpClientPoolTest {
public static void main(String[] args) throws Exception {
//创建连接池管理器
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
//设置最大连接数,发起请求的所有连接
cm.setMaxTotal(100);
//设置每个主机的最大连接数,,,设置Host对应ip地址的连接数量,
// 因为在一个连接池当中可以有多个ip地址发起请求,
// 设置每一个地址的最大连接数,不然会照成ip地址分配不均匀的情况出现
cm.setDefaultMaxPerRoute(10);
//使用连接池管理器发起请求
doGet(cm);
doGet(cm);
}
private static void doGet(PoolingHttpClientConnectionManager cm) throws Exception {
//不是每次创建新的HttpClient,而是从连接池当中获取HttpClient
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
HttpGet httpGet = new HttpGet("https: // www.runoob.com/");
CloseableHttpResponse response = httpClient.execute(httpGet);
if( response.getCode() == 200){
String content = EntityUtils.toString(response.getEntity(), "utf8");
System.out.println(content.length());
}
//不能关闭httpClient,这是是由连接池管理
//httpClient.close();
response.close();
}
}
```
### 5、请求参数
有时候因为网络,或者目标服务器的原因,请求需要更长的时间才能完成,我们需要自定义相关时间。
创建HttpConfigTest

```java
public class HttpConfigTest {
public static void main(String[] args) throws IOException, ParseException {
// 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建HttpGet对象,设置URL地址
HttpGet httpGet = new HttpGet("https: // www.runoob.com/");
//配置请求信息
RequestConfig config =
RequestConfig.custom()
.setConnectTimeout(1000,TimeUnit.MILLISECONDS) //创建连接的最长时间,单位是毫秒
.setConnectionRequestTimeout(500,TimeUnit.MILLISECONDS) //设置获取连接的最长时间
// .setCookieSpec()
.build();
//给请求设置请求信息
httpGet.setConfig(config);
//设置头部信息 可以用作登录 httpGet.setHeaders();
//使用HttpClient发起请求,获取response
CloseableHttpResponse response = httpClient.execute(httpGet);
//解析响应
if(response.getCode() == 200){
String content = EntityUtils.toString(response.getEntity(), "utf-8");
System.out.println(content.length());
}
//关闭response
response.close();
httpClient.close();
}
}
```
更多配置信息请参考,httpclient官网文档
[https://hc.apache.org/httpclient-legacy/logging.html](https://hc.apache.org/httpclient-legacy/logging.html)
中文文档
[http://www.httpclient.cn/](http://www.httpclient.cn/)
## 四、jsoup
我们抓取到页面之后,还需要对页面进行解析。可以使用字符串处理工具解析页面,也可以使用正则表达式,但是这些方法都会带来很大的开发成本,所以我们需要使用一款专门解析 html页面的技术。
### 1、jsoup介绍
jsoup 是一款Java的HTML解析器,可直接解析某个URL地址、HTML文木内容。
它提供了一套非常省力的API,可通过DOM,CSS 以及类似于jQuery的操作方法来取出和操作数据。
官网:[https://jsoup.org/](https://jsoup.org/)

jsoup的主要功能如下:
1.从一个URL,文件或字符串中解析HTML;
2.使用DOM或CSS选择器来查找、取出数据;
3.可操作HTML元素、属性、文本;
### 2、引入依赖

搜索jsoup依赖
找一个使用量比较高的

引入对应的依赖


```xml
org.jsoup
jsoup
1.13.1
```
引入Junit
方便测试使用
```xml
junit
junit
4.12
test
```
Commons IO 操作文件
```xml
commons-io
commons-io
2.11.0
```
Commons-lang3 字符串工具类
```xml
org.apache.commons
commons-lang3
3.12.0
```
### 4、jsoup 解析URL/字符串/文件



#### (1)解析URL
```java
public class JsoupFirstTest {
@Test
public void testUrl() throws Exception {
//解析URL地址,第一个参数是访问的URL第二个参数是访问的超时时间
Document document = Jsoup.parse(new URL("https://www.bilibili.com/"), 10000);
//使用标签选择器,获取title标签当中的内容
String title = document.getElementsByTag("title").first().text();
System.out.println(title);
}
}
```


PS:虽然使用Jsoup可以替代 HttpClient直接发起请求解析数据,但是往往不会这样用,因为实际的开发过程中,需要使用到多线程,连接池,代理等等万式,而 jsoup 对这些的支持并不是很好,所以我们一般把jsoup仅仅作为 Html解析工具使用。
#### (2)解析字符串
我们这里需要准备一些HTML文件
保持:https://www.runoob.com/ 网页到桌面上

修改名称叫index

```java
@Test
public void testString() throws Exception{
//使用工具类读取文件,获取字符串
String content = FileUtils.readFileToString(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//解析字符串
Document doc = Jsoup.parse(content);
String title = doc.getElementsByTag("title").first().text();
System.out.println(title);
}
```
运行测试

#### (3)解析文件
```java
@Test
public void testFile() throws Exception{
//解析文件
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
String title = doc.getElementsByTag("title").first().text();
System.out.println(title);
}
```
运行测试

### 5、使用dom方式遍历文档
#### (1)根据id查询元素 getElementByld
查看我们本地的下载好的HTML页面
我们想爬取如下内容,找到对应的id

id在文档当中是唯一的,所以整个HTML文档当中只有一个标签的id是cate1,返回的Element 是单个对象
```java
@Test
public void testDom() throws Exception{
//解析文件,获取Document对象
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//1、根据id查询元素 getElementByld
Element elementCate1 = doc.getElementById("cate1");
//.text()获取元素的内容
System.out.println(elementCate1.text());
}
```
运行测试

输出:`HTML / CSS`和实际要找的内容是相符的
同理如果我们想获取其他id的内容,更换getElementByld的参数即可

```java
Element elementCate1 = doc.getElementById("cate3");
```

Document 相关方法拓展请参考官方文档:[https://jsoup.org/apidocs/org/jsoup/nodes/Document.html](https://jsoup.org/apidocs/org/jsoup/nodes/Document.html)

Element 相关方法拓展:[https://jsoup.org/apidocs/org/jsoup/nodes/Element.html](https://jsoup.org/apidocs/org/jsoup/nodes/Element.html)

#### (2)根据标签获取元素 getElementsByTag
我们想获取到``标签对应的内容

```java
@Test
public void testTag() throws Exception{
//解析文件,获取Document对象
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//2、根据标签获取元素 getElementsByTag
Elements elements = doc.getElementsByTag("strong");
System.out.println("获取第一个strong标签对应的内容:");
System.out.println(elements.first().text());
System.out.println("获取最后一个strong标签对应的内容:");
System.out.println(elements.last().text());
System.out.println("输出所有strong标签对应的内容:");
for (Element element : elements) {
//.text()获取元素的内容
System.out.println(element.text());
}
}
```
运行测试

Elements其他方法拓展:
查看官方文档:[https://jsoup.org/apidocs/org/jsoup/select/Elements.html](https://jsoup.org/apidocs/org/jsoup/select/Elements.html)

#### (3)根据class 获取元素 getElementsByClass.
查找design 对应的所有内容

```java
@Test
public void testClass() throws Exception{
//解析文件,获取Document对象
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//3.根据class 获取元素 getElementsByClass.
Elements elements = doc.getElementsByClass("design");
System.out.println("获取第一个class = 'design'对应的内容:");
System.out.println(elements.first().text());
System.out.println("获取最后一个class = 'design'对应的内容:");
System.out.println(elements.last().text());
System.out.println("输出所有class = 'design'对应的内容:");
for (Element element : elements) {
//.text()获取元素的内容!
System.out.println(element.text());
}
}
```
运行测试

#### (4)根据属性获取元素getElementsByAttribute
获取属性,有href属性的标签的内容

为了方便测试,我们将头部和底部有href属性的内容删除掉


```java
@Test
public void testAttribute() throws Exception{
//解析文件,获取Document对象
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//3.根据class 获取元素 getElementsByClass.
Elements elements = doc.getElementsByAttribute("href");
System.out.println("获取第一个href属性对应的内容:");
System.out.println(elements.first().text());
System.out.println("获取最后一个href属性对应的内容:");
System.out.println(elements.last().text());
System.out.println("输出所有href属性对应的内容:");
for (Element element : elements) {
//.text()获取元素的内容
System.out.println(element.text());
}
}
```
运行测试

设置对应的key和value
#### (5)根据属性获取元素getElementsByAttributeValue
并筛选对应的key和value

```java
@Test
public void testAttributeKeyValue() throws Exception{
//解析文件,获取Document对象
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//3.根据class 获取元素 getElementsByClass.
Elements elements = doc.getElementsByAttributeValue("href","菜鸟教程官网html/html-tutorial.html");
System.out.println("获取第一个href属性对应的内容:");
System.out.println(elements.first().text());
System.out.println("获取最后一个href属性对应的内容:");
System.out.println(elements.last().text());
System.out.println("输出所有href属性对应的内容:");
for (Element element : elements) {
//.text()获取元素的内容
System.out.println(element.text());
}
}
```
运行测试

### 6、从元素当中获取数据
#### (1)从元素中获取id
```java
@Test
public void testDataId() throws Exception {
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//根据id获取元素
System.out.println("========根据id获取元素==========");
Element element = doc.getElementById("cate3");
//1、从元素中获取id
System.out.println("\n========从元素中获取id==========");
String id = element.id();
System.out.println("获取id为cate3对应的id:"+id);
}
```
运行测试

#### (2) 从元素中获取className
```java
@Test
public void testDataClassName() throws Exception {
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//根据id获取元素
System.out.println("========根据id获取元素==========");
Element element = doc.getElementById("cate3");
//从元素中获取className
System.out.println("\n========从元素中获取className==========");
String className = element.className();
System.out.println("获取id为cate3对应的className:"+className);
//获取多个classNames
System.out.println("\n========获取多个classNames==========");
Set strings = element.classNames();
for (String string : strings) {
System.out.println("获取id为cate3对应的classNames:"+string);
}
}
```
运行测试

#### (3)从元素中获取属性的值 attr
```java
@Test
public void testDataAttr() throws Exception {
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//从元素中获取属性的值 attr
System.out.println("\n========从元素中获取属性的值 attr==========");
Elements elements = doc.getElementsByTag("a");
for (Element element1 : elements) {
String href = element1.attr("href");
System.out.println("获取所有a标签对应href的内容:"+href);
}
}
```
运行测试

#### (4)从元素中获取所有属性attributes
```java
@Test
public void testDataAttributes() throws Exception {
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//从元素中获取所有属性attributes
System.out.println("\n========从元素中获取所有属性attributes==========");
Elements elementsImg = doc.getElementsByTag("img");
for (Element element1 : elementsImg) {
Attributes attributes = element1.attributes();
System.out.println("\n图片img所有的attributes:"+attributes);
System.out.println("图片img的class:"+attributes.get("class"));
System.out.println("图片img的alt:"+attributes.get("alt"));
System.out.println("图片img的height:"+attributes.get("height"));
System.out.println("图片img的width:"+attributes.get("width"));
System.out.println("图片img的src:"+attributes.get("src"));
}
}
```
运行测试

#### (5)从元素中获取文本内容text
```java
@Test
public void testDataText() throws Exception {
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//从元素中获取文本内容text
System.out.println("\n========从元素中获取文本内容text==========");
Elements elementsStrong = doc.getElementsByTag("strong");
System.out.println(elementsStrong.text());
}
```
运行测试

### 7、Selector选择器概述
#### (1)`tagname`: 通过标签查找元素,比如: `strong`
```java
@Test
public void testSelectorTagName() throws Exception{
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//tagname: 通过标签查找元素,比如: strong
System.out.println("========tagname: 通过标签查找元素,比如: strong=========");
Elements elements = doc.select("strong");
for (Element element : elements) {
System.out.println(element.text());
}
}
```
运行测试

#### (2)`#id`:通过ID查找元素,比如: `#cate3`
```java
@Test
public void testSelectorId() throws Exception{
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//#id:通过ID查找元素,比如: #cate3
System.out.println("========tagname:#id:通过ID查找元素,比如: #cate3=========");
Element element = doc.select("#cate3").first();
System.out.println(element.text());
}
```

#### (3)`.class`:通过class名称查找元素,比如:`.design`
```java
@Test
public void testSelectorClass() throws Exception{
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//.class:通过class名称查找元素,比如:.design
System.out.println("========.class:通过class名称查找元素,比如:.design=========");
Elements elements = doc.select(".design");
for (Element element : elements) {
System.out.println(element.text());
}
}
```

#### (4)`[attribute]`:利用属性查找元素,比如:`[href]`
```java
@Test
public void testSelectorAttribute() throws Exception{
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//[attribute]:利用属性查找元素,比如:[href]
System.out.println("========[attribute]:利用属性查找元素,比如:[href]=========");
Elements elements = doc.select("[href]");
for (Element element : elements) {
System.out.println(element.text());
System.out.println(element.attr("href"));
}
}
```
运行测试

#### (5)`[attr=value]`:利用属性值来查找元素
比如: `[href=菜鸟教程地址]`
```java
@Test
public void testSelectorAttributeValue() throws Exception{
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//[attr=value]:利用属性值来查找元素,比如: [href=菜鸟教程地址]
System.out.println("========[attr=value]:利用属性值来查找元素,比如: [href=菜鸟教程地址]=========");
Elements elements = doc.select("[href=菜鸟教程地址]");
for (Element element : elements) {
System.out.println(element.text());
System.out.println(element.attr("href"));
}
}
```
运行测试

### 8、Selector选择器组合使用
#### (1)`el#id`:元素+ID,比如:`div#cate1`
```java
@Test
public void testSelectorElId() throws Exception{
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//el#id:元素+ID,比如:div#cate1
System.out.println("========el#id:元素+ID,比如:div#cate1=========");
Element element = doc.select("div#cate1").first();
System.out.println(element.text());
}
```

#### (2)`el.class`:元素+class,比如:`div.design`
```java
@Test
public void testSelectorElClass() throws Exception{
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//el.class:元素+class,比如:div.design
System.out.println("========el.class:元素+class,比如:div.design=========");
Elements elements = doc.select("div.design");
for (Element element : elements) {
System.out.println(element.text());
}
}
```
#### (3)`el[attr]`:元素+属性名,比如: `a[href]`
```java
@Test
public void testSelectorElAttr() throws Exception{
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//el.class:元素+class,比如:div.design
System.out.println("========el[attr]:元素+属性名,比如: a[href]=========");
Elements elements = doc.select("a[href]");
for (Element element : elements) {
System.out.println(element.text());
System.out.println(element.attr("href"));
}
}
```

#### (4)任意组合:比如: `.item-top[href] strong`
```java
@Test
public void testSelectorArbitrarily() throws Exception{
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//任意组合:比如: .item-top[href] strong
System.out.println("========任意组合:比如: .item-top[href] strong=========");
Elements elements = doc.select(".item-top[href] strong");
for (Element element : elements) {
System.out.println(element.text());
}
}
```
运行测试

#### (5)`ancestor child`:查找某个元素下子元素
比如: `.item-top strong`查找"item-top"下的所有strong
```java
@Test
public void testSelectorAncestorChild() throws Exception{
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//ancestor child:查找某个元素下子元素,比如: .item-top strong查找"item-top"下的所有strong
System.out.println("========ancestor child:查找某个元素下子元素,比如: .item-top strong查找\"item-top\"下的所有strong=========");
Elements elements = doc.select(".item-top strong");
for (Element element : elements) {
System.out.println(element.text());
}
}
```
运行测试

#### (6)`parent > child`:查找某个父元素下的直接子元素
比如`a >strong`查找a第一级(直接子元素)的strong
```java
@Test
public void testSelectorParentChild() throws Exception{
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//parent > child:查找某个父元素下的直接子元素,比如a >strong查找a第一级(直接子元素)的strong
System.out.println("========parent > child:查找某个父元素下的直接子元素,比如a >strong查找a第一级(直接子元素)的strong=========");
Elements elements = doc.select("a > strong");
for (Element element : elements) {
System.out.println(element.text());
}
}
```

#### (7)`parent > *`:查找某个父元素下所有直接子元素
```java
@Test
public void testSelectorParentAll() throws Exception{
//解析文件,获取Document
System.out.println("========解析文件,获取Document==========");
Document doc = Jsoup.parse(new File("C:\\Users\\ZHENG\\Desktop\\index.html"), "utf8");
//parent > *:查找某个父元素下所有直接子元素
System.out.println("========parent > *:查找某个父元素下所有直接子元素=========");
Elements elements = doc.select("a > *");
for (Element element : elements) {
System.out.println(element.text());
}
}
```

## 五、jsoup爬虫案例
学习了HttpClient和l Jsoup,就掌握了如何抓取数据和如何解析数据,接下来,我们做一个小练习,把京东的手机数据抓取下来。主要目的是HttpClient和l Jsoup的学习。
### 1、需求分析
首先访问京东,搜索手机,分析页面,我们抓取以下商品数据:商品图片、价格、标题、商品详情页。

### 2、SPU 和 SKU
除了以上四个属性以外,我们发现上图中的苹果手机有四种产品,我们应该每一种都要抓取。
那么这里就必须要了解spu和l sku的概念
SPU = Standard Product Unit(标准产品单位),
SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个 SPU。
例如上图中的苹果手机就是SPU,包括红色、深灰色、金色、银色
SKU=stock keeping unit(库存量单位)
SKU即库存进出计量的单位,可以是以件、盒、托盘等为单位。SKU是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。在服装、鞋类商品中使用最多最普遍。
例如上图中的苹果手机有几个款式,'10A 4GB+64GB烟波蓝,就是一个sku

### 3、开发准备
#### (1)数据库表分析
根据需求分析,我们创建的表如下:
```sql
CREATE TABLE `jd_item` (
`id` BIGINT(10) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`spu` BIGINT(15) DEFAULT NULL COMMENT '商品集合id',
`sku` BIGINT(15) DEFAULT NULL COMMENT '商品最小品类单元id',
`title` VARCHAR(100) DEFAULT NULL COMMENT '商品价格',
`price` BIGINT(10) DEFAULT NULL COMMENT '商品价格',
`pic` VARCHAR(200) DEFAULT NULL COMMENT '商品图片',
`url` VARCHAR(200) DEFAULT NULL COMMENT '图片详情地址',
`created` DATETIME DEFAULT NULL COMMENT '创建时间',
`update` DATETIME DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY(`id`)
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='京东商品表';
```
#### (2)创建工程添加依赖
使用Spring Boot+Spring Data JPA和定时任务进行开发,需要创建Maven工程并添加依赖。


引入以下依赖

```xml
4.0.0
com.itbluebox
itbluebox-crawler-jd
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-parent
2.7.4
8
8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
runtime
org.apache.httpcomponents.client5
httpclient5
5.1.3
org.jsoup
jsoup
1.13.1
commons-io
commons-io
2.11.0
org.apache.commons
commons-lang3
3.12.0
cn.hutool
hutool-all
5.8.8
com.alibaba
fastjson
1.2.66
org.projectlombok
lombok
org.springframework.boot
spring-boot-maven-plugin
```
#### (3)设置配置文件

```yaml
server:
port: 8081
# DataSource Config
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
jpa:
database: MySql
show-sql: true
```
### 4、代码实现
#### (1)编写pojo
根据数据库表编写,pojo



```java
@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "jd_item")
public class Item {
//主键
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//标准产品单位(商品集合)
private Long spu;
//库存量单位(最小品类单元)
private Long sku;
//商品标题
private String title;
//商品价格
private Double price;
//商品图片
private String pic;
//商品详情地址
private String url;
//创建时间
private Date created;
//更新时间
private Date updated;
}
```
#### (2)编写Dao



```java
public interface ItemDao extends JpaRepository- {
}
```
#### (3)编写service






```java
@Service
public class ItemServiceImpl implements ItemService {
@Autowired
private ItemDao itemDao;
@Override
public void save(Item item) {
itemDao.save(item);
}
@Override
public List
- findAll(Item item) {
//声明查询条件
Example
- example = Example.of(item);
//根据查询条件进行查询
return itemDao.findAll(example);
}
}
```
#### (4)编写引导类


```java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
//使用定时任务,需要先开启定时任务,需要添加注解
@EnableScheduling
public class ItBlueBoxApplication {
public static void main(String[] args) {
SpringApplication.run(ItBlueBoxApplication.class,args);
}
}
```
启动运行测试


#### (5)封装HttpClient
我们需要经常使用HttpClient,所以需要进行封装,方便使用



```java
import org.apache.commons.lang3.ObjectUtils;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Component
public class HttpUtils {
private PoolingHttpClientConnectionManager cm;
public HttpUtils() {
cm = new PoolingHttpClientConnectionManager();
//设置最大连接数
cm.setMaxTotal(1000);
//设置每个主机的最大连接数
cm.setDefaultMaxPerRoute(100);
}
/*
* 根据请求地址,下载页面数据
* */
public String doGetHtml(String url) {
//获取HttpClient对象
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
//创建HttpGet请求对象,设置url地址
HttpGet httpGet = new HttpGet(url);
//设置请求信息
httpGet.setConfig(getConfig());
//使用HttpClient,发起请求,获取响应
CloseableHttpResponse response = null;
String content = null;
try {
response = httpClient.execute(httpGet);
//解析响应,返回结果
if(response.getCode() == 200){
//判断响应体Entity是否为空,如果为空不能使用,如果不为空就可以使用EntityUtils
if(!ObjectUtils.isEmpty(response.getEntity())){
content = EntityUtils.toString(response.getEntity(), "utf8");
}
}
} catch ( Exception e) {
e.printStackTrace();
}finally {
if(!ObjectUtils.isEmpty(response)){
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return content;
}
/*
* 下载图片
* */
public String doGetImage(String url){
//获取HttpClient对象
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
//创建HttpGet请求对象,设置url地址
HttpGet httpGet = new HttpGet(url);
//设置请求信息
httpGet.setConfig(getConfig());
//使用HttpClient,发起请求,获取响应
CloseableHttpResponse response = null;
String picName = null;
try {
response = httpClient.execute(httpGet);
//解析响应,返回结果
if(response.getCode() == 200){
//判断响应体Entity是否为空,如果为空不能使用,如果不为空就可以使用EntityUtils
if(!ObjectUtils.isEmpty(response.getEntity())){
//下载图片
//获取的图片后缀
String extName = url.substring(url.lastIndexOf("."));
//创建图片名称,重命名图片
picName = UUID.randomUUID().toString() + extName;
//下载图片
File file = new File("C:\\Users\\ZHENG\\Desktop\\img\\"+picName);
FileOutputStream outputStream = new FileOutputStream(file);
response.getEntity().writeTo(outputStream);
outputStream.close();
}
}
} catch ( Exception e) {
e.printStackTrace();
}finally {
if(!ObjectUtils.isEmpty(response)){
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//返回图片名称
return picName;
}
//设置请求信息
private RequestConfig getConfig() {
RequestConfig config =
RequestConfig.custom()
.setConnectTimeout(2, TimeUnit.SECONDS)
.setResponseTimeout(500, TimeUnit.MILLISECONDS).build();
return config;
}
}
```
### 5、实现爬虫功能
#### (1)实现数据抓取(定时任务)
使用定时任务,可以定时抓取最新的京东的手机数据

打开京东搜索手机可以看到一个一个的手机,对应的标签详细

分析基本是商品详细详细




```java
import com.itbluebox.jd.dao.ItemDao;
import com.itbluebox.jd.pojo.Item;
import com.itbluebox.jd.service.ItemService;
import com.itbluebox.jd.utils.HttpUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
@Component
public class ItemTask {
@Autowired
private HttpUtils httpUtils;
@Autowired
private ItemService itemService;
//当下载任务完成后,间隔多长时间进行下一次的任务
@Scheduled(fixedDelay = 100 * 1000) //每隔100秒,执行一下itemTaskJD方法
public void itemTaskJD() throws Exception{
//声明需要解析的初始地址
String url = "https://search.jd.com/Search?keyword=%E6%89%8B%E6%9C%BA&pvid=e81fc533c12640c4941ab249dbf120b2&s=56&click=0page=";
//按照页码对手机的搜索结果进行遍历解析
for (int i = 1; i <= 10; i=i+2) {
System.out.println(url + i);
String html = httpUtils.doGetHtml(url + i);
//解析页面获取商品数据并存储
parse(html);
}
System.out.println("手机数据抓取完成");
}
//解析页面获取商品数据并存储
private void parse(String html) {
//解析HTML,获取Document
Document document = Jsoup.parse(html);
//获取SPU
Elements spuEls = document.select("div#J_goodsList > ul > li");
for (Element spuEl : spuEls) {
//获取spu
String spu = spuEl.attr("data-spu");
String title = spuEl.select(".p-name em").text();
String price = spuEl.select(".p-price i").text();
//获取sku
Elements skuEls = spuEl.select("li.ps-item");
for (Element skuEl : skuEls) {
//获取sku
long sku = Long.parseLong(skuEl.select("[data-sku]").attr("data-sku"));
//根据sku查询商品数据
Item item = new Item();
item.setSku(sku);
List
- list = itemService.findAll(item);
if(list.size() > 0){
// 如果商品存在,就进行下一次循环,该商品不保存,因为已经存在
continue;
}
//设置商品的spu
if(ObjectUtils.isEmpty(spu)){
spu = sku+"";
item.setSpu(sku);
}else{
item.setSpu(Long.parseLong(spu));
}
//获取商品的详情连接 菜鸟教程地址
String itemUrl = "https://item.jd.com/"+spu+".html";
item.setUrl(itemUrl);
item.setUpdated(new Date());
//获取商品图片
String picUrl = "https:"+skuEl.select("img[data-sku]").first().attr("data-lazy-img");
picUrl = picUrl.replace("/n9/","/n1/");
picUrl = picUrl.replace("/n7/","/n1/");
String picName = httpUtils.doGetImage(picUrl);
item.setPic(picName);
//获取商品的价格
//https://fts.jd.com/prices/mgets?skuIds=J_100031192618
//data-price
item.setPrice(Double.parseDouble(price));
//获取商品的标题
item.setTitle(title);
item.setCreated(new Date());
item.setUpdated(item.getCreated());
//保存商品数据到数据库当中
itemService.save(item);
}
}
}
}
```
运行测试

运行成功

爬取数据成功

图片下载成功

#### (2)注意(如果爬取数据报错或者需要登录)
在HttpUtils上设置登录的Cookie

## 六、WebMagic
### 1、WebMagic介绍
#### (1)简介
WebMagic官网:[http://webmagic.io/](http://webmagic.io/)

上面完成了爬虫的入门的学习,是一个最基本的爬虫案例之后我们要学习一款爬虫框架的使用就是 WebMagic。其底层用到了我们上一天课程所使用的HttpClient和Jsoup,让我们能够更方便的开发爬虫。
WebMagic项目代码分为核心和扩展两部分。
核心部分(webmagic-core)是一个精简的、模块化的爬虫实现,而扩展部分则包括一些便利的、实用性的功能。
WebMagic的设计目标是尽量的模块化,并体现爬虫的功能特点。这部分提供非常简单、灵活的APIl,在基木不改变开发模式的情况下,编写一个爬虫。
扩展部分(webmagic-extension)提供一些便捷的功能,例如注解模式编写爬虫等。同时内置了一些常用的组件,便于爬虫开发。
#### (2)简介WebMagic功能
简单的API,可快速上手
模块化的结构,可轻松扩展
提供多线程和分布式支持
#### (3)简介WebMagic架构
WebMagic的结构分为Downloader、PageProcessor、Scheduler、Pipeline四大组件,并由Spider将它们彼此组织起来。这四大组件对应爬虫生命周期中的下载、处理、管理和持久化等功能。WebMagic的设计参考了`Scapy`,但是实现方式更Java化一些。(Scapy是Python的爬虫工具,官网:[https://scapy.net/](https://scapy.net/))
而Spider则将这几个组件组织起来,让它们可以互相交互,流程化的执行,可以认为Spider是一个大的容器,它也是WebMagic逻辑的核心。
WebMagic总体架构图如下:

#### (4)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。
#### (5)用于数据流转的对象
1. Request
Request是对URL地址的一层封装,一个Request对应一个URL地址。
它是PageProcessor与Downloader交互的载体,也是PageProcessor控制Downloader唯一方式。
除了URL本身外,它还包含一个Key-Value结构的字段extra。你可以在extra中保存一些特殊的属性,然后在其他地方读取,以完成不同的功能。例如附加上一个页面的一些信息等。
2. Page
Page代表了从Downloader下载到的一个页面——可能是HTML,也可能是JSON或者其他文本格式的内容。
Page是WebMagic抽取过程的核心对象,它提供一些方法可供抽取、结果保存等。在第四章的例子中,我们会详细介绍它的使用。
3. ResultItems
ResultItems相当于一个Map,它保存PageProcessor处理的结果,供Pipeline使用。它的API与Map很类似,值得注意的是它有一个字段skip,若设置为true,则不应被Pipeline处理。
### 3、WebMagic入门程序
#### (1)创建工程


#### (2)引入依赖

```java
us.codecraft
webmagic-core
0.7.6
us.codecraft
webmagic-extension
0.7.6
```
#### (3)加入配置文件
WebMagic使用slf4j-log4j12作为slf4j的实现。
添加 log4j.properties配置文件。

```java
log4j.rootLogger=DEBUG,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
```
#### (3)入门程序编写
这回我们爬取菜鸟教程




```java
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
public class JobProcessor implements PageProcessor {
//负责解析页面
@Override
public void process(Page page) {
//解析Page,返回的数据Page,并且把解析的结果放到
page.putField("divTop",page.getHtml().css("div.sidebar-box div").all());
}
private Site site = Site.me();
@Override
public Site getSite() {
return site;
}
//主函数
public static void main(String[] args) {
Spider.create(new JobProcessor())
.addUrl("菜鸟教程网站地址") //设置要爬取数据的页面
.run(); //执行爬虫
;
}
}
```
运行测试

### 4、WebMagic抽取元素
#### (1)抽取元素Selectable(XPath、正则表达式和 CSS选择器)
WebMagic里主要使用了三种抽取技术
XPath、正则表达式和 CSS选择器。另外,对于JSON格式的内容,可使用JsonPath进行解析。
1. **XPath**
以上是获取属性`id=main-left-cloumn` 的div标签,里面的div标签的内容



```java
//XPath
page.putField("div",page.getHtml().xpath("//div[@id=main-left-cloumn]/div"));
```
运行测试

2. CSS选择器
css选择器是与XPath类似的语言。在上一次的课程中,我们已经学习过了Jsoup 的选择器,它比 XPath 写起来要简单一些,但是如果写复杂一点的抽取规则,就相对要麻烦一点。
`div > a.item-top > strong`表示div下啊标签class为item-top 的div标签下的直接子元素strong标签
```java
//CSS选择器
page.putField("divCSS",page.getHtml().css("div > a.item-top > strong").toString());
```
运行测试


3. 正则表达式
正则表达式则是一种通用的文本抽取语言。在这里一般用于获取url地址。
通过正则表达式找到所有包含`编程语言`的内容,我们可以看到下面有9 个


返回的内容包括`编程语言`的strong标签一共有9个
```java
divCSS:
[
JavaScript 是 Web 的编程语言,
一门通用计算机编程语言,
C++是在C语言的基础上开发的一种通用编程语言,
Scala 是一门多范式(multi-paradigm)的编程语言。,
Go语言是谷歌推出的一种全新的编程语言,
R 语言是为数学研究工作者设计的一种数学编程语言,
Swift 是一种支持多编程范式和编译式的编程语言,用于开发 iOS,OS X 和 watchOS应用程序。,
在 Java 虚拟机上运行的静态类型编程语言,Android 官方开发语言,
C# 是一个简单的、现代的、通用的、面向对象的编程语言
]
```
#### (2)抽取元素API
Selectable相关的抽取元素链式API是WebMagic 的一个核心功能。
使用Selectable接口,可以直接完成页面元素的链式抽取,也无需去关心抽取的细节。
在刚才的例子中可以看到,`page.getHtml()`返回的是一个Html对象,它实现了selectable接口。
这个接口包含的方法分为两类:抽取部分和获取结果部分。
|方法 | 说明 | 示例 |
|--|--|--|
| `xpath(String xpath)` | 使用XPath选择 | `html.xpath("//div[@class='title']")` |
| `$(String selector)` | 使用Css选择器选择 | `html.$("div.title")` |
| `$(String selector,String attr)` | 使用Css选择器选择 | `html.$("div.title","text")` |
| `css(String selector)` | 功能同$(),使用Css选择器选择 | `html.css("div.title")` |
| `links()` | 选择所有链接 | `html.links()` |
| `regex(String regex)` | 使用正则表达式抽取 | `html.regex("\(.\*?)\")` |
| `regex(String regex,int group)` | 使用正则表达式抽取,并指定捕获组 | `html.regex("\(.\*?)\",1)` |
| `replace(String regex, String replacement)` | 替换内容 | `html.replace("\","")` |
这部分抽取API返回的都是一个Selectable接口,意思是说,抽取是支持链式调用的。
#### (3)抽取结果API
当链式调用结束时,我们一般都想要拿到一个字符串类型的结果。这时候就需要用到获取结果的API了。我们知道,一条抽取规则,无论是XPath、CSS选择器或者正则表达式,总有可能抽取到多条元素。WebMagic对这些进行了统一,你可以通过不同的API获取到一个或者多个元素。
| 方法 | 说明 | 示例 |
|--|--|--|
| `get()` | 返回一条String类型的结果 | `String link= html.links().get()` |
| `toString()` | 功能同get(),返回一条String类型的结果 | `String link= html.links().toString()` |
| `all()` | 返回所有抽取结果 | `List links= html.links().all()` |
| `match()` | 是否有匹配结果 | `if (html.links().match()){ xxx; }` |
例如,我们知道页面只会有一条结果,那么可以使用`selectable.get()`或者`selectable.toString()`拿到这条结果。
这里`selectable.toString()`采用了`toString()`这个接口,是为了在输出以及和一些框架结合的时候,更加方便。因为一般情况下,我们都只需要选择一个元素!
`selectable.all()`则会获取到所有元素。
```java
page.putField("divCSS1",page.getHtml().css("div > a.item-top > strong").regex(".*编程语言.*").get());
page.putField("divCSS2",page.getHtml().css("div > a.item-top > strong").regex(".*编程语言.*").toString());
page.putField("divCSS3",page.getHtml().css("div > a.item-top > strong").regex(".*编程语言.*").match());
```
运行测试

#### (4)抽取链接
获取下面的链接


并获取对应的详情信息


```java
//获取到所有的链接,筛选出以,-tutorial.html结尾的
List allUrl = page.getHtml().css(".middle-column-home a").links().regex(".*-tutorial.html$").all();
for (String url : allUrl) {
Request request = new Request();
request.setUrl(url);
page.addTargetRequest(request);
}
page.putField("url",page.getHtml().css("p").all());
```
运行测试

### 5、使用Pipeline保存结果
好了,爬虫编写完成,现在我们可能还有一个问题:我如果想把抓取的结果保存下来,要怎么做呢?WebMagic用于保存结果的组件叫做Pipeline。例如我们通过“控制台输出结果”这件事也是通过一个内置的Pipeline完成的,它叫做ConsolePipeline。那么,我现在想要把结果用Json的格式保存下来,怎么做呢?我只需要将Pipeline的实现换成"JsonFilePipeline"就可以了。
```java
Spider.create(new JobProcessor())
.addUrl("菜鸟网站地址") //设置要爬取数据的页面
.addPipeline(new ConsolePipeline())
.addPipeline(new JsonFilePipeline("C:\\Users\\ZHENG\\Desktop\\爬虫测试"))
.thread(10)//设置线程数
.run(); //执行爬虫
;
```
运行测试


保存成功

### 6、爬虫的配置、启动和终止
#### (1)Spider
Spider是爬虫启动的入口。
在启动爬虫之前,我们需要使用一个`PageProcessor`创建一个Spider对象,然后使用run()进行启动。
同时Spider的其他组件`(Downloader、Scheduler、Pipeline)`都可以通过set方法来进行设置。
| 方法 |说明 | 示例 |
|--|--|--|
| `create(PageProcessor)` | 创建Spider | `Spider.create(new GithubRepoProcessor())` |
| `addUrl(String…)` | 添加初始的URL | `spider .addUrl("http://webmagic.io/docs/")` |
| `addRequest(Request...)` | 添加初始的Request | `Request spider .addRequest("http://webmagic.io/docs/")` |
|`thread(n)` | 开启n个线程 | `spider.thread(5)` |
|`run()` | 启动,会阻塞当前线程执行 | `spider.run()` |
| `start()/runAsync()` | 异步启动,当前线程继续执行 | `spider.start()` |
| `stop()` | 停止爬虫 | `spider.stop()` |
| `test(String)` | 抓取一个页面进行测试 | `spider .test("http://webmagic.io/docs/")` |
| `addPipeline(Pipeline)` | 添加一个Pipeline,一个Spider可以有多个Pipeline |`spider .addPipeline(new ConsolePipeline())` |
| `setScheduler(Scheduler)` | 设置Scheduler,一个Spider只能有个一个Scheduler |`pider.setScheduler(new RedisScheduler())` |
| `setDownloader(Downloader)` | 设置Downloader,一个Spider只能有个一个Downloader | `spider .setDownloader(new SeleniumDownloader())` |
| `get(String)` | 同步调用,并直接取得结果 | `ResultItems result = spider .get("http://webmagic.io/docs/")` |
| `getAll(String…)` | 同步调用,并直接取得一堆结果 | `List results = spider .getAll("http://webmagic.io/docs/", "http://webmagic.io/xxx")` |
#### (2)Site
对站点本身的一些配置信息,例如编码、HTTP头、超时时间、重试策略等、代理等,都可以通过设置Site对象来进行配置。
| 方法 | 说明 | 示例 |
|--|--|--|
| 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") |
| setHttpProxy(HttpHost) | 设置Http代理 | site.setHttpProxy(new HttpHost("127.0.0.1",8080)) |
其中循环重试cycleRetry是0.3.0版本加入的机制。
该机制会将下载失败的url重新放入队列尾部重试,直到达到重试次数,以保证不因为某些网络原因漏抓页面。
```java
private Site site = Site.me()
.setCharset("utf8") //设置编码
.setTimeOut(10000) //设置超时时间,单位是ms
.setRetrySleepTime(3000) //设置重试的间隔时间
.setSleepTime(3) //设置重试次数
;
```

## 七、爬虫的分类
网络爬虫按照系统结构和实现技术,大致可以分为以下几种类型:
通用网络爬虫、聚焦网络爬虫、增量式网络爬虫、深层网络爬虫。
实际的网络爬虫系统通常是几种爬虫技术相结合实现的。
### 1、通用网络爬虫
通用网络爬虫又称全网爬虫(Scalable web Crawler),爬行对象从一些种子URL扩充到整个web,
主要为门户站点搜索引擎和大型 web 服务提供商采集数据。
这类网络爬虫的爬行范围和数量巨大,对于爬行速度和存储空间要求较高,对于爬行页面的顺序要求相对较低,
同时由于待刷新的页面太多,通常采用并行工作方式,但需要较长时间才能刷新一次页面。
简单的说就是互联网上抓取所有数据。
### 2、聚焦网络爬虫
聚焦网络爬虫(Focused Crawler),又称主题网络爬虫(Topical Crawler),是指选择性地爬行那些与预先定义好的主题相关页面的网络爬虫。
和通用网络爬虫相比,聚焦爬虫只需要爬行与主题相关的页面,极大地节省了硬件和网络资源,保存的页面也由于数量少而更新快,还可以很好地满足一些特定人群对特定领域信息的需求。
简单的说就是互联网上只抓取某一种数据。
### 3、聚焦网络爬虫
聚焦网络爬虫(Focused crawler),又称主题网络爬虫( Topical Crawler),是指选择性地爬行那些与预先定义好的主题相关页面的网络爬虫。
和通用网络爬虫相比,聚焦爬虫只需要爬行与主题相关的页面,极大地节省了硬件和网络资源,保存的页面也由于数量少而更新快,还可以很好地满足一些特定人群对特定领域信息的需求。
简单的说就是互联网上只抓取某一种数据。
### 4、增量式网络爬虫
增量式网络爬虫(Incremental web Crawler)是指对已下载网页采取增量式更新和只爬行新产生的或者已经发生变化网页的爬虫,它能够在一定程度上保证所爬行的页面是尽可能新的页面。
和周期性爬行和刷新页面的网络爬虫相比,增量式爬虫只会在需要的时候爬行新产生或发生更新的页面﹐并不重新下载没有发生变化的页面,可有效减少数据下载量,及时更新已爬行的网页,减小时间和空间上的耗费,但是增加了爬行算法的复杂度和实现难度。
简单的说就是互联网上只抓取刚刚更新的数据。
### 5、Deep Web爬虫
Web页面按存在方式可以分为表层网页( Surface Web和深层网页(Deep
Web,也称Invisible Web Pages或 Hidden Web)。
表层网页是指传统搜索引擎可以索引的页面,以超链接可以到达的静态网页为主构成的 Web页面。Deep Web是那些大部分内容不能通过静态链接获取的隐藏在搜索表单后的,只有用户提交一些关键词才能获得的web页面。
## 八、WebMagic爬虫案例(案例分析)
我们已经学完了WebMagic的基木使用方法,现在准备使用webMagic实现爬取数据的功能。这里是一个比较完整的实现。
在这里我们实现的是聚焦网络爬虫,只爬取招聘的相关数据。
### 1、案例分析
要实现的是爬取51JOB上的招聘信息。
只爬取“计算机软件”和“互联网电子商务”两个行业的信息。
首先访问页面并搜索两个行业。结果如下。


### 2、数据库表
```sql
CREATE TABLE `job_info` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
`company_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '公司名称',
`company_addr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '公司联系方式地址',
`company_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '公司信息',
`job_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '职位名称',
`job_addr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '工作地点',
`job_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '职位信息',
`salary_min_max` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '薪资范围,最大',
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '招聘信息详情页',
`time` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '职位最近发布时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
```
### 3、实现流程
我们需要解析职位列表页,获取职位的详情页,再解析页面获取数据。获取url地址的流程如下。

但是在这里有个问题:在解析页面的时候,很可能会解析出相同的url地址(例如商品标题和商品图片超链接,而且url一样),如果不进行处理,同样的url会解析处理多次,浪费资源。所以我们需要有一个url去重的功能。
### 4、Scheduler组件
WebMagic提供了scheduler可以帮助我们解决以上问题,
Scheduler是 WebMagic 中进行URL管理的组件。一般来说,Scheduler包括两个作用。
- 对待抓取的URL队列进行管理。
- 对已抓取的URL进行去重。
#### (1)Scheduler
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选择不同的去重方式,以适应不同的需要,目前提供了两种去重方式。
Redisscheduler是使用Redis的 set进行去重,
其他的Scheduler 默认都使用HashSetDuplicateRemover来进行去重。
#### (2)过滤器
|类| 说明 |
|--|--|
| HashSetDuplicateRemover | 使用HashSet来进行去重,占用内存较大 |
| BloomFilterDuplicateRemover | 使用BloomFilter来进行去重,占用内存较小,但是可能漏抓页面 |
案例演示
```java
page.addTargetRequest("https://www.runoob.com/");
page.addTargetRequest("https://www.runoob.com/");
page.addTargetRequest("https://www.runoob.com/");
page.addTargetRequest("https://www.runoob.com/");
```
运行测试,我们发现虽然设置了多个相同请求链接,但是只发起了一次请求

查询看对眼的过滤器
```java
//主函数
public static void main(String[] args) {
Spider spider = Spider.create(new JobProcessor())
.addUrl("https://www.runoob.com/") //设置要爬取数据的页面
.addPipeline(new ConsolePipeline())
.thread(10); //执行爬虫
Scheduler scheduler = spider.getScheduler();
spider.run();
}
```
打断点以后查看重新运行

我们可以看到默认的过滤器是HashSetDuplicateRemovera
#### (3)使用布隆去除过滤器
设置布隆去重过滤器,指定最多对一千万数据进行过滤去重
```java
.setScheduler(new QueueScheduler().setDuplicateRemover(new BloomFilterDuplicateRemover(10000000)))
```

运行测试我们发现抛出异常

如果要使用BloomFilter,必须要加入以下依赖:
```xml
com.google.guava
guava
31.1-jre
```
重新运行
我们发现过滤器是:BloomFilterDuplicateRemover

### 5、三种去重方式
去重就有三种实现方式,那有什么不同呢?
- HashSet
使用java 中的 HashSet不能重复的特点去重。优点是容易理解。使用方便。缺点:占用内存大,性能较低。
- Redis去重
使用Redis 的 set进行去重。优点是速度快(Redis本身速度就很快),而且去重不会占用爬虫服务器的资源,可以处理更大数据量的数据爬去。
缺点:需要准备Redis 服务器,增加开发和使用成本。
- 布隆过滤器
使用布隆过滤器也可以实现去重。优点是占用的内存要比使用HashSet要小的多,也适合大量数据的去重操作。
缺点:有误判的可能。没有重复可能会判定重复,但是重复数据一定会判定重复。
布隆过滤器(Bloom Filter)是由 Burton Howard Bloom于
1970年提出,它是一
种 space efficient的概率型数据结构,用于判断一个元素是否在集合中。在垃圾邮件过滤的黑白名单方法、爬虫(Crawler)的网址判重模块中等等经常被用到。
哈希表也能用于判断元素是否在集合中,但是布隆过滤器只需要哈希表的1/8或1/4的空间复杂度就能完成同样的问题。布隆过滤器可以插入元素,但不可以删除已有元素。其中的元素越多,误报率越大,但是漏报是不可能的。
**原理:**
布隆过滤器需要的是一个位数组(和位图类似)和K个映射函数(和 Hash表类似),在初始状态时,对于长度为m的位数组array,它的所有位被置0。
## 九、WebMagic爬虫案例(案例实现)
### 1、开发准备
#### (1)创建工程
创建Maven工程,并加入依赖。pom.xml为:
```xml
4.0.0
com.itbluebox
itbluebox-crawler-jd
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-parent
2.7.4
8
8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
runtime
org.apache.httpcomponents.client5
httpclient5
5.1.3
org.jsoup
jsoup
1.13.1
commons-io
commons-io
2.11.0
org.apache.commons
commons-lang3
3.12.0
cn.hutool
hutool-all
5.8.8
com.alibaba
fastjson
1.2.66
org.projectlombok
lombok
us.codecraft
webmagic-core
0.7.6
org.slf4j
slf4j-log4j12
us.codecraft
webmagic-extension
0.7.6
com.google.guava
guava
31.1-jre
org.springframework.boot
spring-boot-maven-plugin
```
#### (2)配置文件
application.yml
```yml
server:
port: 8081
# DataSource Config
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
jpa:
database: MySql
show-sql: true
```
log4j.properties
```properties
log4j.rootLogger=DEBUG,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
```
#### (3)编写POJO
```java
@Data
@AllArgsConstructor
@NoArgsConstructor
@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 String salaryMinMax;
private String url;
private String time;
}
```
#### (4)编写DAO
```java
import com.itbluebox.job.pojo.JobInfo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface JobInfoDao extends JpaRepository {
}
```
#### (5)编写Service
```java
public interface JobInfoService {
/*
* 保存工作信息
* */
void save(JobInfo jobInfo);
/*
根据条件查询工作信息
*/
List findJobInfo(JobInfo jobInfo);
}
```
```java
@Slf4j
@Service
public class JobInfoServiceImpl implements JobInfoService {
@Autowired
private JobInfoDao jobInfoDao;
@Transactional
@Override
public void save(JobInfo jobInfo) {
//查询原有的数据
//如果URL和时间查询数据
JobInfo param = new JobInfo();
param.setUrl(jobInfo.getUrl());
param.setTime(jobInfo.getTime());
//执行查询
List jobInfoList = findJobInfo(param);
log.info("执行查询");
//判断查询结果是否为空
if(ObjectUtils.isEmpty(jobInfoList)){
//如果查询结果为空,表示招聘信息数据不存在,或者已经更新了,需要新增数据或更新数据库
JobInfo paramUrl = new JobInfo();
paramUrl.setUrl(jobInfo.getUrl());
//判断数据库当中是否有已经存在的数据
List jobInfosByUrl = findJobInfo(paramUrl);
System.out.println(jobInfosByUrl);
if(ObjectUtils.isEmpty(jobInfosByUrl)){
//如果不存在,就执行新增
log.info("如果不存在,就执行新增");
jobInfoDao.save(jobInfo);
}else {
//如果已经存在,就执行更新
log.info("如果已经存在,就执行更新");
JobInfo jobInfo1 = jobInfosByUrl.get(0);
jobInfo.setId(jobInfo1.getId());
jobInfoDao.save(jobInfo);
}
}
}
@Override
public List findJobInfo(JobInfo jobInfo) {
//设置查询条件
Example example = Example.of(jobInfo);
//执行查询
return jobInfoDao.findAll(example);
}
}
```
#### (6)编写引导类Application
```java
@SpringBootApplication
@EnableScheduling//开启定时任务
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
```
整体目录结构

### 2、解析页面
点击搜索我们发现调用了该接口
https://search.51job.com/list/000000,000000,0000,32%252c01,9,99,java,2,1.html?lang=c&postchannel=0000&workyear=99&cotype=99°reefrom=99&jobterm=99&companysize=99&ord_field=0&dibiaoid=0&line=&welfare=&u_atoken=e3cd9fd1-142a-456b-ae9a-48e9a7765c08&u_asession=01Vz6mSyKiCoM4C4KAw5n4_gDRC3jSjCfrX5Cx8F4ednl0cMO3TgJ1Lx95u30kpvlIX0KNBwm7Lovlpxjd_P_q4JsKWYrT3W_NKPr8w6oU7K8JUQPDVqhX19_B8LwDJPqdRtkGUIYxSD4nrbJ-m3SEp2BkFo3NEHBv0PZUm6pbxQU&u_asig=05MnJ8y49Xl-DNbLqKN3ifjGwOO0bNzmsII2MXnpcUSuzmVGQ_C85v2ane9ktiIUGIpaFyeWr7CNanYEuQ76YUjsGMz3RuPoI0WSaO3kiViF_sEStSW7JVJThfDplhWz7CDUfD8xtVaRHK709TNQQWtWFgXQ-TPLJv-xFtKvhiWnf9JS7q8ZD7Xtz2Ly-b0kmuyAKRFSVJkkdwVUnyHAIJzdILGopBpbSTPF_5v0X7MVImSbENJskhuvux742wAf8ZbhhSO2NLVUSswjCy6InLeu3h9VXwMyh6PgyDIVSG1W8oEoTudexHI-N02FIXTvpVv8P5eW-8dFUgAAovUv7CNLNOXiKUFS3eOnbcLpPjlE02FLTWPXCSYqFz_k3_dc6lmWspDxyAEEo4kbsryBKb9Q&u_aref=lekYE0bxDYTBPBIgNqUVglOWHkw%3D

接口当中有我们需要的数据,我们可以直接解析JSON数据获取到对应的数据

### 3、实现爬取数据
创建JobProcessor
```java
@Component
public class JobProcessor implements PageProcessor {
private Integer pageNUm = 1;
private String url = "https://search.51job.com/list/000000,000000,0000,32%252c01,9,99,java,2,1.html?lang=c&postchannel=0000&workyear=99&cotype=99°reefrom=99&jobterm=99&companysize=99&ord_field=0&dibiaoid=0&line=&welfare=&u_atoken=e3cd9fd1-142a-456b-ae9a-48e9a7765c08&u_asession=01Vz6mSyKiCoM4C4KAw5n4_gDRC3jSjCfrX5Cx8F4ednl0cMO3TgJ1Lx95u30kpvlIX0KNBwm7Lovlpxjd_P_q4JsKWYrT3W_NKPr8w6oU7K8JUQPDVqhX19_B8LwDJPqdRtkGUIYxSD4nrbJ-m3SEp2BkFo3NEHBv0PZUm6pbxQU&u_asig=05MnJ8y49Xl-DNbLqKN3ifjGwOO0bNzmsII2MXnpcUSuzmVGQ_C85v2ane9ktiIUGIpaFyeWr7CNanYEuQ76YUjsGMz3RuPoI0WSaO3kiViF_sEStSW7JVJThfDplhWz7CDUfD8xtVaRHK709TNQQWtWFgXQ-TPLJv-xFtKvhiWnf9JS7q8ZD7Xtz2Ly-b0kmuyAKRFSVJkkdwVUnyHAIJzdILGopBpbSTPF_5v0X7MVImSbENJskhuvux742wAf8ZbhhSO2NLVUSswjCy6InLeu3h9VXwMyh6PgyDIVSG1W8oEoTudexHI-N02FIXTvpVv8P5eW-8dFUgAAovUv7CNLNOXiKUFS3eOnbcLpPjlE02FLTWPXCSYqFz_k3_dc6lmWspDxyAEEo4kbsryBKb9Q&u_aref=lekYE0bxDYTBPBIgNqUVglOWHkw%3D";
@Override
public void process(Page page) {
//发起请求后解析JSON接口当中的数据
String jsonStr = page.getJson().toString();
//转换为JSONObject对象
JSONObject parse = (JSONObject) JSONObject.parse(jsonStr);
//获取对应的值,转换为JSONArray数组
JSONArray array = JSONArray.parseArray(parse.getString("engine_jds"));
List jobInfoList = new ArrayList();
for (Object o : array) {
//循环遍历获取数据
JobInfo jobInfo = new JobInfo();
JSONObject parseStr = (JSONObject) JSONObject.parse(o.toString());
jobInfo.setCompanyName(parseStr.getString("company_name"));
jobInfo.setCompanyAddr(parseStr.getString("workarea_text"));
//attribute_text
jobInfo.setCompanyInfo(
parseStr.getString("company_name")
+parseStr.getString("companytype_text")
+parseStr.getString("companyind_text")
+parseStr.getString("companysize_text")
+parseStr.getString("workarea_text")
);
jobInfo.setJobName(parseStr.getString("job_name")+"("+parseStr.getString("companyind_text")+")");
jobInfo.setJobAddr(parseStr.getString("workarea_text"));
jobInfo.setJobInfo(
parseStr.getString("job_title")
+parseStr.getString("companytype_text")
+parseStr.getString("attribute_text")
+parseStr.getString("jobwelf")
+parseStr.getString("jobwelf_list")
);
jobInfo.setSalaryMinMax(parseStr.getString("providesalary_text"));
jobInfo.setUrl(parseStr.getString("job_href"));
jobInfo.setTime(parseStr.getString("updatedate"));
//下一页URL
for(int i = 2;i < 10;i++){
String bkurl = "搜索之后的地址";
//把URL放到任务队列当中
page.addTargetRequest(bkurl);
}
//保存到内存当中
jobInfoList.add(jobInfo);
}
page.putField("jobInfoList",jobInfoList);
}
private Site site = Site.me()
.setCharset("gbk") //设置编码
.setTimeOut(10 * 1000) //设置超时时间
.setRetrySleepTime(3000) //设置重试的间隔时间
.setRetryTimes(3)
.addHeader("Accept","application/json, text/javascript, */*; q=0.01")
.addHeader("Accept-Encoding","gzip, deflate, br")
.addHeader("Connection","keep-alive")
.addHeader("Cookie","登录后找到对应的Cookie")
.addHeader("Host","search.51job.com")
.addHeader("Referer","搜索之后的地址")
.addHeader("Sec-Fetch-Dest","empty")
.addHeader("Sec-Fetch-Mode","cors")
.addHeader("Sec-Fetch-Site","same-origin")
.addHeader("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0")
.addHeader("X-Requested-With","XMLHttpRequest")
; //设置重试次数
@Override
public Site getSite() {
return site;
}
//initialDelay当任务启动后等待多久执行方法
//fixedDelay每隔多久执行方法
@Scheduled(initialDelay = 1000,fixedDelay = 100*1000)
public void process(){
Spider.create(new JobProcessor())
.addUrl(url)
.setScheduler(new QueueScheduler().setDuplicateRemover(new BloomFilterDuplicateRemover(100000)))
.thread(10)
.run();
;
}
}
```
运行测试

### 4、保存数据到Mysql当中
创建SpringDataPipeline
```java
@Component
public class SpringDataPipeline implements Pipeline {
@Autowired
private JobInfoService jobInfoService;
@Override
public void process(ResultItems resultItems, Task task) {
//获取封装好的招聘详情对象
List jobInfoList = resultItems.get("jobInfoList");
//判断数据是否不为空
if(!ObjectUtils.isEmpty(jobInfoList)){
for (JobInfo jobInfo : jobInfoList) {
jobInfoService.save(jobInfo);
}
}
}
}
```
完善JobProcessor
```java
@Autowired
private SpringDataPipeline springDataPipeline;
```
```java
@Scheduled(initialDelay = 1000,fixedDelay = 100*1000)
public void process(){
Spider.create(new JobProcessor())
.addUrl(url)
.setScheduler(new QueueScheduler().setDuplicateRemover(new BloomFilterDuplicateRemover(100000)))
.thread(10)
.addPipeline(springDataPipeline)
.run();
;
}
```
运行测试
运行成功

MYSQL当中数据插入成功

### 5、源代码下载
[https://download.csdn.net/download/qq_44757034/87188021](https://download.csdn.net/download/qq_44757034/87188021)
## 十、spider-flow
因为是图形化的具体查询官网教程即可
官网:[https://www.spiderflow.org/](https://www.spiderflow.org/)
