使用HttpClient和Jsoup爬取京东手机信息案例

使用HttpClient和Jsoup爬取京东手机信息案例

1. 需求分析

  • 首先访问京东,搜索手机,分析页面,我们抓取以下商品数据:商品图片、价格、标题、商品详情页,
    在这里插入图片描述

  • SPU和SKU
    除了这四个属性以外,我们发现中的苹果手机有四种产品,我们应该每一种都要抓取。那么这里就必须要了解spu和sku的概念:

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

    查看页面的源码也可以看出区别
    在这里插入图片描述

2. 开发准备

  • 创建的数据库表,IDEA集成Mysql,可以看这篇文章:

    DROP database IF EXISTS `crawler`;
    create database crawler;
    use crawler;
    DROP database IF EXISTS `jd_item`;
    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和定时任务进行开发,需要创建Maven工程并添加以下依赖

    <?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>cn.itcast</groupId>
        <artifactId>itcast-crawler-jd</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.3.0.RELEASE</version>
        </parent>
    
        <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>
                <version>5.1.6</version>
            </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>
    </project>
    
  • 添加配置文件,加入application.properties配置文件

    #DB Configuration:
    spring.datasource.driverClassName=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://127.0.0.1:3306/crawler
    spring.datasource.username=root
    spring.datasource.password=root
    
    #JPA Configuration:
    spring.jpa.database=MySQL
    spring.jpa.show-sql=true
    

3. 代码实现

  • 3.1 编写pojo
    根据数据库表,编写pojo
    package cn.itcast.jd.pojo;
    
    import javax.persistence.*;
    import java.util.Date;
    
    @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;
        }
    
    }
    
  • 3.2. 编写dao
    package cn.itcast.jd.dao;
    
    import cn.itcast.jd.pojo.Item;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    import java.util.List;
    
    public interface ItemDao extends JpaRepository<Item,Long> {
          
          
    }
    
  • 3.3. 编写Service
    ItemService接口
    package cn.itcast.jd.service;
    
    import cn.itcast.jd.pojo.Item;
    import org.springframework.stereotype.Service;
    
    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.itcast.jd.service.impl;
    
    import cn.itcast.jd.dao.ItemDao;
    import cn.itcast.jd.pojo.Item;
    import cn.itcast.jd.service.ItemService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.domain.Example;
    import org.springframework.stereotype.Service;
    
    import javax.transaction.Transactional;
    import java.util.List;
    
    @Service
    public class ItemServiceImpl implements ItemService {
          
          
    
        @Autowired
        private ItemDao itemDao;
    
        @Override
        @Transactional
        public void save(Item item) {
          
          
            this.itemDao.save(item);
        }
    
        @Override
        public List<Item> findAll(Item item) {
          
          
            //声明查询条件
            Example<Item> example = Example.of(item);
    
            //根据查询调价进行查询数据
            List<Item> list = this.itemDao.findAll(example);
    
            return list;
        }
    }
    
  • 3.4. 编写引导类
    package cn.itcast.jd;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.scheduling.annotation.EnableScheduling;
    
    @SpringBootApplication
    //设置开启定时任务
    @EnableScheduling
    public class Application {
          
          
    
        public static void main(String[] args) {
          
          
            SpringApplication.run(Application.class,args);
        }
    }
    
    
  • 3.5. 封装HttpClient
    需要经常使用HttpClient,所以需要进行封装,方便使用
    package cn.itcast.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.util.UUID;
    
    @Component
    public class HttpUtils {
          
          
    
        private PoolingHttpClientConnectionManager cm;
    
        public HttpUtils() {
          
          
            this.cm = new PoolingHttpClientConnectionManager();
    
            //设置最大连接数
            cm.setMaxTotal(200);
            //设置每个主机的并发数
            cm.setDefaultMaxPerRoute(20);
        }
    
        /**
         * 根据请求地址下载页面数据
         *
         * @param url
         * @return 页面数据
         */
        public String doGetHtml(String url) {
          
          
            //获取HttpClient对象
            CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(this.cm).build();
    
            //声明httpGet请求对象
            HttpGet httpGet = new HttpGet(url);
            //设置请求参数RequestConfig
            httpGet.setConfig(this.getConfig());
    
            //设置请求Request Headers中的User-Agent,告诉京东说这是浏览器访问
           httpGet.addHeader("User-Agent","Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Mobile Safari/537.36");
    
            CloseableHttpResponse response=null;
            try {
          
          
                //使用HttpClient发起请求,返回response
                response = httpClient.execute(httpGet);
                //解析response返回数据
                if (response.getStatusLine().getStatusCode() == 200) {
          
          
                    String html = "";
                    //如果response.getEntity获取的结果为空,在执行EntityUtils.toString会报错
                    //需要对Entity进行非空的判断
                    if (response.getEntity() != null) {
          
          
                        html = EntityUtils.toString(response.getEntity(), "utf8");
                    }
                    return html;
                }
            } catch (Exception e) {
          
          
                e.printStackTrace();
            } finally {
          
          
                try {
          
          
                    if (response != null) {
          
          
                        response.close();
                    }
                    // 不能关闭,现在使用的是连接管理器
                    // httpClient.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请求对象
            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() == 200) {
          
          
                    //图片后缀
                    String extName = url.substring(url.lastIndexOf("."));
                    //重命名
                    String imageName = UUID.randomUUID().toString() + extName;
                    //声明输出的文件
                    FileOutputStream outputStream = new FileOutputStream(new File("D:\\images\\" + imageName));
                    //下载图片
                    response.getEntity().writeTo(outputStream);
                    //返回生成的图片名
                    return imageName;
                }
            } catch (Exception e) {
          
          
                e.printStackTrace();
            } finally {
          
          
                try {
          
          
                    if (response != null) {
          
          
                        response.close();
                    }
                    // 不能关闭,现在使用的是连接管理器
                    // httpClient.close();
                } catch (IOException e) {
          
          
                    e.printStackTrace();
                }
            }
            //下载失败,返回空串
            return "";
        }
    
        //获取请求参数对象
        private RequestConfig getConfig() {
          
          
            RequestConfig config = RequestConfig.custom()
                    .setConnectTimeout(1000)  //创建连接的最长时间
                    .setConnectionRequestTimeout(500) //获取连接的最长时间
                    .setSocketTimeout(100000)  //数据传输的最长时间
                    .build();
            return config;
        }
    }
    
  • 3.6. 实现数据抓取
    使用定时任务,可以定时抓取最新的数据
    package cn.itcast.jd.task;
    
    import cn.itcast.jd.pojo.Item;
    import cn.itcast.jd.service.ItemService;
    import cn.itcast.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.util.Date;
    import java.util.List;
    
    @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=%E6%89%8B%E6%9C%BA&wq=%E6%89%8B%E6%9C%BA&s=120&click=0&page=";
            //按照页面对手机的搜索结果进行遍历解析
            for (int i = 1; i < 5; i = i + 2) {
          
          
                String html = httpUtils.doGetHtml(url + i);
                //解析页面,获取商品数据并存储
                if (html !=null){
          
          
                    this.parse(html);
                }
            }
    
            System.out.println("手机数据抓取完成!");
        }
    
        /**
         *  解析页面,获取商品数据并存储
         * @param html
         * @throws Exception
         */
        private void parse(String html) throws Exception {
          
          
            //解析html获取Document
            Document doc = Jsoup.parse(html);
            //获取spu信息
            Elements spuEles = doc.select("div#J_goodsList > ul > li");
            for (Element spuEle : spuEles) {
          
          
                //获取spu
                String attr = spuEle.attr("data-spu");
                long spu = Long.parseLong(attr.equals("")?"0":attr);
    
                //获取sku信息
                Elements skuEles = spuEle.select("li.ps-item");
                for (Element skuEle : skuEles) {
          
          
                    //获取sku
                    long sku = Long.parseLong(skuEle.select("[data-sku]").attr("data-sku"));
    
                    //根据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");
    
                    //图片路径可能会为空的情况:一下为两种解决方式,第一种会让数据不全,第二种任会报错
                    if(picUrl.equals("https:")){
          
          
                        break;
                    }
                    /*if(!StringUtils.isNotBlank(picUrl)){
                        picUrl ="https:"+ skuEle.select("img[data-sku]").first().attr("data-lazy-img-slave");
                    }*/
    
                    picUrl = picUrl.replace("/n9/","/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);
                }
            }
        }
    }
    
    爬取成功在这里插入图片描述

4. bug分析

  • 爬取页面失败,跳到登录页面的地址,<script>window.location.href='https://passport.jd.com/uc/login'</script>
    解决:设置cookie,进入京东页面,开发者选项,找到请求头如下图,复制到HttpUtils工具类即可,直接复制以下代码也可以:
    在这里插入图片描述

    //设置请求Request Headers中的User-Agent,告诉京东说这是浏览器访问
    httpGet.addHeader("User-Agent","Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Mobile Safari/537.36");
    
  • 报告连接异常,或许是网速太慢了,超出了最长连接时间,将时间延长即可:

    //获取请求参数对象
    private RequestConfig getConfig() {
          
          
        RequestConfig config = RequestConfig.custom()
                .setConnectTimeout(1000)  //创建连接的最长时间
                .setConnectionRequestTimeout(500) //获取连接的最长时间
                .setSocketTimeout(100000)  //数据传输的最长时间
                .build();
        return config;
    }
    
  • 报错: java.lang.IllegalArgumentException
    因为long spu = Long.parseLong(spuEle.attr(“data-spu”));
    直接写这个语句也会报类型转换异常,字符串为空是不知道怎么转换,所以可修改为

    //获取spu
    String attr = spuEle.attr("data-spu");
    long spu = Long.parseLong(attr.equals("")?"0":attr);
    
  • 报错: java.lang.IllegalArgumentException
    是因为图片路径可能会为空的情况

    //图片路径可能会为空的情况:以下为两种解决方式,第一种会让数据不全,第二种任会报错
     if(picUrl.equals("https:")){
          
          
          break;
      }
     if(!StringUtils.isNotBlank(picUrl)){
          
          
         picUrl ="https:"+ skuEle.select("img[data-sku]").first().attr("data-lazy-img-slave");
      }
    

参考: https://blog.csdn.net/hellowork10/article/details/106292150

猜你喜欢

转载自blog.csdn.net/weixin_44505194/article/details/106634835