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

前言

網(wǎng)絡(luò)爬蟲,是一種按照一定的規(guī)則,自動地抓取萬維網(wǎng)信息的程序或者腳本。爬蟲可以通過模擬瀏覽器訪問網(wǎng)頁,從而獲取數(shù)據(jù),一般網(wǎng)頁里會有很多個URL,爬蟲可以訪問這些URL到達(dá)其他網(wǎng)頁,相當(dāng)于形成了一種數(shù)據(jù)結(jié)構(gòu)——圖,我們通過廣度優(yōu)先搜索和深度優(yōu)先搜索的方式來遍歷這個圖,從而做到不斷爬取數(shù)據(jù)的目的。最近準(zhǔn)備做一個電商網(wǎng)站,商品的原型就打算從一些電商網(wǎng)站上爬取,這里使用了HttpClient和Jsoup實現(xiàn)了一個簡答的爬取商品的demo,采用了多線程的方式,并將爬取的數(shù)據(jù)持久化到了數(shù)據(jù)庫。

項目環(huán)境搭建

整體使用技術(shù)

我IDE使用了Spring Tool Suite(sts),你也可以使用Eclipse或者是IDEA,安利使用IDEA,真的好用,誰用誰知道。
整個項目使用Maven進(jìn)行構(gòu)建嗎,使用Springboot進(jìn)行自動裝配,使用HttpClient對網(wǎng)頁進(jìn)行抓取,Jsoup對網(wǎng)頁進(jìn)行解析,數(shù)據(jù)庫連接池使用Druild,還使用了工具類Guava和Commons.lang3。

項目結(jié)構(gòu)

在sts里面新建一個maven工程,創(chuàng)建如下的包


項目結(jié)構(gòu).png
  • common 一些通用工具類
  • constant 系統(tǒng)常量
  • dao 數(shù)據(jù)庫訪問層
  • service 服務(wù)層
  • handler 調(diào)度控制層
  • entity 實體層


    這樣分層的意義是使得項目結(jié)構(gòu)層次清晰,每層都有著其對應(yīng)的職責(zé),便于擴(kuò)展和維護(hù)

pom文件

這里使用maven進(jìn)行構(gòu)建,還沒有了解maven的童鞋自行去了解,使用maven的好處是不用自己導(dǎo)入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里換成自己環(huán)境對應(yīng)的配置

sql文件

這里我們創(chuàng)建了一個數(shù)據(jù)庫和一張表,以便后面將商品信息持久化到數(shù)據(jù)庫
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 '商品標(biāo)價',
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='商品信息表';

網(wǎng)頁的分析

網(wǎng)址URL的分析

我們要爬取的網(wǎng)頁的URL的基本地址是https://search.jd.com/Search
我們打開這個網(wǎng)頁,在搜索框內(nèi)搜索零食,我們看一下我們的瀏覽器的地址欄的URL的變化,發(fā)現(xiàn)瀏覽器的地址欄變成了https://search.jd.com/Search?keyword=零食&enc=utf-8&wq=零食&pvid=2c636c9dc26c4e6e88e0dea0357b81a3
我們就可以對參數(shù)進(jìn)行分析,keywordwq應(yīng)該是代表要搜索的關(guān)鍵字,enc代表的編碼,pvid不知道是什么,我們把這個參數(shù)去掉看能不能訪問https://search.jd.com/Search?keyword=零食&enc=utf-8&wq=零食,發(fā)現(xiàn)這個URL也是可以正常訪問到這個網(wǎng)址的,那么我們就可以暫時忽略這個參數(shù),參數(shù)就設(shè)置就設(shè)置keyword,wqenc
這里我們要設(shè)置的參數(shù)就是

  • keyword 零食
  • wq 零食
  • enc utf-8

網(wǎng)頁內(nèi)容的分析

我們打開我們要爬取數(shù)據(jù)的頁面


商品.png

使用瀏覽器-檢查元素


商品源代碼.png

通過查看源碼,我們發(fā)現(xiàn)JD的商品列表放在id是J_goodsList的div下的的class是gl-warp clearfix的ul標(biāo)簽下的class是gl-item的li標(biāo)簽下
再分別審查各個元素,我們發(fā)現(xiàn)
  • li標(biāo)簽的data-sku的屬性值就是商品的ID
  • li標(biāo)簽下的class為p-name p-name-type-2的em的值就是商品的名稱
  • li標(biāo)簽下的class為p-price的strong標(biāo)簽下的i標(biāo)簽的值是商品的價格
  • li標(biāo)簽下的class為p-img的img標(biāo)簽的src值就是商品的圖片URL

對網(wǎng)頁進(jìn)行了分析以后,我們就可以通過對DOM結(jié)點的選擇來篩選我們想要的數(shù)據(jù)了

代碼的編寫

這里我們封裝了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
     *            參數(shù)
     * @return
     */
    public static String sendGet(String url, Map<String, String> headers, Map<String, String> params) {
        // 創(chuàng)建HttpClient對象
        CloseableHttpClient client = HttpClients.createDefault();
        StringBuilder reqUrl = new StringBuilder(url);
        String result = "";
        /*
         * 設(shè)置param參數(shù)
         */
        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);
        /**
         * 設(shè)置頭部
         */
        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("網(wǎng)絡(luò)請求出錯,請檢查原因");
        } finally {
            // 關(guān)閉資源
            try {
                if (response != null) {
                    response.close();
                }
                client.close();
            } catch (IOException e) {
                logger.error("網(wǎng)絡(luò)關(guān)閉錯誤錯,請檢查原因");
            }
        }
        return result;
    }

    /**
     * POST請求
     * 
     * @param url
     *            請求url
     * @param headers
     *            頭部
     * @param params
     *            參數(shù)
     * @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);
        /**
         * 設(shè)置參數(shù)
         */
        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("不支持的編碼");
            }
            /**
             * 設(shè)置頭部
             */
            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("網(wǎng)絡(luò)請求出錯,請檢查原因");
            } finally {
                try {
                    if (response != null) {
                        response.close();
                    }
                    client.close();
                } catch (IOException e) {
                    logger.error("網(wǎng)絡(luò)關(guān)閉錯誤");
                }
            }
        }
        return result;
    }
    /**
     * post請求發(fā)送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 + "]");
        /**
         * 設(shè)置頭部
         */
        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("網(wǎng)絡(luò)請求出錯,請檢查原因");
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
                client.close();
            } catch (IOException e) {
                logger.error("網(wǎng)絡(luò)關(guān)閉錯誤");
            }
        }
        return result;
    }
}

SyConstant.java 系統(tǒng)常量

SysConstant.java

package com.exmaple.spider.constant;
/**
 * 系統(tǒng)全局常量
 * @author ZGJ
 * @date 2017年7月15日
 */
public interface SysConstant {
    /**
     * 系統(tǒng)默認(rèn)字符集
     */
    String DEFAULT_CHARSET = "utf-8";
    /**
     * 需要爬取的網(wǎng)站
     */
    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";
    }
    /**
     * 默認(rèn)日期格式
     */
    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層

因為這里僅僅涉及到把商品信息寫入到數(shù)據(jù)庫比較簡單的操作,并沒有使用MyBatis或者Hibernate框架,只是使用了Spring的JdbcTemplate對數(shù)據(jù)進(jìn)行插入操作
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層實現(xiàn)了向數(shù)據(jù)庫里插入商品信息,使用JdbcTemplate和占位符的方式設(shè)置sql語句

SpiderService 爬蟲服務(wù)層

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對頁面進(jìn)行解析,Jsoup的使用和Jquery的DOM結(jié)點選取基本相似,可以看作是java版的Jquery,如果寫過Jquery的人基本上就可以看出是什么意思。
每抓取一條信息就會打印一次記錄,而且使用fastjson將對象轉(zhuǎn)換成json字符串并輸出
在寫測試代碼的時候發(fā)現(xiàn),發(fā)現(xiàn)爬取的數(shù)據(jù)只有前10條是完整的,后面的爬取的有些是不完整的,按道理來說是對于整個頁面都是通用的,就是不知道為什么只有前面才是完整的,排查了很久沒用發(fā)現(xiàn)原因,這里就只選擇了前面的10條作為要爬取的數(shù)據(jù)
我們了解到,我們要爬取數(shù)據(jù)前要分析我們要爬取的數(shù)據(jù)有哪些,再分析網(wǎng)友的結(jié)構(gòu),然后對網(wǎng)頁進(jìn)行解析,選取對應(yīng)的DOM或者使用正則表達(dá)式篩選,思路首先要清晰,有了思路之后剩下的也只是把你的思路翻譯成代碼而已了。

SpiderHandler 爬蟲調(diào)度處理器

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;
/**
 * 爬蟲調(diào)度處理器
 * @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();
        // 使用現(xiàn)線程池提交任務(wù)
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        //引入countDownLatch進(jìn)行線程同步,使主線程等待線程池的所有任務(wù)結(jié)束,便于計時
        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("爬蟲結(jié)束....");
        logger.info("[開始時間:" + fdf.format(startDate) + ",結(jié)束時間:" + fdf.format(endDate) + ",耗時:"
                + (endDate.getTime() - startDate.getTime()) + "ms]");

    }
}

SpiderHandelr作為一個爬蟲服務(wù)調(diào)度處理器,這里采用了ExecutorService線程池創(chuàng)建了5個線程進(jìn)行多線程爬取,我們通過翻頁發(fā)現(xiàn),翻頁過后地址URL多了一個page參數(shù),而且這個參數(shù)還只能是奇數(shù)才有效,也就是page為1,3,5,7……代表第1,2,3,4……頁。這里就只爬了100頁,每頁10條數(shù)據(jù),將page作為不同的參數(shù)傳給不同的任務(wù)。
這里我想統(tǒng)計一下整個爬取任務(wù)所用的時間,假如不使用同步工具類的話,因為任務(wù)是分到線程池中去運(yùn)行的,而主線程會繼續(xù)執(zhí)行下去,主線程和線程池中的線程是獨(dú)立運(yùn)行的,主線程會提前結(jié)束,所以就無法統(tǒng)計時間。
這里我們使用CountDownLatch同步工具類,它允許一個或多個線程一直等待,直到其他線程的操作執(zhí)行完后再執(zhí)行。也就是說可以讓主線程等待線程池內(nèi)的線程執(zhí)行結(jié)束再繼續(xù)執(zhí)行,里面維護(hù)了一個計數(shù)器,開始的時候構(gòu)造計數(shù)器的初始數(shù)量,每個線程執(zhí)行結(jié)束的時候調(diào)用countdown()方法,計數(shù)器就減1,調(diào)用await()方法,假如計數(shù)器不為0就會阻塞,假如計數(shù)器為0了就可以繼續(xù)往下執(zhí)行

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

這里使用了Java8中的lambda表達(dá)式替代了匿名內(nèi)部類,詳細(xì)的可以自行去了解
這里還可以根據(jù)自己的業(yè)務(wù)需求做一些代碼的調(diào)整和優(yōu)化,比如實現(xiàn)定時任務(wù)爬取等等

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之前執(zhí)行這個方法

運(yùn)行結(jié)果

我們以Spring Boot App的方式運(yùn)行App.java文件,得到的結(jié)果如下:


爬取信息.png

我們在看一下數(shù)據(jù)庫內(nèi)的信息


數(shù)據(jù)庫記錄.png

發(fā)現(xiàn)數(shù)據(jù)庫也有信息了,大功告成

總結(jié)

寫一個簡單的爬蟲其實也不難,但是其中也有不少的知識點需要梳理和記憶,發(fā)現(xiàn)問題或者是錯誤,查google,查文檔,一點點debug去調(diào)試,最終把問題一點點的解決,編程其實需要是解決問題的能力,這種的能力的鍛煉需要我們?nèi)ザ鄬懘a,寫完了代碼之后還要多思考,思考為什么要這樣寫?還有沒有更好的實現(xiàn)方式?為什么會出問題?需要怎么解決?這才是一名優(yōu)秀的程序員應(yīng)該養(yǎng)成的習(xí)慣,共勉!

個人博客: http://blog.zgj12138.cn
CSDN: http://blog.csdn.net/zgj12138

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容