Java多線程爬蟲爬取京東商品信息

前言

網絡爬蟲,是一種按照一定的規則,自動地抓取萬維網信息的程序或者腳本。爬蟲可以通過模擬瀏覽器訪問網頁,從而獲取數據,一般網頁里會有很多個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
CSDN: http://blog.csdn.net/zgj12138

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容