Java多线程爬虫爬取京东商品信息

2023-05-16

前言

网络爬虫,是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。爬虫可以通过模拟浏览器访问网页,从而获取数据,一般网页里会有很多个URL,爬虫可以访问这些URL到达其他网页,相当于形成了一种数据结构——图,我们通过广度优先搜索和深度优先搜索的方式来遍历这个图,从而做到不断爬取数据的目的。最近准备做一个电商网站,商品的原型就打算从一些电商网站上爬取,这里使用了HttpClient和Jsoup实现了一个简答的爬取商品的demo,采用了多线程的方式,并将爬取的数据持久化到了数据库。

项目环境搭建

整体使用技术

我IDE使用了Spring Tool Suite(sts),你也可以使用Eclipse或者是IDEA,安利使用IDEA,真的好用,谁用谁知道。
整个项目使用Maven进行构建吗,使用Springboot进行自动装配,使用HttpClient对网页进行抓取,Jsoup对网页进行解析,数据库连接池使用Druild,还使用了工具类Guava和Commons.lang3。

项目结构

在sts里面新建一个maven工程,创建如下的包
项目结构.png
- common 一些通用工具类
- constant 系统常量
- dao 数据库访问层
- service 服务层
- handler 调度控制层
- entity 实体层

这样分层的意义是使得项目结构层次清晰,每层都有着其对应的职责,便于扩展和维护

pom文件

这里使用maven进行构建,还没有了解maven的童鞋自行去了解,使用maven的好处是不用自己导入jar包和完整的生命周期控制,注意,使用阿里云的镜像速度回加快很多。项目的pom.xml文件如下
pom.xml

<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>com.exmaple</groupId>
    <artifactId>spider-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spider-demo</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <jsoup.version>1.10.3</jsoup.version>
        <guava.version>22.0</guava.version>
        <lang3.version>3.6</lang3.version>
        <mysql.version>5.1.42</mysql.version>
        <druid.version>1.1.0</druid.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.4.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- httpclient -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <!-- jsoup -->
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>${jsoup.version}</version>
        </dependency>
        <!-- guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>
        <!-- commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${lang3.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.34</version>
        </dependency>

    </dependencies>

    <build>
        <finalName>spider-demo</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

application.yml文件

spring boot的配置文件有两种形式,放在src/main/resources目录下,分别是application.ymlapplication.properties
这里为了配置更加简洁,使用了application.yml作为我们的配置文件
application.yml

# mysql
spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/spider?useUnicode=true&characterEncoding=UTF-8&&useSSL=true
        username: root
        password: 123

这里可以在url,username和pssword里换成自己环境对应的配置

sql文件

这里我们创建了一个数据库和一张表,以便后面将商品信息持久化到数据库
db.sql

USE spider;
CREATE TABLE `goods_info` (
  `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `goods_id` VARCHAR(255) NOT NULL COMMENT '商品ID',
  `goods_name` VARCHAR(255) NOT NULL COMMENT '商品名称',
  `img_url` VARCHAR(255) NOT NULL COMMENT '商品图片地址',
  `goods_price` VARCHAR(255) NOT NULL COMMENT '商品标价',
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='商品信息表';

网页的分析

网址URL的分析

我们要爬取的网页的URL的基本地址是https://search.jd.com/Search
我们打开这个网页,在搜索框内搜索零食,我们看一下我们的浏览器的地址栏的URL的变化,发现浏览器的地址栏变成了https://search.jd.com/Search?keyword=零食&enc=utf-8&wq=零食&pvid=2c636c9dc26c4e6e88e0dea0357b81a3
我们就可以对参数进行分析,keywordwq应该是代表要搜索的关键字,enc代表的编码,pvid不知道是什么,我们把这个参数去掉看能不能访问https://search.jd.com/Search?keyword=零食&enc=utf-8&wq=零食,发现这个URL也是可以正常访问到这个网址的,那么我们就可以暂时忽略这个参数,参数就设置就设置keyword,wqenc
这里我们要设置的参数就是
- keyword 零食
- wq 零食
- enc utf-8

网页内容的分析

我们打开我们要爬取数据的页面
商品.png
使用浏览器-检查元素
商品源代码.png
通过查看源码,我们发现JD的商品列表放在id是J_goodsList的div下的的class是gl-warp clearfix的ul标签下的class是gl-item的li标签下
再分别审查各个元素,我们发现
- li标签的data-sku的属性值就是商品的ID
- li标签下的class为p-name p-name-type-2的em的值就是商品的名称
- li标签下的class为p-price的strong标签下的i标签的值是商品的价格
- li标签下的class为p-img的img标签的src值就是商品的图片URL

对网页进行了分析以后,我们就可以通过对DOM结点的选择来筛选我们想要的数据了

代码的编写

这里我们封装了HttpClientUtils作为我们的工具类,以便以后使用

HttpClientUtils工具类

HttpClient.java

package com.exmaple.spider.common;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.exmaple.spider.constant.SysConstant;

/**
 * HttpClient工具类
 * 
 * @author ZGJ
 * @date 2017年7月14日
 */
public class HttpClientUtils {

    private final static Logger logger = LoggerFactory.getLogger(HttpClientUtils.class);

    private final static String GET_METHOD = "GET";
    private final static String POST_METHOD = "POST";

    /**
     * GET请求
     * 
     * @param url
     *            请求url
     * @param headers
     *            头部
     * @param params
     *            参数
     * @return
     */
    public static String sendGet(String url, Map<String, String> headers, Map<String, String> params) {
        // 创建HttpClient对象
        CloseableHttpClient client = HttpClients.createDefault();
        StringBuilder reqUrl = new StringBuilder(url);
        String result = "";
        /*
         * 设置param参数
         */
        if (params != null && params.size() > 0) {
            reqUrl.append("?");
            for (Entry<String, String> param : params.entrySet()) {
                reqUrl.append(param.getKey() + "=" + param.getValue() + "&");
            }
            url = reqUrl.subSequence(0, reqUrl.length() - 1).toString();
        }
        logger.debug("[url:" + url + ",method:" + GET_METHOD + "]");
        HttpGet httpGet = new HttpGet(url);
        /**
         * 设置头部
         */
        logger.debug("Header\n");
        if (headers != null && headers.size() > 0) {
            for (Entry<String, String> header : headers.entrySet()) {
                httpGet.addHeader(header.getKey(), header.getValue());
                logger.debug(header.getKey() + " : " + header.getValue());
            }
        }
        CloseableHttpResponse response = null;
        try {
            response = client.execute(httpGet);
            /**
             * 请求成功
             */
            if (response.getStatusLine().getStatusCode() == 200) {
                HttpEntity entity = response.getEntity();
                result = EntityUtils.toString(entity, SysConstant.DEFAULT_CHARSET);
            }
        } catch (IOException e) {
            logger.error("网络请求出错,请检查原因");
        } finally {
            // 关闭资源
            try {
                if (response != null) {
                    response.close();
                }
                client.close();
            } catch (IOException e) {
                logger.error("网络关闭错误错,请检查原因");
            }
        }
        return result;
    }

    /**
     * POST请求
     * 
     * @param url
     *            请求url
     * @param headers
     *            头部
     * @param params
     *            参数
     * @return
     */
    public static String sendPost(String url, Map<String, String> headers, Map<String, String> params) {
        CloseableHttpClient client = HttpClients.createDefault();
        String result = "";
        HttpPost httpPost = new HttpPost(url);
        /**
         * 设置参数
         */
        if (params != null && params.size() > 0) {
            List<NameValuePair> paramList = new ArrayList<>();
            for (Entry<String, String> param : params.entrySet()) {
                paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
            }
            logger.debug("[url: " + url + ",method: " + POST_METHOD + "]");
            // 模拟表单提交
            try {
                UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList, SysConstant.DEFAULT_CHARSET);
                httpPost.setEntity(entity);
            } catch (UnsupportedEncodingException e) {
                logger.error("不支持的编码");
            }
            /**
             * 设置头部
             */
            if (headers != null && headers.size() > 0) {
                logger.debug("Header\n");
                if (headers != null && headers.size() > 0) {
                    for (Entry<String, String> header : headers.entrySet()) {
                        httpPost.addHeader(header.getKey(), header.getValue());
                        logger.debug(header.getKey() + " : " + header.getValue());
                    }
                }
            }
            CloseableHttpResponse response = null;
            try {
                response = client.execute(httpPost);
                HttpEntity entity = response.getEntity();
                result = EntityUtils.toString(entity, SysConstant.DEFAULT_CHARSET);
            } catch (IOException e) {
                logger.error("网络请求出错,请检查原因");
            } finally {
                try {
                    if (response != null) {
                        response.close();
                    }
                    client.close();
                } catch (IOException e) {
                    logger.error("网络关闭错误");
                }
            }
        }
        return result;
    }
    /**
     * post请求发送json
     * @param url
     * @param json
     * @param headers
     * @return
     */
    public static String senPostJson(String url, String json, Map<String, String> headers) {
        CloseableHttpClient client = HttpClients.createDefault();
        String result = "";
        HttpPost httpPost = new HttpPost(url);
        StringEntity stringEntity = new StringEntity(json, ContentType.APPLICATION_JSON);
        httpPost.setEntity(stringEntity);
        logger.debug("[url: " + url + ",method: " + POST_METHOD + ", json: " + json + "]");
        /**
         * 设置头部
         */
        if (headers != null && headers.size() > 0) {
            logger.debug("Header\n");
            if (headers != null && headers.size() > 0) {
                for (Entry<String, String> header : headers.entrySet()) {
                    httpPost.addHeader(header.getKey(), header.getValue());
                    logger.debug(header.getKey() + " : " + header.getValue());
                }
            }
        }
        CloseableHttpResponse response = null;
        try {
            response = client.execute(httpPost);
            HttpEntity entity = response.getEntity();
            result = EntityUtils.toString(entity, SysConstant.DEFAULT_CHARSET);
        } catch (IOException e) {
            logger.error("网络请求出错,请检查原因");
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
                client.close();
            } catch (IOException e) {
                logger.error("网络关闭错误");
            }
        }
        return result;
    }
}

SyConstant.java 系统常量

SysConstant.java

package com.exmaple.spider.constant;
/**
 * 系统全局常量
 * @author ZGJ
 * @date 2017年7月15日
 */
public interface SysConstant {
    /**
     * 系统默认字符集
     */
    String DEFAULT_CHARSET = "utf-8";
    /**
     * 需要爬取的网站
     */
    String BASE_URL = "https://search.jd.com/Search";

    interface Header {
        String ACCEPT = "Accept";
        String ACCEPT_ENCODING = "Accept-Encoding";
        String ACCEPT_LANGUAGE = "Accept-Language";
        String CACHE_CONTROL = "Cache-Controle";
        String COOKIE = "Cookie";
        String HOST = "Host";
        String PROXY_CONNECTION = "Proxy-Connection";
        String REFERER = "Referer";
        String USER_AGENT = "User-Agent";
    }
    /**
     * 默认日期格式
     */
    String DEFAULT_DATE_FORMAT = "yyy-MM-dd HH:mm:ss";
}

GoodsInfo 商品信息

GoodsInfo.java

package com.exmaple.spider.entity;

public class GoodsInfo {
    private Integer id;

    private String goodsId;

    private String goodsName;

    private String imgUrl;

    private String goodsPrice;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getGoodsId() {
        return goodsId;
    }

    public void setGoodsId(String goodsId) {
        this.goodsId = goodsId;
    }

    public String getGoodsName() {
        return goodsName;
    }

    public void setGoodsName(String goodsName) {
        this.goodsName = goodsName;
    }

    public String getImgUrl() {
        return imgUrl;
    }

    public void setImgUrl(String imgUrl) {
        this.imgUrl = imgUrl;
    }

    public String getGoodsPrice() {
        return goodsPrice;
    }

    public void setGoodsPrice(String goodsPrice) {
        this.goodsPrice = goodsPrice;
    }

    public GoodsInfo(String goodsId, String goodsName, String imgUrl, String goodsPrice) {
        super();
        this.goodsId = goodsId;
        this.goodsName = goodsName;
        this.imgUrl = imgUrl;
        this.goodsPrice = goodsPrice;
    }

}

GoodsInfoDao 商品信息Dao层

因为这里仅仅涉及到把商品信息写入到数据库比较简单的操作,并没有使用MyBatis或者Hibernate框架,只是使用了Spring的JdbcTemplate对数据进行插入操作
GoodsInfoDao.java

package com.exmaple.spider.dao;

import java.util.List;

import com.exmaple.spider.entity.GoodsInfo;

/**
 * 商品Dao层
 * @author ZGJ
 * @date 2017年7月15日
 */
public interface GoodsInfoDao {
    /**
     * 插入商品信息
     * @param infos
     */
    void saveBatch(List<GoodsInfo> infos);
}

GoodsInfoDaoImpl.java

package com.exmaple.spider.dao.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.exmaple.spider.dao.GoodsInfoDao;
import com.exmaple.spider.entity.GoodsInfo;

@Repository
public class GoodsInfoDaoImpl implements GoodsInfoDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public void saveBatch(List<GoodsInfo> infos) {
        String sql = "REPLACE INTO goods_info(" + "goods_id," + "goods_name," + "goods_price," + "img_url) "
                + "VALUES(?,?,?,?)";
        for(GoodsInfo info : infos) {
            jdbcTemplate.update(sql, info.getGoodsId(), info.getGoodsName(), info.getGoodsPrice(), info.getImgUrl());
        }
    }
}

商品的Dao层实现了向数据库里插入商品信息,使用JdbcTemplate和占位符的方式设置sql语句

SpiderService 爬虫服务层

SpiderService.java

package com.exmaple.spider.service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.alibaba.fastjson.JSON;
import com.exmaple.spider.common.HttpClientUtils;
import com.exmaple.spider.constant.SysConstant;
import com.exmaple.spider.dao.GoodsInfoDao;
import com.exmaple.spider.entity.GoodsInfo;
import com.google.common.collect.Lists;

@Service
public class SpiderService {
    private static Logger logger = LoggerFactory.getLogger(SpiderService.class);
    @Autowired
    private GoodsInfoDao goodsInfoDao;
    private static String HTTPS_PROTOCOL = "https:";

    public void spiderData(String url, Map<String, String> params) {
        String html = HttpClientUtils.sendGet(url, null, params);
        if(!StringUtils.isBlank(html)) {
            List<GoodsInfo> goodsInfos =parseHtml(html);
            goodsInfoDao.saveBatch(goodsInfos);
        }
    }
    /**
     * 解析html
     * @param html
     */
    private List<GoodsInfo> parseHtml(String html) {
        //商品集合
        List<GoodsInfo> goods = Lists.newArrayList();
        /**
         * 获取dom并解析
         */
        Document document = Jsoup.parse(html);
        Elements elements = document.
                select("ul[class=gl-warp clearfix]").select("li[class=gl-item]");
        int index = 0;
        for(Element element : elements) {
            String goodsId = element.attr("data-sku");
            String goodsName = element.select("div[class=p-name p-name-type-2]").select("em").text();
            String goodsPrice = element.select("div[class=p-price]").select("strong").select("i").text();
            String imgUrl = HTTPS_PROTOCOL + element.select("div[class=p-img]").select("a").select("img").attr("src");
            GoodsInfo goodsInfo = new GoodsInfo(goodsId, goodsName, imgUrl, goodsPrice);
            goods.add(goodsInfo);
            String jsonStr = JSON.toJSONString(goodsInfo);
            logger.info("成功爬取【" + goodsName + "】的基本信息 ");
            logger.info(jsonStr);
            if(index ++ == 9) {
                break;
            }
        }
        return goods;
    }
}

Service层通过使用HttpClientUtils模拟浏览器访问页面,然后再使用Jsoup对页面进行解析,Jsoup的使用和Jquery的DOM结点选取基本相似,可以看作是java版的Jquery,如果写过Jquery的人基本上就可以看出是什么意思。
每抓取一条信息就会打印一次记录,而且使用fastjson将对象转换成json字符串并输出
在写测试代码的时候发现,发现爬取的数据只有前10条是完整的,后面的爬取的有些是不完整的,按道理来说是对于整个页面都是通用的,就是不知道为什么只有前面才是完整的,排查了很久没用发现原因,这里就只选择了前面的10条作为要爬取的数据
我们了解到,我们要爬取数据前要分析我们要爬取的数据有哪些,再分析网友的结构,然后对网页进行解析,选取对应的DOM或者使用正则表达式筛选,思路首先要清晰,有了思路之后剩下的也只是把你的思路翻译成代码而已了。

SpiderHandler 爬虫调度处理器

SpiderHandler.java

package com.exmaple.spider.handler;

import java.util.Date;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.commons.lang3.time.FastDateFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.exmaple.spider.constant.SysConstant;
import com.exmaple.spider.service.SpiderService;
import com.google.common.collect.Maps;
/**
 * 爬虫调度处理器
 * @author ZGJ
 * @date 2017年7月15日
 */
@Component
public class SpiderHandler {
    @Autowired
    private SpiderService spiderService;

    private static final Logger logger = LoggerFactory.getLogger(SpiderHandler.class);

    public void spiderData() {
        logger.info("爬虫开始....");
        Date startDate = new Date();
        // 使用现线程池提交任务
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        //引入countDownLatch进行线程同步,使主线程等待线程池的所有任务结束,便于计时
        CountDownLatch countDownLatch = new CountDownLatch(100);
        for(int i = 1; i < 201; i += 2) {
            Map<String, String> params = Maps.newHashMap();
            params.put("keyword", "零食");
            params.put("enc", "utf-8");
            params.put("wc", "零食");
            params.put("page", i + "");
            executorService.submit(() -> {
                spiderService.spiderData(SysConstant.BASE_URL, params);
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executorService.shutdown();
        Date endDate = new Date();

        FastDateFormat fdf = FastDateFormat.getInstance(SysConstant.DEFAULT_DATE_FORMAT);
        logger.info("爬虫结束....");
        logger.info("[开始时间:" + fdf.format(startDate) + ",结束时间:" + fdf.format(endDate) + ",耗时:"
                + (endDate.getTime() - startDate.getTime()) + "ms]");

    }
}

SpiderHandelr作为一个爬虫服务调度处理器,这里采用了ExecutorService线程池创建了5个线程进行多线程爬取,我们通过翻页发现,翻页过后地址URL多了一个page参数,而且这个参数还只能是奇数才有效,也就是page为1,3,5,7……代表第1,2,3,4……页。这里就只爬了100页,每页10条数据,将page作为不同的参数传给不同的任务。
这里我想统计一下整个爬取任务所用的时间,假如不使用同步工具类的话,因为任务是分到线程池中去运行的,而主线程会继续执行下去,主线程和线程池中的线程是独立运行的,主线程会提前结束,所以就无法统计时间。
这里我们使用CountDownLatch同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。也就是说可以让主线程等待线程池内的线程执行结束再继续执行,里面维护了一个计数器,开始的时候构造计数器的初始数量,每个线程执行结束的时候调用countdown()方法,计数器就减1,调用await()方法,假如计数器不为0就会阻塞,假如计数器为0了就可以继续往下执行

executorService.submit(() -> {
    spiderService.spiderData(SysConstant.BASE_URL, params);
    countDownLatch.countDown();
});

这里使用了Java8中的lambda表达式替代了匿名内部类,详细的可以自行去了解
这里还可以根据自己的业务需求做一些代码的调整和优化,比如实现定时任务爬取等等

App.java Spring Boot启动类

App.java

package com.exmaple.spider;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import com.exmaple.spider.handler.SpiderHandler;

@SpringBootApplication
public class App {
    @Autowired
    private SpiderHandler spiderHandler;

    public static void main(String[] args) throws Exception {
        SpringApplication.run(App.class, args);
    }

    @PostConstruct
    public void task() {
        spiderHandler.spiderData();
    }
}

使用@PostConstruct注解会在spring容器实例化bean之前执行这个方法

运行结果

我们以Spring Boot App的方式运行App.java文件,得到的结果如下:
爬取信息.png
我们在看一下数据库内的信息
数据库记录.png
发现数据库也有信息了,大功告成

总结

写一个简单的爬虫其实也不难,但是其中也有不少的知识点需要梳理和记忆,发现问题或者是错误,查google,查文档,一点点debug去调试,最终把问题一点点的解决,编程其实需要是解决问题的能力,这种的能力的锻炼需要我们去多写代码,写完了代码之后还要多思考,思考为什么要这样写?还有没有更好的实现方式?为什么会出问题?需要怎么解决?这才是一名优秀的程序员应该养成的习惯,共勉!

个人博客: http://blog.zgj12138.cn
简书: http://www.jianshu.com/u/276f89e5b3b1

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java多线程爬虫爬取京东商品信息 的相关文章

  • Android Studio报错:Error:Could not find com.android.tools.build:gradle:4.1 记一次不长记性的坑

    本文地址 xff1a https blog csdn net zengsidou article details 79797417 看字面意思 xff0c 这个问题是Gradle没有对应版本 在搜索引擎没有找到方法之后 xff0c 尝试自己
  • VBox关闭dhcp

    VBox关闭dhcp C Program Files Oracle VirtualBox gt VBoxManage exe list dhcpservers NetworkName HostInterfaceNetworking Virt
  • Android 使用LottieAnimationView 做启动动画

    lt xml version 61 34 1 0 34 encoding 61 34 utf 8 34 gt lt RelativeLayout xmlns android 61 34 http schemas android com ap
  • Android OkHttp★

    1 OkHttp OkHttp是Square公司开发的一个处理网络请求的开源项目 是目前Android使用最广泛的网络框架 OkHttp的特点 支持HTTP 2并允许对同一主机的所有请求共享一个socket连接 如果非HTTP 2 则通过连
  • Android GestureDetector★★★

    1 GestureDetecor 用户触摸屏幕时会产生许多手势 xff0c 一般通过重写View类的onTouch 方法可以处理一些触摸事件 xff0c 但是这个方法太过简单 xff0c 如果需要处理一些复杂的手势 xff0c 用这个接口就
  • Android canvas

    1 Canvas Canvas指画布 xff0c 表现在屏幕上就是一块区域 xff0c 可以在上面使用各种API绘制想要的东西 canvas内部维持了一个mutable Bitmap xff0c 所以它可以使用颜色值去填充整个Bitmap
  • Android apk打包流程★

    1 apk打包 Android开发中打包apk主要有两种方式 使用Android Studio集成直接生成apk 使用ant工具在命令行方式下打包apk 不管哪种方式 打包apk的本质过程都是一样的 Android的apk包文件包括两部分
  • Android ViewPager用法

    1 适配器PagerAdapter ViewPager使用适配器类将数据和view的处理分离 xff0c ViewPager的适配器叫PagerAdapter xff0c 这是一个抽象类 xff0c 不能实例化 xff0c 所以它有两个子类
  • Android Fragment★★

    1 Fragment fragment译为 碎片 xff0c 是Android 3 0 xff08 API 11 xff09 提出的 xff0c 最开始是为了适配大屏的平板 Fragment看起来和Activity一样 xff0c 是一个用
  • Android设计模式—适配器模式★★★

    1 适配器模式 适配器模式是指把一个类的接口变换成客户端所期待的另一种接口 xff0c 从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作 适配器模式是为了解决接口不兼容问题的 比如厂商给你的接口和你现有的接口对接不起来 旧的数据
  • Android 类加载机制

    nbsp 1 类加载机制 java文件不是可执行的文件 需要先编译成 class文件才可以被虚拟机执行 而类加载就是指通过类加载器把 class文件加载到虚拟机的内存空间 具体来说是方法区 类通常是按需加载 即第一次使用该类时才加载 Jav
  • Android Bitmap防止内存溢出

    1 Bitmap 在Android开发中经常会使用到Bitmap xff0c 而Bitmap使用不当很容易引发OOM Bitmap占用内存大小的计算公式为 xff1a 图片宽度 图片高度 一个像素点所占字节数 xff0c 因此减小这三个参数
  • Swift NSAttributedString的使用

    NSMutableAttributedString let testAttributes 61 NSAttributedStringKey foregroundColor UIColor blue NSAttributedStringKey
  • Android ViewStub

    1 ViewStub ViewStub是一个可用于性能优化的控件 xff0c 它是一个不可见的 零尺寸的View xff0c 可以在运行时进行延迟加载一个布局文件 xff0c 从而提高显示速率 viewstub和include比较像 xff
  • Android Jetpack—LiveData和数据倒灌

    1 LiveData LiveData是Android Jetpack包提供的一种可观察的数据存储器类 xff0c 它可以通过添加观察者被其他组件观察其变更 不同于普通的观察者 xff0c LiveData最重要的特征是它具有生命周期感知能
  • Gradle build 报错:Received status code 400 from server: Bad Request

    全部错误是这样的 xff1a Could not GET 39 https dl google com dl android maven2 com android tools build gradle 3 1 2 gradle 3 1 2
  • 排列组合详解

    在笔试题中看到的一个选择题 用1 3的瓷砖密铺3 20的地板有几种方式 xff1f 排列组合问题 排列和组合问题 xff0c 其实是两种问题 xff0c 区分它们的原则是是否需要考虑顺序的不同 排列问题 xff0c 考虑顺序 xff1b 组
  • SCKKRS-关键词、关键短语提取

    1 简介 SCKKRS Self supervised Contextual Keyword and Keyphrase Retrieval with Self Labelling 本文根据2019年 Self supervised Con
  • kali安装vnc

    一 安装x11vnc 1 经过N多次的实验 xff0c kali一直报错 xff0c tightvncserver一直报错 怎么配置都是黑屏 xff0c 奔溃 最后退而求其次 xff0c 安装x11vnc 2 很简单的命令 sudo apt
  • 计算机硬件技术基础第一章总结

    1 1 计算机发展概述 1 1 1 计算机的发展简史 第一台计算机 xff1a ENIAC 第一代 xff1a 电子管数字计算机 xff08 1946 1958 xff09 逻辑元件 xff1a 真空电子管体积大 xff0c 功耗高 xff

随机推荐

  • CentOS7安装Oracle JDK1.8

    JDK1 8下载地址 https www oracle com java technologies javase javase8 archive downloads html 需要登录之后才能下载文件 xff0c 下载jdk 8u202 l
  • Ubuntu 16.04 安装 rtl8812au系列 (DWA-182) wireless adapter driver

    Ubuntu 16 04 安装 rtl8812au系列 DWA 182 wireless adapter driver 刚刚开始使用Linux xff0c 一脸懵逼 xff0c 命令行搞得一愣一愣的 xff0c 不过熟悉了之后就好很多了 一
  • SpringBoot项目启动失败报错Annotation-specified bean name ‘xx‘ for bean class [xxx] conflicts with existing

    问题描述 xff1a 项目启动就会报 xff1a Annotation specified bean name xx for bean class xxx conflicts with existing non compatible bea
  • Visual Studio高效实用的扩展工具、插件

    说明 xff1a 对一个有想法的程序员来说 xff0c 善于使用一款高效的开发工具是很重要的 xff0c 今天给大家介绍的是宇宙第一IDE vs用起来很不错的开发工具 xff0c 假如大家觉得不错也可以尝试的用用 xff0c 毕竟对于我们这
  • java琐事

    并发编程 并发的意义 并发通常是提高运行在单处理器上的程序的性能 如果程序中的某个任务因为该程序控制范围之外的某些条件 I O 而导致不能继续执行 xff0c 那么这个任务或线程就阻塞了 如果没有并发 xff0c 整个程序都讲停下来 从性能
  • java类的初始化和实例化的初始化(类的初始化过程)

    Java类的加载顺序 父类静态代变量 父类静态代码块 子类静态变量 子类静态代码块 父类非静态变量 xff08 父类实例成员变量 xff09 父类构造函数 子类非静态变量 xff08 子类实例成员变量 xff09 子类构造函数 上面的说法也
  • 最优吞吐量和最短停顿时间

    在实践活动中 xff0c 我们通过最优吞吐量和最短停顿时间来评价jvm系统的性能 吞吐量越高算法越好 暂停时间越短算法越好 首先让我们来明确垃圾收集 GC 中的两个术语 吞吐量 throughput 和暂停时间 pause times JV
  • sql执行慢的原因有哪些,如何进行sql优化?

    一 导致SQL执行慢的原因 1 硬件问题 如网络速度慢 xff0c 内存不足 xff0c I O吞吐量小 xff0c 磁盘空间满了等 2 没有索引或者索引失效 xff08 一般在互联网公司 xff0c DBA会在半夜把表锁了 xff0c 重
  • 阿里java开发手册2019年最新版619(华山版)PDF下载

    链接 https pan baidu com s 1ANvBu1hidnvRCZILDGXuQA 密码 ugq8
  • Mockito:org.mockito.exceptions.misusing.InvalidUseOfMatchersException

    org span class token punctuation span mockito span class token punctuation span exceptions span class token punctuation
  • 一个简单通用的基于java反射实现pojo转为fastjson对象的方法

    最近在公司工作需要实现一个工具实现一个pojo转为fastjson对象的通用工具 xff0c 直接上源码 span class token comment 通用的pojo转为Json对象的方法 64 author ZFX 64 date20
  • Java魔法类:Unsafe应用解析

    这个美团大神对于Unsafe的分析很全面 https tech meituan com 2019 02 14 talk about java magic class unsafe html
  • Linux X-Window Error: Can‘t open display: :0

    问题过程描述 许多经常部署Oracle数据的管理员经常需要对数据库软件进行部署 xff0c 但大多数都是通过远程部署的方式进行部署 xff0c 使用远程部署有两种方式 xff0c 一种是通过脚本部署 xff0c 另一种就是通过图形化进行部署
  • maven打包生成war跳过单元测试

    maven将项目打包成war包的命令是 mvn install 或mvn package 每次生成war包时会进行所以的单元测试 xff0c 如果想跳过单元测试直接生成war包有以下3种方式 方法1 xff1a 在pom xml中加入如下代
  • 程序员每天工作多少个小时_程序员每天实际工作几个小时?

    程序员每天工作多少个小时 您如何看待 xff0c 程序员每天实际工作多长时间 xff1f 大多数人会说答案是8到9个小时 有人说他们每天工作12个小时或更长时间 尽管这是正确的 xff0c 但它并不是大多数程序员实际工作的数量 xff0c
  • ubuntu 显示缺少库文件 libcom_err.so.2 解决办法

    运行任何代码都显示 xff1a error while loading shared libraries libcom err so 2 cannot open shared object file No such file or dire
  • 记CVTE第一次面试

    首先说明一下博主是一个大三的学生 xff0c 专业计算机科学与技术 xff0c 主学的方向是Web后台开发 xff0c 主语言是Java 前几天看到CVTE有校园招聘实习生 xff0c 就报名参加了 xff0c 做了CVTE的笔试题 xff
  • Java Socket 编程那些事(1)

    前言 最近在准备面试和笔试的一些东西 xff0c 回去翻看了Java关于IO的基础 xff0c 发现很多基础还是没有记牢固 xff0c 现在回头重新学习 xff0c 就从socket通讯开始吧 xff0c 虽然说现在企业很少直接编写sock
  • Redis集群的原理和搭建

    前言 Redis 是我们目前大规模使用的缓存中间件 xff0c 由于它强大高效而又便捷的功能 xff0c 得到了广泛的使用 单节点的Redis已经就达到了很高的性能 xff0c 为了提高可用性我们可以使用Redis集群 本文参考了Rdis的
  • Java多线程爬虫爬取京东商品信息

    前言 网络爬虫 xff0c 是一种按照一定的规则 xff0c 自动地抓取万维网信息的程序或者脚本 爬虫可以通过模拟浏览器访问网页 xff0c 从而获取数据 xff0c 一般网页里会有很多个URL 爬虫可以访问这些URL到达其他网页 xff0