分布式網絡爬蟲的基本實現簡述


  一、前言

    前一段時間,小小的寫了一個爬蟲,是關於電商網站的。今天,把它分享出來,供大家參考,如有不足之處,請見諒!(抱拳)

  二、准備工作

    我們實現的這個爬蟲是Java編寫的。所用到的框架或者技術如下:

    Redis:分布式的Key-Value數據庫,用來作存儲臨時的頁面URL的倉庫。

    HttpClient:Apache旗下的一款軟件,用來下載頁面。

    HtmlCleaner&xPath:網頁分析軟件,解析出相關的需要的信息。

    MySQL數據庫:用於存儲爬取過來的商品的詳細信息。

    ZooKeeper:分布式協調工具,用於后期監控各個爬蟲的運行狀態。

  三、業務需求

    抓取某電商商品詳細信息,需要的相關字段為:商品ID、商品名稱、商品價格、商品詳細信息。

  四、整體架構布局

    首先是我們的核心部分——爬蟲程序。爬蟲的過程為:從Redis數據倉庫中取出URL,利用HttpClient進行下載,下載后的頁面內容,我們使用HtmlCleaner和xPath進行頁面解析,這時,我們解析的頁面可能是商品的列表頁面,也有可能是商品的詳細頁面。如果是商品列表頁面,則需要解析出頁面中的商品詳細頁面的URL,並放入Redis數據倉庫,進行后期解析;如果是商品的詳細頁面,則存入我們的MySQL數據。具體的架構圖如下:

    當核心程序爬蟲編寫完后,為了加快頁面抓取效率,我們需要對爬蟲進行改進:一是對爬蟲程序進行多線程改造;二是將爬蟲部署到多台服務器,進一步加快爬蟲的抓取效率。在實際生產環境中,由於刀片服務器的穩定性不太好,所以可能導致一些問題,例如:爬蟲進程掛掉,這些問題有可能是經常出現的,所以我們需要對其進行監控,一旦發現爬蟲進程掛了,立即啟動腳本對爬蟲進程進行重新啟動,保證我們整個爬蟲核心的持續性作業。這是就用到了我們的分布式協調服務ZooKeeper。我們可以另外寫一個監控進程的程序用來實時監控爬蟲的運行情況,原理是:爬蟲在啟動時,在ZooKeeper服務中注冊一個自己的臨時目錄,監控程序利用ZooKeeper監控爬蟲所注冊的臨時目錄,利用ZooKeeper的性質——如果注冊臨時目錄的程序掛掉后,這個臨時目錄過一會兒也會消失,利用這個性質,我們的監控程序監控爬蟲所注冊的臨時目錄,一旦發現臨時目錄消失,則說明改服務器上的爬蟲進程已掛,於是我們需要啟動腳本重新啟動爬蟲進程。隨后我們將抓取得到的商品詳細信息存儲到我們的分布式MySQL數據庫中。以下是整個爬蟲項目的架構圖:

 

    接下來,我們將重點分析爬蟲架構中的各個部分。

    五、Redis數據庫——臨時存儲待抓取的URL。

      Redis數據庫是一個基於內存的Key-Value非關系型數據庫。由於其讀寫速度極快,收到了人們的熱捧(每秒10W左右的讀寫速度)。選用Redis數據庫作臨時數據存儲正是基於此。為了使我們的爬蟲優先抓取商品列表頁面,我們在Redis中定義了兩個隊列(利用Redis的list的lpop和rpush模擬),分別是高優先級隊列和低優先級隊列,我們再高優先級隊列中存儲了商品列表頁面,在低優先級隊列存儲了商品詳細頁面。這樣我們就可以保證爬蟲再進行抓取數據的時候,優先從高優先級隊列取數據,從而使得爬蟲優先抓取商品列表頁面。為了很好地利用Redis數據庫,我們編寫了一個對於Redis的操作工具類。

package cn.mrchor.spider.utils;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisUtils {
    
    public static String highKey = "jd_high_key";
    public static String lowKey = "jd_low_key";
    
    private JedisPool jedisPool = null;
    /**
     * 構造函數初始化jedis數據庫連接池
     */
    public JedisUtils() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(10);
        jedisPoolConfig.setMaxTotal(100);
        jedisPoolConfig.setMaxWaitMillis(10000);
        jedisPoolConfig.setTestOnBorrow(true);
        jedisPool = new JedisPool("192.168.52.128", 6379);
    }
    /**
     * 獲取jedis對象操作jedis數據庫
     * @return
     */
    public Jedis getJedis() {
        return this.jedisPool.getResource();
    }
    
    /**
     * 往list添加數據
     */
    public void addUrl(String key, String url) {
        Jedis jedis = jedisPool.getResource();
        jedis.lpush(key, url);
        jedisPool.returnResourceObject(jedis);
    }
    
    /**
     * 從list中取數據
     */
    public String getUrl(String key) {
        Jedis jedis = jedisPool.getResource();
        String url = jedis.rpop(key);
        jedisPool.returnResourceObject(jedis);
        return url;
    }
}

    六、HttpClient——使用IP代理抓取數據

      為防止爬蟲在頻繁訪問電商頁面的行為被對方程序發現,爬蟲程序一般在進行抓取數據的時候都是利用代理IP來抓取,以減少爬蟲被電商發現的概率。我們可以使用一些網上的免費IP代理,如西刺代理,也可以花錢買一些IP進行代理下載電商頁面。在使用代理進行頁面下載時,可能出現連接超時,但這有可能是網絡波動導致,也可能是代理IP失效。為了防止出現誤判,我們在此做了三次嘗試連接的機制代碼,如果三次嘗試失敗,則認為這個IP失效。

    七、HtmlCleaner&xPath——對下載過來的頁面進行解析

      解析頁面是比較繁瑣的任務,我們首先要確定需要解析的對象,然后再利用瀏覽器提供的xPath工具,copy xpath,然后再根據這個xpath解析出需要的東西。下圖是我們解析商品價格用到的xpath(值為:/html/body/div[5]/div/div[2]/div[3]/div/div[1]/div[2]/span/span[2]),具體的解析代碼附在圖后:

 

package cn.mrchor.spider.process;

import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.TagNode;
import org.htmlcleaner.XPatherException;
import org.json.JSONArray;
import org.json.JSONObject;

import cn.mrchor.spider.domain.Page;
import cn.mrchor.spider.download.HttpClientDownloadModeImpl;
import cn.mrchor.spider.utils.HtmlUtils;
import cn.mrchor.spider.utils.PageUtils;

public class HttpCleanerProcessModeImpl implements ProcessMode {

    @Override
    public void process(Page page) {
        // 創建htmlcleaner對象
        HtmlCleaner htmlCleaner = new HtmlCleaner();
        // 使用htmlcleaner對象操作頁面內容content,得到tagNode對象
        TagNode tagNode = htmlCleaner.clean(page.getContent());
        if (page.getUrl().startsWith("http://list.jd.com/list.html")) {
            // 商品列表頁面解析
            try {
                // 解析商品列表頁面的url //*[@id="plist"]/ul/li[1]/div/div[4]/a
                Object[] goodsList = tagNode.evaluateXPath("//*[@id=\"plist\"]/ul/li/div/div[4]/a");
                for (Object object : goodsList) {
                    TagNode goodsUrl = (TagNode) object;
                    System.err.println("http:" + goodsUrl.getAttributeByName("href"));
                    page.addUrl("http:" + goodsUrl.getAttributeByName("href"));
                }
                // 解析商品列表頁面中下一頁的url
                Object[] nextPage = tagNode.evaluateXPath("//*[@id=\"J_topPage\"]/a[2]");
                for (Object object : nextPage) {
                    TagNode nextUrl = (TagNode) object;
                    if (!nextUrl.getAttributeByName("href").contains("javascript:;")) {
                        page.addUrl("http://list.jd.com" + nextUrl.getAttributeByName("href"));
                        // System.err.println("http://list.jd.com" +
                        // nextUrl.getAttributeByName("href"));
                    }
                }
            } catch (XPatherException e) {
                e.printStackTrace();
            }
        } else if (page.getUrl().startsWith("http://item.jd.com/")) {
            // 商品詳細信息的頁面解析
            GoodsInfoProcess.goodsInfoProcess(tagNode, page);
        }
    }
}

    八、MySQL數據庫——存儲商品詳細信息

    在操作MySQL數據庫這一塊,我們也是寫了一個數據庫操作工具類,使用了Apache數據庫連接池DBCP,這個算是比較簡單的,配置了DBCP的配置文件后就可以很好地使用了:

package cn.mrchor.spider.utils;

import java.sql.Connection;
import java.sql.SQLException;

import com.mchange.v2.c3p0.ComboPooledDataSource;

public class MySqlConnectionTool {
    
    public static Connection getConnection(String database) {
        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource(database);
        Connection connection = null;
        try {
            connection = comboPooledDataSource.getConnection();
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return connection;
    }
    public static Connection getConnection() {
        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
        Connection connection = null;
        try {
            connection = comboPooledDataSource.getConnection();
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return connection;
    }
}

    九、ZooKeeper服務——監控爬蟲集群進程的運行情況

    

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM