京东手机数据爬取案例

学习了HttpClient 和l Jsoup,就掌握了如何抓取数据和如何解析数据,接下来,我们做一个小练习,把京东的手机数据抓取下来。
主要目的是 HttpClient和 Jsoup 的学习。

1. 需求分析

首先访问京东,搜索手机,分析页面,我们抓取以下商品数据:商品图片、价格、标题、商品详情页。
image.png


1.1 SPU和 SKU

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

2. 开发准备

2.1 数据库表分析

  1. -- 创建crawler数据库, 再创建表
  2. CREATE TABLE `jd_item` (
  3. `id` bigint(10) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  4. `spu` bigint(15) DEFAULT NULL COMMENT '商品集合id',
  5. `sku` bigint(15) DEFAULT NULL COMMENT '商品最小品类单元id',
  6. `title` varchar(100) DEFAULT NULL COMMENT '商品标题',
  7. `price` bigint(10) DEFAULT NULL COMMENT '商品价格',
  8. `pic` varchar(200) DEFAULT NULL COMMENT '商品图片',
  9. `url` varchar(200) DEFAULT NULL COMMENT '商品详情地址',
  10. `created` datetime DEFAULT NULL COMMENT '创建时间',
  11. `updated` datetime DEFAULT NULL COMMENT '更新时间',
  12. PRIMARY KEY (`id`),
  13. KEY `sku` (`sku`) USING BTREE
  14. ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='京东商品表';


2.2 添加依赖

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <modelVersion>4.0.0</modelVersion>
  6. <parent>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-parent</artifactId>
  9. <version>2.0.2.RELEASE</version>
  10. </parent>
  11. <groupId>cn.itbuild</groupId>
  12. <artifactId>itbuild-crawler-jd</artifactId>
  13. <version>1.0-SNAPSHOT</version>
  14. <dependencies>
  15. <!--SpringMVC-->
  16. <dependency>
  17. <groupId>org.springframework.boot</groupId>
  18. <artifactId>spring-boot-starter-web</artifactId>
  19. </dependency>
  20. <!--SpringData Jpa-->
  21. <dependency>
  22. <groupId>org.springframework.boot</groupId>
  23. <artifactId>spring-boot-starter-data-jpa</artifactId>
  24. </dependency>
  25. <!--MySQL连接包-->
  26. <dependency>
  27. <groupId>mysql</groupId>
  28. <artifactId>mysql-connector-java</artifactId>
  29. <version>8.0.11</version>
  30. </dependency>
  31. <!-- HttpClient -->
  32. <dependency>
  33. <groupId>org.apache.httpcomponents</groupId>
  34. <artifactId>httpclient</artifactId>
  35. </dependency>
  36. <!--Jsoup-->
  37. <dependency>
  38. <groupId>org.jsoup</groupId>
  39. <artifactId>jsoup</artifactId>
  40. <version>1.10.3</version>
  41. </dependency>
  42. <!--工具包-->
  43. <dependency>
  44. <groupId>org.apache.commons</groupId>
  45. <artifactId>commons-lang3</artifactId>
  46. </dependency>
  47. </dependencies>
  48. </project>

2.3 添加配置文件

application.properties

  1. #DB Configuration
  2. spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
  3. spring.datasource.url=jdbc:mysql://127.0.0.1:3306/crawler?useSSL=false&serverTimezone=Asia/Shanghai
  4. spring.datasource.username=root
  5. spring.datasource.password=root
  6. #JPA
  7. spring.jpa.database=MYSQL
  8. spring.jpa.show-sql=true


3. 代码实现

3.1 编写pojo

  1. package cn.itbuild.jd.pojo;
  2. import javax.persistence.*;
  3. import java.util.Date;
  4. /**
  5. * @Date 2020/12/22 16:46
  6. * @Version 10.21
  7. * @Author DuanChaojie
  8. */
  9. @Entity
  10. @Table(name = "jd_item")
  11. public class Item {
  12. //主键
  13. @Id
  14. @GeneratedValue(strategy = GenerationType.IDENTITY)
  15. private Long id;
  16. //标准产品单位(商品集合)
  17. private Long spu;
  18. //库存量单位(最小品类单元)
  19. private Long sku;
  20. //商品标题
  21. private String title;
  22. //商品价格
  23. private Double price;
  24. //商品图片
  25. private String pic;
  26. //商品详情地址
  27. private String url;
  28. //创建时间
  29. private Date created;
  30. //更新时间
  31. private Date updated;
  32. public Long getId() {
  33. return id;
  34. }
  35. public void setId(Long id) {
  36. this.id = id;
  37. }
  38. public Long getSpu() {
  39. return spu;
  40. }
  41. public void setSpu(Long spu) {
  42. this.spu = spu;
  43. }
  44. public Long getSku() {
  45. return sku;
  46. }
  47. public void setSku(Long sku) {
  48. this.sku = sku;
  49. }
  50. public String getTitle() {
  51. return title;
  52. }
  53. public void setTitle(String title) {
  54. this.title = title;
  55. }
  56. public Double getPrice() {
  57. return price;
  58. }
  59. public void setPrice(Double price) {
  60. this.price = price;
  61. }
  62. public String getPic() {
  63. return pic;
  64. }
  65. public void setPic(String pic) {
  66. this.pic = pic;
  67. }
  68. public String getUrl() {
  69. return url;
  70. }
  71. public void setUrl(String url) {
  72. this.url = url;
  73. }
  74. public Date getCreated() {
  75. return created;
  76. }
  77. public void setCreated(Date created) {
  78. this.created = created;
  79. }
  80. public Date getUpdated() {
  81. return updated;
  82. }
  83. public void setUpdated(Date updated) {
  84. this.updated = updated;
  85. }
  86. }


3.2 编写dao

package cn.itbuild.jd.dao;

import cn.itbuild.jd.pojo.Item;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;


@Repository
public interface ItemDao extends JpaRepository<Item, Long> {
}


3.3 编写Service

ItemService
package cn.itbuild.jd.service;

import cn.itbuild.jd.pojo.Item;

import java.util.List;

public interface ItemService {

    /**
     * 保存商品
     * @param item
     */
    public void save(Item item);

    /**
     * 根据条件查询商品
     * @param item
     * @return
     */
    public List<Item> findAll(Item item);
}


ItemServiceImpl

package cn.itbuild.jd.service.impl;

import cn.itbuild.jd.dao.ItemDao;
import cn.itbuild.jd.pojo.Item;
import cn.itbuild.jd.service.ItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @Date 2020/12/22 16:50
 * @Version 10.21
 * @Author DuanChaojie
 */
@Service
public class ItemServiceImpl implements ItemService {

    @Autowired
    private ItemDao itemDao;

    @Override
    public void save(Item item) {
        this.itemDao.save(item);
    }

    @Override
    public List<Item> findAll(Item item) {
        // 1.声明查询条件
        Example<Item> example = Example.of(item);

        // 2.根据查询条件进行查询数据
        List<Item> list = this.itemDao.findAll(example);

        return list;
    }
}


3.4 编写引导类

package cn.itbuild.jd;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling//使用定时任务, 需要先开启定时任务, 需要添加注解
public class JdApplication {
    public static void main(String[] args) {
        SpringApplication.run(JdApplication.class, args);
    }
}


3.5 封装HttpClient

package cn.itbuild.jd.utils;

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 java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.UUID;

@Component
public class HttpUtils {

    private PoolingHttpClientConnectionManager cm;

    public HttpUtils() {
        this.cm = new PoolingHttpClientConnectionManager();
        // 设置最大连接数
        this.cm.setMaxTotal(100);
        // 设置每个主机的最大连接数
        this.cm.setDefaultMaxPerRoute(10);
    }

    /**
     * 根据请求地址下载页面数据
     *
     * @param url
     * @return 页面数据
     */
    public String doGetHtml(String url) {
        // 获取HttpClient对象
        CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(this.cm).build();

        // 创建httpGet请求对象, 设置url地址
        HttpGet httpGet = new HttpGet(url);

        // 设置请求信息
        httpGet.setConfig(getConfig());

        // 设置请求头, 伪装用户
        setHeaders(httpGet);

        CloseableHttpResponse response = null;

        try {
            // 使用HttpClient发起请求, 获取响应
            response = httpClient.execute(httpGet);

            // 解析响应, 返回结果
            if (response.getStatusLine().getStatusCode() == 200) {
                // 判断响应体Entity是否不为空, 如果不为空就可以使用EntityUtils
                if (response.getEntity() != null) {
                    String content = EntityUtils.toString(response.getEntity(), "utf8");
                    return content;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭response
            if (response != null) {
                try {
                    response.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        // 返回空串
        return "";
    }

    /**
     * 下载图片
     * @param url
     * @return 图片名称
     */
    public String doGetImage(String url) {
        // 获取HttpClient对象
        CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(this.cm).build();

        // 创建httpGet请求对象, 设置url地址
        HttpGet httpGet = new HttpGet(url);

        // 设置请求信息
        httpGet.setConfig(getConfig());

        // 设置请求头, 伪装用户
        setHeaders(httpGet);

        CloseableHttpResponse response = null;

        try {
            // 使用HttpClient发起请求, 获取响应
            response = httpClient.execute(httpGet);

            // 解析响应, 返回结果
            if (response.getStatusLine().getStatusCode() == 200) {
                // 判断响应体Entity是否不为空, 如果不为空就可以使用EntityUtils
                if (response.getEntity() != null) {
                    // 下载图片
                    // 获取图片的后缀
                    String extName = url.substring(url.lastIndexOf("."));
                    // 创建图片名, 重命名图片
                    String picName = UUID.randomUUID().toString() + extName;
                    // 下载图片
                    // 声明OutPutStream
                    OutputStream outputStream = new FileOutputStream(new File("E:/file/gitee/crawler/jd-image/" + picName));
                    response.getEntity().writeTo(outputStream);
                    // 返回图片名称
                    return picName;
                }
            }
        } catch (IOException 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 setHeaders(HttpGet httpGet) {
        // 使用HttpClient爬取数据时, 为了防止被网站拦截, 应该设置请求头
        httpGet.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36");
    }
}


3.6 实现数据抓取

package cn.itbuild.jd.task;

import cn.itbuild.jd.pojo.Item;
import cn.itbuild.jd.service.ItemService;
import cn.itbuild.jd.utils.HttpUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
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.io.IOException;
import java.util.Date;
import java.util.List;

/**
 * @Date 2020/12/22 17:05
 * @Version 10.21
 * @Author DuanChaojie
 */
@Component
public class ItemTask {

    @Autowired
    private HttpUtils httpUtils;

    @Autowired
    private ItemService itemService;

    private static final ObjectMapper MAPPER = new ObjectMapper();

    /**
     * 当下载任务完成后,间隔多长时间进行下一次的任务
     * @throws Exception
     */
    @Scheduled(fixedDelay = 10*1000)
    public void itemTask() throws Exception{
        // 生明需要解析的初始地址
        String utl = "https://search.jd.com/search?keyword=%E6%89%8B%E6%9C%BA&wq=%E6%89%8B%E6%9C%BA&ev=559_103811%5E&s=57&click=0&page=";

        // 按照页面对手机的搜索结果进行遍历解析
        for (int i = 19; i < 20; i = i + 2) {
            String html = httpUtils.doGetHtml(utl + i);
            this.parse(html);
        }

        System.out.println("手机数据抓取完成...");
    }

    /**
     * 解析html页面,获取商品数据并存储,核心逻辑
     * @param html
     */
    private void parse(String html) throws IOException {
        // 解析html获取Document对象
        Document doc = Jsoup.parse(html);

        // 获取spuEles信息
        Elements spuEles = doc.select("div#J_goodsList>ul>li");

        // 遍历spuEles
        for (Element spuEle : spuEles) {
            // 排除没有data-spu的值的内容
            if (StringUtils.isNotEmpty(spuEle.attr("data-spu"))) {
                // 获取spu
                long spu = Long.parseLong(spuEle.attr("data-spu"));

                // 获取sku信息
                Elements skuEles = spuEle.select("ul.ps-main>li.ps-item");
                // 根据sku过去商品数据
                for (Element skuEle : skuEles) {
                    // 获取sku
                    long sku = Long.parseLong(skuEle.select("[data-sku]").first().attr("data-sku"));

                    Item item = new Item();

                    item.setSku(sku);
                    List<Item> list = this.itemService.findAll(item);
                    if (list.size() > 0) {
                        // 如果商品存在,就进行下一个循环
                        continue;
                    }
                    // 设置商品的spu
                    item.setSpu(spu);
                    // 获取商品的详情的url
                    String itemUrl = "https://item.jd.com/"+sku+".html";
                    item.setUrl(itemUrl);

                    // 获取商品的图片
                    String picUrl = "https:" + skuEle.select("img[data-sku]").first().attr("data-lazy-img");
                    picUrl = picUrl.replace("/n7/","/n1/");
                    String picName = this.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());

                    // 保存商品数据到数据库中
                    this.itemService.save(item);
                }
            }
        }

    }
}