Java简单爬虫入门案例
所需知识: HttpClient + [Jsoup本次接口返回JSON不需要Jsoup解析页面],[Jackson解析JSON,大家也可以用其他工具解析JSON] HttpClient用于模拟发起请求,Jsoup用于解析,虽然Jsoup可以发起请求,但HttpClient拥有线程池等,可以自定义配置,因此我们一般采用HttpClient发起请求,Jsoup仅用于解析html页面。
一、业务需求
二、思路分析
三、代码实现
四、常见问题
业务需求
简单爬取到豆瓣图书的选影视模块前100本,并将图书的基本信息爬取下来存储中数据库中(mysql)
https://movie.douban.com/tag/#/ 目标网址
思路分析
打开这个网址我们可以看到
页面展示了一些电影信息,为简单起,我们直接就爬取默认分类。
可以看到页面有20条电影信息,但是这些信息怎么来的呢?让我们右键检查打开Chrome浏览器的开发者模式,点击Network,选择 XHR类型,然后F5重新加载页面,我们可以看到,出现了一条异步请求。
我们点击这条请求,然后点response,可以看到,请求直接返回了JSON数据,看来我们已经找到了要爬取的地址了。
接下来我们分析url
我们在页面上点击加载更多,在观察XHR,能够发现又发起了一次请求,我们多点几次对比来看。
https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start=0
https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start=20
https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start=40
https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start=60
通过这四个请求的url我们可以发现,只有start在变,并且每次请求会多出20条数据,那么这个start就代表了起始的第几条。然后请求过来20条数据。
分析到这里,我们已经清楚如何获取到所需的数据了,下面我们开始编写代码。
代码实现
- 数据库表创建
我们选择接口返回数据中的一条查看。
因此,我们将如图这些数据存入数据库即可,对于图片,我们将图片存入本地,将图片名称存入数据库中即可。
建表语句如下(在navicat中选择数据库,新建查询,粘贴进去执行):
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for book
-- ----------------------------
DROP TABLE IF EXISTS `book`;
CREATE TABLE `book` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`directors` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`rate` double NULL DEFAULT NULL,
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`casts` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
-
环境搭建
创建一个maven项目,不使用骨架,我们只是为了演示爬取并存储数据,与在web项目中核心代码相同。
导入坐标 (我为了便于理解,我使用尽量少的jar包,所以代码不是特别整洁)
<dependencies> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.7</version> </dependency> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.10.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> <!--解析JSON--> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.10.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.10.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.10.2</version> </dependency> </dependencies>
配置Mybatis
这里我使用的是Mybatis,因为在一般web项目中都使用mybatis,大家也可以自行使用其他(jdbc),我们只涉及简单的sql语句。
我使用的是Mysql8版本,Mybatis使用的是注解开发
在Resources下创建sqlMapConfig.xml文件,注意我使用的Mysql8版本,如果你们使用的是5版本需要更改下driver和url
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 数据源环境-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://127.0.0.1:3306/db1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT&allowPublicKeyRetrieval=true" />
<property name="username" value="root" />
<property name="password" value="jiajie123" />
</dataSource>
</environment>
</environments>
<!-- 配置映射关系-->
<mappers>
<package name="cn.jayjia.mapper" />
</mappers>
</configuration>
- 创建实体类
public class Book {
private Integer id;
private String directors;
private Double rate;
private String url;
private String casts;
private String cover;
private String title;
添加getter/setter方法,我就不写了,大家注意添加
}
- 创建BookMapper用于操作数据库
package cn.jayjia.mapper;
import cn.jayjia.pojo.Book;
import org.apache.ibatis.annotations.Insert;
public interface BookMapper {
@Insert("insert into book values(null, #{directors}, #{rate}, #{url}, #{casts}, #{cover}, #{title})")
void save(Book book);
}
- 编写核心代码
注释比较清晰,大家可以直接从main方法入手
代码重点主要在于 doGetContent() 和 doGetImage()
连接池管理器和RequestConfig都可以不配置,我在代码中注释了,如果大家觉得影响阅读可以不适用。但User-Agent头信息必须添加
public class Main {
// HttpClient连接池管理器
private static PoolingHttpClientConnectionManager manager;
// 解析JSON
private static ObjectMapper mapper = new ObjectMapper();
public static void main(String[] args) throws JsonProcessingException {
SqlSession sqlSession = getSqlSession();
// 获取操作数据库对象
BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);
initPoolingManager(); // 初始化连接池管理器并设置参数
String url = "https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=&start="; // url后start变化
for (int i = 0; i < 100; i += 20) {
String content = doGetContent(url + i); // 请求接口返回JSON数据
JsonNode jsonNode = mapper.readTree(content);
// 我们观察返回的json数据,数据是在data下的,我们要注意,先获取data才能获取想要的数据
jsonNode = jsonNode.get("data");
for (JsonNode node : jsonNode) {
Book book = new Book();
book.setDirectors(node.get("directors").asText());
book.setRate(node.get("rate").asDouble());
book.setUrl(node.get("url").asText());
book.setCasts(node.get("casts").asText());
book.setTitle(node.get("title").asText());
// 下载图片并将图片名存储到数据库中
book.setCover(doGetImage(node.get("cover").asText()));
// 将图书信息存储到数据库中
bookMapper.save(book);
}
}
System.out.println("爬取完成!");
}
// 请求图片下载到本地并返回图片名
private static String doGetImage(String url) {
// 创建HttpClient对象,大家也可以不创建管理器 直接 CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(manager).build();
// 创建Get请求
HttpGet httpGet = new HttpGet(url);
// 设置请求头信息,User-Agent欺骗网址是浏览器访问
httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36");
httpGet.setConfig(getConfig()); // 设置get请求的一些参数,如连接时间,可以不设置
CloseableHttpResponse response = null; // 返回结果
// 前面都一样,只是处理结果不一样
try {
response = httpClient.execute(httpGet);
if (response.getStatusLine().getStatusCode() == 200) { // 如果请求成功再处理数据
String picName = UUID.randomUUID().toString(); // 使用UUID生成随即名称防止重复
picName += ".png";//别忘记加上文件后缀,我们统一存成png格式
OutputStream os = new FileOutputStream("F:/img/douban/" + picName + ".png");
response.getEntity().writeTo(os); // 使用writeTo将图片通过字节流传输
return picName;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (response != null) { // 要记得关闭reponse,不需要关闭HttpClient因为我们使用了连接池
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return ""; // 请求失败
}
public static String doGetContent(String url) {
// 创建HttpClient对象,大家也可以不创建管理器 直接 CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(manager).build();
// 创建Get请求
HttpGet httpGet = new HttpGet(url);
// 设置请求头信息,User-Agent欺骗网址是浏览器访问
httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36");
httpGet.setConfig(getConfig()); // 设置get请求的一些参数,如连接时间,可以不设置
CloseableHttpResponse response = null; // 返回结果
try {
response = httpClient.execute(httpGet);
if (response.getStatusLine().getStatusCode() == 200) { // 如果请求成功再处理数据
return EntityUtils.toString(response.getEntity(), "utf8");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (response != null) { // 要记得关闭reponse,不需要关闭HttpClient因为我们使用了连接池
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return ""; // 请求失败
}
private static void initPoolingManager() {
// 创建HttpClient连接池管理器, 并设置连接数
manager = new PoolingHttpClientConnectionManager();
manager.setMaxTotal(100);
manager.setDefaultMaxPerRoute(10);
}
private static SqlSession getSqlSession(){
// 加载配置文件
InputStream resourceAsStream = Main.class.getClassLoader().getResourceAsStream("sqlMapConfig.xml");
// 获取sqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 获取session对象并设置事务自动提交
return sqlSessionFactory.openSession(true);
}
private static RequestConfig getConfig() {
RequestConfig config = RequestConfig.custom().setConnectionRequestTimeout(2000) // 创建时间
.setConnectTimeout(4000) // 获取数据连接时间
.setSocketTimeout(10*1000) // 数据传输时间
.build();
return config;
}
}
至此,代码编写完成,运行我们就可以看到数据库的数据保存成功并且图片也下载成功了。
常见问题
- javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
解决方案:SSLHandshakeException解决方案 - 我们访问接口获取的reponse和我们在页面上的reponse不一样,拿不到想想要的数据。
解决方案:打开浏览器的network,找到请求,将请求中的Request headers都添加到我们的HttpGet/或HttpPost中即可。