学于黑马和传智播客联合做的教学项目 感谢
黑马官网
传智播客官网
微信搜索"艺术行者",关注并回复关键词"webcrawler"获取视频和教程资料!
b站在线视频
学习目标
能够完成爬虫入门程序
能够说出爬虫解决的问题
能够使用HttpClient的Get请求
能够使用HttpClient的Post请求
能够设置HttpClient的连接池
能够使用Jsoup解析字符串、文件中的Html数据
能够使用dom方式查询解析Html
能够使用选择器查询解析Html
能够说出SKU和SPU的区别
能够实现案例
1.课程计划
网络爬虫介绍
入门程序
HttpClient抓取数据
Jsoup解析数据
爬虫案例
2.网络爬虫
2.1 网络爬虫介绍
在大数据时代,信息的采集是一项重要的工作,而互联网中的数据是海量的,如果单纯靠人力进行信息采集,不仅低效繁琐,搜集的成本也会提高。如何自动高效地获取互联网中我们感兴趣的信息并为我们所用是一个重要的问题,而爬虫技术就是为了解决这些问题而生的。
网络爬虫(Web crawler)也叫做网络机器人,可以代替人们自动地在互联网中进行数据信息的采集与整理。它是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本,可以自动采集所有其能够访问到的页面内容,以获取相关数据。
从功能上来讲,爬虫一般分为数据采集,处理,储存三个部分。爬虫从一个或若干初始网页的URL开始,获得初始网页上的URL,在抓取网页的过程中,不断从当前页面上抽取新的URL放入队列,直到满足系统的一定停止条件。
2.2 为什么学网络爬虫
我们初步认识了网络爬虫,但是为什么要学习网络爬虫呢?只有清晰地知道我们的学习目的,才能够更好地学习这一项知识。在此,总结了4种常见的学习爬虫的原因:
- 可以实现搜索引擎
我们学会了爬虫编写之后,就可以利用爬虫自动地采集互联网中的信息,采集回来后进行相应的存储或处理,在需要检索某些信息的时候,只需在采集回来的信息中进行检索,即实现了私人的搜索引擎。
- 大数据时代,可以让我们获取更多的数据源。
在进行大数据分析或者进行数据挖掘的时候,需要有数据源进行分析。我们可以从某些提供数据统计的网站获得,也可以从某些文献或内部资料中获得,但是这些获得数据的方式,有时很难满足我们对数据的需求,而手动从互联网中去寻找这些数据,则耗费的精力过大。此时就可以利用爬虫技术,自动地从互联网中获取我们感兴趣的数据内容,并将这些数据内容爬取回来,作为我们的数据源,再进行更深层次的数据分析,并获得更多有价值的信息。
- 可以更好地进行搜索引擎优化(SEO)。
对于很多SEO从业者来说,为了更好的完成工作,那么就必须要对搜索引擎的工作原理非常清楚,同时也需要掌握搜索引擎爬虫的工作原理。
而学习爬虫,可以更深层次地理解搜索引擎爬虫的工作原理,这样在进行搜索引擎优化时,才能知己知彼,百战不殆。
- 有利于就业。
从就业来说,爬虫工程师方向是不错的选择之一,因为目前爬虫工程师的需求越来越大,而能够胜任这方面岗位的人员较少,所以属于一个比较紧缺的职业方向,并且随着大数据时代和人工智能的来临,爬虫技术的应用将越来越广泛,在未来会拥有很好的发展空间。
3.网络爬虫入门程序
3.1 环境准备
创建Maven工程并给pom.xml加入依赖
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!-- HttpClient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
加入log4j.properties
log4j.rootLogger=DEBUG,A1
log4j.logger.org.example = DEBUG
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.2 编写代码
编写最简单的爬虫,抓取传智播客首页:https://www.apple.com/cn/
package org.example.test;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
/**
* @author HackerStar
* @create 2020-05-20 21:14
*/
public class WebCrawler01 {
public static void main(String[] args) throws Exception {
//1. 打开浏览器(创建HttpClient对象)
CloseableHttpClient httpClient = HttpClients.createDefault();
//2. 输入网址(发起get请求创建HttpGet对象)
HttpGet httpGet = new HttpGet("https://www.apple.com/cn/");
//3.按回车,发起请求,返回响应(使用HttpClient对象发起请求)
CloseableHttpResponse response = httpClient.execute(httpGet);
//4. 解析响应,获取数据
//判断状态码是否是200
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity httpEntity = response.getEntity();
String content = EntityUtils.toString(httpEntity, "utf8");
System.out.println(content);
}
}
}
测试结果:可以获取到页面数据
4.HttpClient
网络爬虫就是用程序帮助我们访问网络上的资源,我们一直以来都是使用HTTP协议访问互联网的网页,网络爬虫需要编写程序,在这里使用同样的HTTP协议访问网页。
这里我们使用Java的HTTP协议客户端 HttpClient这个技术,来实现抓取网页数据。
4.1 GET请求
package org.example.test;
import org.apache.http.ParseException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
/**
* @author HackerStar
* @create 2020-05-20 21:29
*/
public class WebCrawler02 {
public static void main(String[] args) throws Exception {
CloseableHttpClient httpClient = null;
CloseableHttpResponse response = null;
try {
//创建HttpClient对象
httpClient = HttpClients.createDefault();
//创建HttpGet对象,设置url访问地址
HttpGet httpGet = new HttpGet("https://www.apple.com/cn");
//使用HttpClient发起请求,获取response
response = httpClient.execute(httpGet);
if (response.getStatusLine().getStatusCode() == 200) {
String content = EntityUtils.toString(response.getEntity(), "utf8");
System.out.println(content);
}
} catch (IOException e) {
e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
} finally {
if(response!=null){
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (httpClient != null) {
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4.2 带参数的GET请求
package org.example.test;
import org.apache.http.ParseException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
/**
* @author HackerStar
* @create 2020-05-21 09:05
*/
public class WebCrawler03 {
public static void main(String[] args) throws Exception {
CloseableHttpClient httpClient = null;
CloseableHttpResponse response = null;
URIBuilder uriBuilder = null;
try {
//创建HttpClient对象
httpClient = HttpClients.createDefault();
//创建URIBuilder,设置url访问地址
uriBuilder = new URIBuilder("https://www.acfun.cn/search?");
//设置参数
uriBuilder.setParameter("keyword", "apple");
//创建HttpGet对象,设置url访问地址
HttpGet httpGet = new HttpGet(uriBuilder.build());
System.out.println("发起请求的信息:" + httpGet);
//使用HttpClient发起请求,获取response
response = httpClient.execute(httpGet);
//解析响应
if (response.getStatusLine().getStatusCode() == 200) {
String content = EntityUtils.toString(response.getEntity(), "utf8");
System.out.println(content);
}
} catch (IOException e) {
e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
} finally {
//关闭流
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (httpClient != null) {
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
请求结果
4.3 POST请求
package org.example.test;
import org.apache.http.ParseException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
/**
* @author HackerStar
* @create 2020-05-21 09:37
*/
public class WebCrawler04 {
public static void main(String[] args) throws Exception {
//创建连接池管理器
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
//设置最大连接数(也就是最多分配100浏览器)
cm.setMaxTotal(100);
//设置每个主机的最大连接数(也就是给一个网站最多分配10个浏览器)
cm.setDefaultMaxPerRoute(10);
//使用连接池管理器发起请求
doPost(cm);
}
private static void doPost(PoolingHttpClientConnectionManager cm) throws Exception {
CloseableHttpClient buildClient = null;
HttpPost httpPost = null;
CloseableHttpResponse response = null;
try {
//不是每次创建新的HttpClient,而是从连接池中获取HttpClient对象
buildClient = HttpClients.custom().setConnectionManager(cm).build();
httpPost = new HttpPost("https://www.acfun.cn");
System.out.println("请求:" + httpPost);
response = buildClient.execute(httpPost);
if (response.getStatusLine().getStatusCode() == 200) {
String content = EntityUtils.toString(response.getEntity(), "utf8");
System.out.println(content);
}
} catch (IOException e) {
e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//不能关闭HttpClient,由连接池管理HttpClient
}
}
}
请求结果:
4.4 带参数的POST请求
package org.example.test;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* @author HackerStar
* @create 2020-05-21 09:37
*/
public class WebCrawler05 {
public static void main(String[] args) throws Exception {
//创建连接池管理器
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
//设置最大连接数(也就是最多分配100浏览器)
cm.setMaxTotal(100);
//设置每个主机的最大连接数(也就是给一个网站最多分配10个浏览器)
cm.setDefaultMaxPerRoute(10);
//使用连接池管理器发起请求
doPost(cm);
}
private static void doPost(PoolingHttpClientConnectionManager cm) throws Exception {
CloseableHttpClient buildClient = null;
HttpPost httpPost = null;
CloseableHttpResponse response = null;
//创建表单的Entity对象,第一个参数就是封装好的表单数据,第二个参数就是编码
List<NameValuePair> params = null;
UrlEncodedFormEntity formEntity = null;
try {
//不是每次创建新的HttpClient,而是从连接池中获取HttpClient对象
buildClient = HttpClients.custom().setConnectionManager(cm).build();
params = new ArrayList<>();
params.add(new BasicNameValuePair("keyword","java"));
//创建表单的Entity对象,第一个参数就是封装好的表单数据,第二个参数就是编码
formEntity = new UrlEncodedFormEntity(params, "UTF-8");
//创建HttpPost对象,设置url访问地址
httpPost = new HttpPost("https://www.bilibili.com/search");
httpPost.setEntity(formEntity);
System.out.println("请求:" + httpPost);
//使用HttpClient发起请求,获取response
response = buildClient.execute(httpPost);
//解析响应
if (response.getStatusLine().getStatusCode() == 200) {
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
System.out.println(content);
}
} catch (IOException e) {
e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//不能关闭HttpClient,由连接池管理HttpClient
}
}
}
请求结果:
4.5 连接池
如果每次请求都要创建HttpClient,会有频繁创建和销毁的问题,可以使用连接池来解决这个问题。
测试以下代码,并断点查看每次获取的HttpClient都是不一样的。
package org.example.test;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
/**
* @author HackerStar
* @create 2020-05-21 10:31
*/
public class WebCrawler06 {
public static void main(String[] args) {
//创建连接池管理器
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
//设置最大连接数
cm.setMaxTotal(100);
//设置每个主机的最大连接数
cm.setDefaultMaxPerRoute(10);
//使用连接池管理器发起请求
doGet(cm);
doGet(cm);
}
private static void doGet(PoolingHttpClientConnectionManager cm) {
//不是每次创建新的HttpClient,而是从连接池中获取HttpClient对象
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
HttpGet httpGet = new HttpGet("https://www.baidu.com/");
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpGet);
if (response.getStatusLine().getStatusCode() == 200) {
String content = EntityUtils.toString(response.getEntity(), "utf8");
System.out.println(content.length());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
//不能关闭HttpClient,由连接池管理HttpClient
//httpClient.close();
}
}
}
}
4.6 请求参数
有时候因为网络,或者目标服务器的原因,请求需要更长的时间才能完成,我们需要自定义相关时间。
package org.example.test;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
/**
* @author HackerStar
* @create 2020-05-21 10:31
*/
public class WebCrawler07 {
public static void main(String[] args) {
//创建连接池管理器
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
//设置最大连接数
cm.setMaxTotal(100);
//设置每个主机的最大连接数
cm.setDefaultMaxPerRoute(10);
//使用连接池管理器发起请求
doGet(cm);
}
private static void doGet(PoolingHttpClientConnectionManager cm) {
//不是每次创建新的HttpClient,而是从连接池中获取HttpClient对象
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
//创建HttpGet对象,设置url访问地址
HttpGet httpGet = new HttpGet("https://www.baidu.com/");
//配置请求信息
RequestConfig config = RequestConfig.custom().setConnectTimeout(1000)//创建连接的最长时间,单位是毫秒
.setConnectionRequestTimeout(500)//设置获取连接的最长时间,单位是毫秒
.setSocketTimeout(10 * 1000)//设置数据传输的最长时间,单位是毫秒
.build();
//给请求设置请求信息
httpGet.setConfig(config);
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpGet);
if (response.getStatusLine().getStatusCode() == 200) {
String content = EntityUtils.toString(response.getEntity(), "utf8");
System.out.println(content.length());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
//不能关闭HttpClient,由连接池管理HttpClient
//httpClient.close();
}
}
}
}
5. Jsoup
我们抓取到页面之后,还需要对页面进行解析。可以使用字符串处理工具解析页面,也可以使用正则表达式,但是这些方法都会带来很大的开发成本,所以我们需要使用一款专门解析html页面的技术。
5.1 Jsoup介绍
Jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。
jsoup的主要功能如下:
从一个URL,文件或字符串中解析HTML;
使用DOM或CSS选择器来查找、取出数据;
可操作HTML元素、属性、文本;
加入Jsoup依赖:
<!--Jsoup-->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.10.3</version>
</dependency>
<!--测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!--工具-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
5.2 Jsoup解析
5.2.1 解析url
package org.example.test;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import org.junit.Test;
import java.net.MalformedURLException;
import java.net.URL;
/**
* @author HackerStar
* @create 2020-05-21 10:46
*/
public class test {
@Test
public void testJouspUrl() throws Exception {
/**
* 解析URL地址
* 第一个参数:访问的url
* 第二个参数:访问时候的超时时间
*/
Document dom = Jsoup.parse(new URL("https://www.apple.com/cn"), 1000);
//使用标签选择器,获取title标签中的内容
String title = dom.getElementsByTag("title").first().text();
System.out.println(title);
}
}
PS:虽然使用Jsoup可以替代HttpClient直接发起请求解析数据,但是往往不会这样用,因为实际的开发过程中,需要使用到多线程,连接池,代理等等方式,而Jsoup对这些的支持并不是很好,所以我们一般把jsoup仅仅作为Html解析工具使用
5.2.2 解析字符串
先准备以下html文件
<!DOCTYPE html>
<html>
<head>
<title>你的下一台电脑,何必是电脑。</title>
</head>
<body>
<div class="city">
<h3 id="city_bj">郑州中心</h3>
<fb:img src="/2018czgw/images/slogan.jpg" class="slogan"/>
<div class="city_in">
<div class="city_con" style="display: none;">
<ul>
<li id="test" class="class_a class_b">
<a href="https://www.apple.com/cn" target="_blank">
<span class="s_name">郑州</span>
</a>
</li>
<li>
<a href="https://www.apple.com" target="_blank">
<span class="s_name">加利福尼亚</span>
</a>
</li>
<li>
<a href="https://www.apple.com/cn" target="_blank">
<span abc="123" class="s_name">广州</span>
</a>
</li>
<ul>
<li>北京</li>
</ul>
</ul>
</div>
</div>
</div>
</body>
</html>
Jsoup可以直接输入字符串,并封装为Document对象
@Test
public void testJsoupString() throws Exception {
//使用工具类读取文件,获取字符串
String html = FileUtils.readFileToString(new File("/Development/Java/IDEA_Project/WebCrawler/Day01/src/web/Test.html"), "UTF-8");
//解析字符串
Document dom = Jsoup.parse(html);
//获取title的内容
Element title = dom.getElementsByTag("title").first();
System.out.println(title);
}
5.2.3 解析文件
Jsoup可以直接解析文件,并封装为Document对象
@Test
public void testJsoupHtml() throws Exception {
//解析文件
Document dom = Jsoup.parse(new File("/Development/Java/IDEA_Project/WebCrawler/Day01/src/web/Test.html"), "UTF-8");
//获取title的内容
Element title = dom.getElementsByTag("title").first();
System.out.println(title);
}
5.2.4 使用dom方式遍历文档
元素获取
根据id查询元素getElementById
根据标签获取元素getElementsByTag
根据class获取元素getElementsByClass
根据属性获取元素getElementsByAttribute
@Test
public void testJsoupDom() throws Exception {
//解析文件
Document dom = Jsoup.parse(new File("/Development/Java/IDEA_Project/WebCrawler/Day01/src/web/Test.html"), "UTF-8");
/**
* 获取元素
*/
//1.根据id查询元素getElementById
//Element element = dom.getElementById("city_zz");
//2.根据标签获取元素getElementsByTag
//Element element = dom.getElementsByTag("span").first();
//3.根据class获取元素getElementByClass
//Element element = dom.getElementsByClass("class_a class_b").first();
//Element element = dom.getElementsByClass("class_a").first();
//4.根据属性获取原色getElementByAttribute
//Element element = dom.getElementsByAttribute("href").first();
Element element = dom.getElementsByAttribute("id").first();
System.out.println(element);
}
元素中获取数据
从元素中获取id
从元素中获取className
从元素中获取属性的值attr
从元素中获取所有属性attributes
从元素中获取文本内容text
@Test
public void testData() throws Exception {
//解析文件,获取Document
Document dom = Jsoup.parse(new File("/Development/Java/IDEA_Project/WebCrawler/Day01/src/web/Test.html"), "UTF-8");
//根据id获取元素
Element element = dom.getElementById("test");
String str = "";
/**
* 从元素中获取数据
*/
//1.从元素中获取id
str = element.id();
System.out.println("获取到的id是:" + str);
//2.从元素中获取className
Set<String> strings = element.classNames();
System.out.print("获取到的classNames是:" );
strings.forEach( string-> System.out.print(string + "\t"));
System.out.println();
//3.从元素中获取属性的值attr
String aClass = element.attr("class");
String id = element.attr("id");
System.out.println("获取的属性的值为:" + "id:" + id + "\tclass:" + aClass);
//4.从元素中获取所有属性attributes
Attributes attributes = element.attributes();
System.out.println("所有属性:" + attributes);
//5.从元素中获取文本内容text
String text = element.text();
System.out.println("文本内容是:" + text);
}
5.2.5 使用选择器语法查找元素
Jsoup elements对象支持类似于CSS (或jquery)的选择器语法,来实现非常强大和灵活的查找功能。这个select 方法在Document, Element,或Elements对象中都可以使用。且是上下文相关的,因此可实现指定元素的过滤,或者链式选择访问。
Select方法将返回一个Elements集合,并提供一组方法来抽取和处理结果。
5.2.6Selector选择器概述
tagname: 通过标签查找元素,比如:span
#id: 通过ID查找元素,比如:# city_bj
.class: 通过class名称查找元素,比如:.class_a
[attribute]: 利用属性查找元素,比如:[abc]
[attr=value]: 利用属性值来查找元素,比如:[class=s_name]
@Test
public void testSelector() throws Exception {
//解析文件,获取Document
Document dom = Jsoup.parse(new File("/Development/Java/IDEA_Project/WebCrawler/Day01/src/web/Test.html"), "UTF-8");
//tagname: 通过标签查找元素,比如:span
Elements elements = dom.select("span");
System.out.print("通过标签查找元素:");
elements.forEach(element -> System.out.print(element + "\t"));
System.out.println();
//#id: 通过ID查找元素,比如:#city_bj
Element element = dom.select("#test").first();
System.out.println("通过ID查找元素:" + element);
//.class:通过class名称查找元素,比如:.class_a
Element element1 = dom.select(".class_a").first();
System.out.println("通过class名称查找元素:" + element1);
//[attribute]: 利用属性查找元素,比如:[abc]
Element element2 = dom.select("[abc]").first();
System.out.println("利用属性查找元素:" + element2);
//[attr=value]: 利用属性值来查找元素,比如:[class=s_name]
Elements elements1 = dom.select("[class=s_name]");
System.out.print("通过标签查找元素:");
elements1.forEach(element3 -> System.out.print(element3 + "\t"));
System.out.println();
}
5.2.7 Selector选择器组合使用
el#id: 元素+ID,比如: h3#city_bj
el.class: 元素+class,比如: li.class_a
el[attr]: 元素+属性名,比如: span[abc]
任意组合: 比如:span[abc].s_name
ancestor child: 查找某个元素下子元素,比如:.city_con li 查找"city_con"下的所有li
parent > child: 查找某个父元素下的直接子元素,比如:
.city_con > ul > li 查找city_con第一级(直接子元素)的ul,再找所有ul下的第一级li
parent > *: 查找某个父元素下所有直接子元素
@Test
public void testSelector2() throws Exception {
//解析文件,获取Document
Document dom = Jsoup.parse(new File("/Development/Java/IDEA_Project/WebCrawler/Day01/src/web/Test.html"), "UTF-8");
//el#id: 元素+ID,比如: h3#city_zz
System.out.println("el#id:" + dom.select("h3#city_zz").first());
//el.class: 元素+class,比如: li.class_a
System.out.println("el.class:" + dom.select("li.class_a").first());
//el[attr]: 元素+属性名,比如: span[abc]
System.out.println("el[attr]:" + dom.select("span[abc]").first());
//任意组合: 比如:span[abc].s_name
System.out.println("任意组合: " + dom.select("pan[abc].s_name").first());
//ancestor child: 查找某个元素下子元素,比如:.city_con li 查找"city_con"下的所有li
System.out.println("ancestor child:\n" + dom.select(".city_con li"));
//parent > child: 查找某个父元素下的直接子元素,比如:
//.city_con > ul > li 查找city_con第一级(直接子元素)的ul,再找所有ul下的第一级li
System.out.println("parent > child:\n" + dom.select(".city_con > ul > li"));
//parent > *: 查找某个父元素下所有直接子元素
System.out.println("parent > *:\n" + dom.select(".city_con > ul > *"));
}
6.爬虫案例
目前京东和淘宝修改了代码,爬数据返回的是登陆连接,所以在此只能学习思路与代码书写,效果看不到了
6.1 需求分析
首先访问京东,搜索手机,分析页面,我们抓取以下商品数据:
商品图片、价格、标题、商品详情页
SPU和SKU
除了以上四个属性以外,我们发现上图中的Iphone11ProMax有四种产品,我们应该每一种都要抓取。那么这里就必须要了解spu和sku的概念。
SPU = Standard Product Unit(标准产品单位):
SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。
例如上图中的Iphone11ProMax就是SPU,包括暗夜绿、深空灰色、金色、银色
SKU=stock keeping unit(库存量单位):
SKU即库存进出计量的单位, 可以是以件、盒、托盘等为单位。SKU是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。在服装、鞋类商品中使用最多最普遍。
例如上图中的苹果手机有几个款式,例如暗夜绿色苹果手机,就是一个sku。
查看页面的源码也可以看出区别
6.2 开发准备
6.2.1 数据库表分析
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 '创建时间',
`updated` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `sku` (`sku`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='京东商品表';
6.2.2 添加依赖
使用Spring Boot+Spring Data JPA和定时任务进行开发,需要创建Maven工程并添加以下依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
</parent>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!--SpringMVC-->
<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>
<!--MySQL连接包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- HttpClient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!--Jsoup-->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.10.3</version>
</dependency>
<!--工具包-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
6.2.3 添加配置文件
加入application.properties配置文件到/src/main/resources
#DB Configuration:
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/WebCrawler
spring.datasource.username=root
spring.datasource.password=root
#JPA Configuration:
spring.jpa.database=MySQL
spring.jpa.show-sql=true
6.3 代码实现
6.3.1 编写poji
package org.example.pojo;
import javax.persistence.*;
import javax.xml.crypto.Data;
/**
* @author HackerStar
* @create 2020-05-21 15:55
*/
@Entity
@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 Data created;
//更新时间
private Data updated;
@Override
public String toString() {
return "Item{" +
"id=" + id +
", spu=" + spu +
", sku=" + sku +
", title='" + title + '\'' +
", price=" + price +
", pic='" + pic + '\'' +
", url='" + url + '\'' +
", created=" + created +
", updated=" + updated +
'}';
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getSpu() {
return spu;
}
public void setSpu(Long spu) {
this.spu = spu;
}
public Long getSku() {
return sku;
}
public void setSku(Long sku) {
this.sku = sku;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public String getPic() {
return pic;
}
public void setPic(String pic) {
this.pic = pic;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Data getCreated() {
return created;
}
public void setCreated(Data created) {
this.created = created;
}
public Data getUpdated() {
return updated;
}
public void setUpdated(Data updated) {
this.updated = updated;
}
}
6.3.2 编写dao
package org.example.dao;
import org.example.pojo.Item;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* @author HackerStar
* @create 2020-05-21 16:02
*/
public interface ItemDao extends JpaRepository<Item, Long> {
}
6.3.3 编写service
ItemService接口
package org.example.service;
import org.example.pojo.Item;
import java.util.List;
/**
* @author HackerStar
* @create 2020-05-21 16:04
*/
public interface ItemService {
//根据条件查询数据
public List<Item> findAll(Item item);
//保存数据
public void save(Item item);
}
ItemServiceImpl实现类
package org.example.service.impl;
import org.example.dao.ItemDao;
import org.example.pojo.Item;
import org.example.service.ItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import java.util.List;
/**
* @author HackerStar
* @create 2020-05-21 16:06
*/
@Service
public class ItemServiceImpl implements ItemService {
@Autowired
private ItemDao itemDao;
@Override
public List<Item> findAll(Item item) {
//声明查询条件
Example<Item> example = Example.of(item);
//根据查询条件进行查询数据
List<Item> all = itemDao.findAll(example);
return all;
}
@Override
@Transactional
public void save(Item item) {
itemDao.save(item);
}
}
6.3.4 编写引导类
package org.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @author HackerStar
* @create 2020-05-21 16:09
*/
@SpringBootApplication
//使用定时任务,需要先开启定时任务,需要添加注解
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
6.3.5 封装HttpClient
我们需要经常使用HttpClient,所以需要进行封装,方便使用。
package org.example.utils;
import org.apache.http.ParseException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Component;
import sun.net.www.http.HttpClient;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.UUID;
/**
* @author HackerStar
* @create 2020-05-21 16:13
*/
@Component
public class HttpUtils {
private PoolingHttpClientConnectionManager cm;
//HttpClient对象
private CloseableHttpClient httpClient = null;
//httpGet请求对象
private HttpGet httpGet = null;
public HttpUtils() {
cm = new PoolingHttpClientConnectionManager();
//设置最大连接数
cm.setMaxTotal(100);
//设置每个主机的最大连接数
cm.setDefaultMaxPerRoute(10);
}
/**
* 根据请求地址下载页面数据
*
* @param url
* @return 页面数据
*/
public String doGetHtml(String url) {
init(url);
CloseableHttpResponse response = null;
try {
//使用HttpClient发起请求,获取响应
response = httpClient.execute(httpGet);
//System.out.println(EntityUtils.toString(response.getEntity(), "utf8"));//查看是否获取到了数据
//解析响应,返回结果
if (response.getStatusLine().getStatusCode() == 200) {
//判断响应体Entity是否不为空,如果不为空就可以使用EntityUtils
if (response.getEntity() != null) {
String content = EntityUtils.toString(response.getEntity(), "utf8");
return content;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return "";
}
/**
* 下载图片
*
* @param url
* @return 图片名称
*/
public String doGetImage(String url) {
init(url);
CloseableHttpResponse response = null;
//使用HttpClient发起请求,获取响应
try {
response = httpClient.execute(httpGet);
//解析响应,下载图片
if (response.getStatusLine().getStatusCode() == 200) {
//判断响应体Entity是否不为空
if (response.getEntity() != null) {
//下载图片
//获取图片的后缀
String extName = url.substring(url.lastIndexOf("."));
//创建图片名,重命名图片
String picName = UUID.randomUUID().toString() + extName;
//下载图片
//声明OutPutStream
OutputStream outputStream = new FileOutputStream(new File("/Development/Java/IDEA_Project/WebCrawler/pic"));
response.getEntity().writeTo(outputStream);
//返回图片名称
return picName;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭response
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//如果下载失败,返回空串
return "";
}
//设置请求信息
private RequestConfig getConfig() {
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(1000)//创建连接的最长时间
.setConnectionRequestTimeout(500)//获取连接的最长时间
.setSocketTimeout(10000)//数据传输的最长时间
.build();
return config;
}
//初始化
private void init(String url) {
//获取HttpClient对象
httpClient = HttpClients.custom().setConnectionManager(cm).build();
//创建httpGet请求对象,设置url地址
httpGet = new HttpGet(url);
//设置请求信息
httpGet.setConfig(getConfig());
}
}
6.3.6 实现数据抓取
使用定时任务,可以定时抓取最新的数据。
package org.example.task;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.pojo.Item;
import org.example.service.ItemService;
import org.example.service.impl.ItemServiceImpl;
import org.example.utils.HttpUtils;
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;
/**
* @author HackerStar
* @create 2020-05-21 16:55
*/
@Component
public class ItemTask {
@Autowired
private HttpUtils httpUtils;
@Autowired
private ItemService itemService;
private static final ObjectMapper MAPPER = new ObjectMapper();
//当下载任务完成后,间隔多长时间进行下一次的任务。
@Scheduled(fixedDelay = 100 * 1000)
public void itemTask() throws Exception {
String url = "https://search.jd.com/Search?keyword=手机&page=";
//按照页面对手机的搜索结果进行遍历解析
for (int i = 1; i < 3; i++) {
String html = httpUtils.doGetHtml(url + i);
parse(html);
}
System.out.println("手机商品页面抓取完毕!");
}
//解析页面,获取商品数据并存储
private void parse(String html) throws Exception {
//解析html获取Document
Document dom = Jsoup.parse(html);
//获取spu信息
Elements spuEles = dom.select("div#J_goodsList > ul > li");
for (Element spuEle :
spuEles) {
//获取spu
long spu = Long.parseLong(spuEle.attr("data-spu"));
//获取sku信息
Elements skuEles = spuEle.select("li.ps-item");
for (Element skuEle :
skuEles) {
long sku = Long.parseLong(skuEle.select("[data-sku]").attr("data-sku"));
// long sku = Long.parseLong(spuEle.attr("data-sku"));//******long sku = Long.parseLong(skuEle.select("[data-sku]").attr("data-sku"));
//根据sku查询商品数据
Item item = new Item();
item.setSku(sku);
List<Item> list = itemService.findAll(item);
if (list.size() > 0) {
//如果商品存在,就进行下一个循环,该商品不保存,因为已存在
continue;
}
//设置商品的spu
item.setSpu(spu);
//获取商品的详情的url
String itemUrl = "https://https://item.jd.com/" + sku + ".html";
item.setUrl(itemUrl);
//获取商品的图片
String picUrl ="https:"+ skuEle.select("img[data-sku]").first().attr("data-lazy-img");
// String picUrl = "https:" + skuEle.select("img[src]").first();//*****String picUrl ="https:"+ skuEle.select("img[data-sku]").first().attr("data-lazy-img");
picUrl = picUrl.replace("n9", "n1");
String picName = httpUtils.doGetImage(picUrl);
item.setPic(picName);
//获取商品的价格
String priceJson = this.httpUtils.doGetHtml("https://p.3.cn/prices/mgets?skuIds=J_" + sku);//******
double price = MAPPER.readTree(priceJson).get(0).get("p").asDouble();
item.setPrice(price);//获取商品的标题
String itemInfo = this.httpUtils.doGetHtml(item.getUrl());
String title = Jsoup.parse(itemInfo).select("div.sku-name").text();
item.setTitle(title);
item.setCreated(new Date());
item.setUpdated(item.getCreated());
//保存商品数据到数据库中
itemService.save(item);
}
}
}
}
7 遇到的异常
7.1 警告:Establishing SSL connection without server’s identity verification is not recommended
WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification
译:请注意:不建议在没有服务器身份验证的情况下建立SSL连接。根据MySQL 5.5.45+、5.6.26+和5.7.6+的要求,如果不设置显式选项,则必须建立默认的SSL连接。您需要通过设置useSSL=false显式地禁用SSL,或者设置useSSL=true并为服务器证书验证提供信任存储
解决方法:
修改application.properties文件中的部分代码
jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
7.2 WARN: spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed
启动springboot jpa,项目启动的时候有个warn的log。
解决方案,在application.properties配置文件中添加如下代码:
spring.jpa.open-in-view=false