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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

這裏可以在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='商品信息表';
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

網頁的分析

網址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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212

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";
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

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;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

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());
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

商品的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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

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]");

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

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();
});
  • 1
  • 2
  • 3
  • 4

這裏使用了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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

使用@PostConstruct註解會在spring容器實例化bean之前執行這個方法

運行結果

我們以Spring Boot App的方式運行App.java文件,得到的結果如下: 
爬取信息.png
我們在看一下數據庫內的信息 
數據庫記錄.png
發現數據庫也有信息了,大功告成

總結

寫一個簡單的爬蟲其實也不難,但是其中也有不少的知識點需要梳理和記憶,發現問題或者是錯誤,查google,查文檔,一點點debug去調試,最終把問題一點點的解決,編程其實需要是解決問題的能力,這種的能力的鍛鍊需要我們去多寫代碼,寫完了代碼之後還要多思考,思考爲什麼要這樣寫?還有沒有更好的實現方式?爲什麼會出問題?需要怎麼解決?這纔是一名優秀的程序員應該養成的習慣,共勉!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章