爬虫案例
学习了HttpClient和Jsoup,就掌握了如何抓取数据和如何解析数据,接下来,我们做一个小练习,把京东的手机数据抓取下来。
主要目的是HttpClient和Jsoup的学习。
需求分析
首先访问京东,搜索手机,分析页面,我们抓取以下商品数据:
商品图片、价格、标题、商品详情页
SPU和SKU
除了以上四个属性以外,我们发现上图中的苹果手机有四种产品,我们应该每一种都要抓取。那么这里就必须要了解spu和sku的概念
SPU = Standard Product Unit (标准产品单位)
SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。
例如上图中的苹果手机就是SPU,包括红色、深灰色、金色、银色
SKU=stock keeping unit(库存量单位)
SKU即库存进出计量的单位, 可以是以件、盒、托盘等为单位。SKU是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。在服装、鞋类商品中使用最多最普遍。
例如上图中的苹果手机有几个款式,红色苹果手机,就是一个sku
数据库表分析
根据需求分析,我们创建的表如下
代码语言:javascript复制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='京东商品表';
项目示例
使用Spring Boot Spring Data JPA
和定时任务进行开发
添加依赖
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ray</groupId>
<artifactId>crawler-jd</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>crawler-jd</name>
<description>京东爬虫示例</description>
<properties>
<java.version>1.8</java.version>
</properties>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件
代码语言:javascript复制#DB Configuration:
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/ray0804?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
#JPA Configuration:
spring.jpa.database=MySQL
spring.jpa.show-sql=true
# 每次启动清空表,可不用
spring.jpa.hibernate.ddl-auto=create
代码实现
代码语言:javascript复制@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 Date created;
//更新时间
private Date 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 Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
public Date getUpdated() {
return updated;
}
public void setUpdated(Date updated) {
this.updated = updated;
}
}
代码语言:javascript复制public interface ItemDao extends JpaRepository<Item, Long> {
}
代码语言:javascript复制public interface ItemService {
//根据条件查询数据
List<Item> findAll(Item item);
//保存数据
void save(Item item);
}
代码语言:javascript复制@Service
public class ItemServiceImpl implements ItemService {
@Autowired
private ItemDao itemDao;
@Override
public List<Item> findAll(Item item) {
Example example = Example.of(item);
List list = this.itemDao.findAll(example);
return list;
}
@Override
@Transactional
public void save(Item item) {
this.itemDao.save(item);
}
}
封装HttpClient
我们需要经常使用HttpClient,所以需要进行封装,方便使用
代码语言:javascript复制@Component
public class HttpUtils {
// 响应成功
private final int SUCCESS_CODE = 200;
private final PoolingHttpClientConnectionManager manager;
public HttpUtils() {
this.manager = new PoolingHttpClientConnectionManager();
// 设置最大连接数
this.manager.setMaxTotal(200);
// 设置每个主机的并发数
this.manager.setDefaultMaxPerRoute(20);
}
/**
* @Description: 根据请求地址 url 下载页面数据
* @Author: Ray
* @Date: 2020/8/4 0004 16:05
**/
public String getHtml(String url) {
// 获取 HttpClient 对象
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(manager).build();
// 声明 httpGet 请求对象
HttpGet httpGet = new HttpGet(url);
// 设置请求参数 RequestConfig
httpGet.setConfig(this.getConfig());
// 设置一下头信息:模拟环境
httpGet.setHeader("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:62.0) Gecko/20100101 Firefox/62.0");
// 在外面声明,方便后面的关闭操作
CloseableHttpResponse response = null;
try {
// 使用HttpClient发起请求,返回response
response = httpClient.execute(httpGet);
// 解析response返回数据
if (response.getStatusLine().getStatusCode() == SUCCESS_CODE) {
String html = "";
// 如果response。getEntity获取的结果是空,在执行EntityUtils.toString会报错
// 需要对Entity进行非空的判断
if (response.getEntity() != null) {
html = EntityUtils.toString(response.getEntity(), "utf8");
}
return html;
}
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
// 不能关闭,现在使用的是连接管理器
// httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
/**
* @Description: 下载图片
* @Author: Ray
* @Date: 2020/8/4 0004 16:07
**/
public String getImage(String url) {
// 获取 HttpClient 对象
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(manager).build();
// 声明httpGet请求对象
HttpGet httpGet = new HttpGet(url);
// 设置请求参数RequestConfig
httpGet.setConfig(this.getConfig());
CloseableHttpResponse response = null;
try {
// 使用HttpClient发起请求,返回response
response = httpClient.execute(httpGet);
// 解析response下载图片
if (response.getStatusLine().getStatusCode() == SUCCESS_CODE) {
if (response.getEntity() != null) {
// 获取文件后缀
String extName = url.substring(url.lastIndexOf("."));
// 使用uuid生成图片名
String imageName = UUID.randomUUID().toString() extName;
// 指定保存的目录,不存在会创建
String dir = "D:/images/";
File dirPath = new File(dir);
if (!dirPath.exists()) {
dirPath.mkdirs();
}
// 声明输出的文件
File file = new File(dir imageName);
OutputStream outputStream = new FileOutputStream(file);
// 使用响应体输出文件,writeTo 写入到哪里
response.getEntity().writeTo(outputStream);
// 返回生成的图片名
return imageName;
}
}
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
// 关闭连接
response.close();
}
// 不能关闭,现在使用的是连接管理器
// httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
/**
* @Description: 获取请求参数对象
* @Author: Ray
* @Date: 2020/8/4 0004 16:13
**/
private RequestConfig getConfig() {
RequestConfig config = RequestConfig.custom()
// 设置创建连接的超时时间
.setConnectTimeout(1000)
// 设置获取连接的超时时间
.setConnectionRequestTimeout(500)
// 设置数据传输的超时时间
.setSocketTimeout(10000)
.build();
return config;
}
}
实现数据抓取
使用定时任务,可以定时抓取最新的数据
代码语言:javascript复制@Component
public class ItemTask {
@Autowired
private HttpUtils httpUtils;
@Autowired
private ItemService itemService;
public static final ObjectMapper MAPPER = new ObjectMapper();
//设置定时任务执行完成后,再间隔100秒执行一次
@Scheduled(fixedDelay = 1000 * 100)
public void process() throws Exception {
// https://search.jd.com/Search?keyword=手机&wq=手机&page=1&s=1&click=0
String url = "https://search.jd.com/Search?keyword=手机&wq=手机&click=0&page=";
// 遍历执行,获取所有数据
for (int i = 1; i <= 10; i ) {
//发起请求进行访问,获取页面数据,先访问第一页
String newUrl = "";
if (i > 1) {
int page = i 2;
newUrl = url page "&s=" ((50 * (i-1)) 1);
} else {
newUrl = url i "&s=" i;
}
System.out.println(newUrl);
String html = this.httpUtils.getHtml(newUrl);
//解析页面数据,保存数据到数据库中
this.parseHtml(html);
}
System.out.println("执行完毕");
}
//解析页面,并把数据保存到数据库中
private void parseHtml(String html) throws Exception {
//System.out.println(html);
//使用jsoup解析页面
Document document = Jsoup.parse(html);
//获取商品数据 - 直接子元素
Elements spus = document.select("div#J_goodsList > ul > li");
//遍历商品spu数据
for (Element spuEle : spus) {
//获取商品spu
// 2020年8月7日14:52:38 发现 spu 为空的情况,加了个判断
String spu = spuEle.attr("data-spu");
long spuId = 0L;
if (StringUtils.isNotBlank(spu)) {
spuId = Long.parseLong(spu);
}
//获取商品sku数据
Elements skus = spuEle.select("li.ps-item img");
// 获取商品sku
for (Element skuEle : skus) {
Long skuId = Long.parseLong(skuEle.attr("data-sku"));
//判断商品是否被抓取过,可以根据sku判断
Item param = new Item();
param.setSku(skuId);
List<Item> list = this.itemService.findAll(param);
//判断是否查询到结果
if (!list.isEmpty()) {
//如果有结果,表示商品已下载,进行下一次遍历
continue;
}
//保存商品数据,声明商品对象
Item item = new Item();
//商品spu
item.setSpu(spuId);
//商品sku
item.setSku(skuId);
//商品url地址
item.setUrl("https://item.jd.com/" skuId ".html");
//创建时间
item.setCreated(new Date());
//修改时间
item.setUpdated(item.getCreated());
//获取商品标题
String itemHtml = this.httpUtils.getHtml(item.getUrl());
String title = Jsoup.parse(itemHtml).select("div.sku-name").text();
item.setTitle(title);
//获取商品价格
String priceUrl = "https://p.3.cn/prices/mgets?skuIds=J_" skuId;
String priceJson = this.httpUtils.getHtml(priceUrl);
// 解析 json 数据获取商品价格
double price = MAPPER.readTree(priceJson).get(0).get("p").asDouble();
item.setPrice(price);
//获取图片地址
if (StringUtils.isNoneBlank(skuEle.attr("data-lazy-img"))) {
// n9 换成 n1 使图片变大
String picUrl = "https:" skuEle.attr("data-lazy-img").replace("/n9/", "/n1/");
System.out.println(picUrl);
// 下载图片
String picName = this.httpUtils.getImage(picUrl);
item.setPic(picName);
}
// 保存商品数据
this.itemService.save(item);
}
}
}
}
引导类
开启定时任务
代码语言:javascript复制@SpringBootApplication
// 开启定时任务
@EnableScheduling
public class CrawlerJdApplication {
public static void main(String[] args) {
SpringApplication.run(CrawlerJdApplication.class, args);
}
}