Java 動手寫爬蟲: 2、 深度爬取

第二篇

前面實現了一個最基礎的爬取單網頁的爬蟲,這一篇則着手解決深度爬取的問題html

簡單來說,就是爬了一個網頁以後,繼續爬這個網頁中的連接java

1. 需求背景

背景比較簡單和明確,當爬了一個網頁以後,目標是不要就此打住,掃描這個網頁中的連接,繼續爬,因此有幾個點須要考慮:git

  • 哪些連接能夠繼續爬 ?
  • 是否要一直爬下去,要不要給一個終止符?
  • 新的連接中,提取內容的規則和當前網頁的規則不一致能夠怎麼辦?

2. 設計

針對上面的幾點,結合以前的實現結構,在執行 doFetchPage 方法獲取網頁以後,還得作一些其餘的操做github

  • 掃描網頁中的連接,根據過濾規則匹配知足要求的連接
  • 記錄一個depth,用於表示爬取的深度,即從最原始的網頁出發,到當前頁面中間轉了幾回(講到這裏就有個循環爬取的問題,後面說)
  • 不一樣的頁面提取內容規則不同,所以能夠考慮留一個接口出來,讓適用方本身來實現解析網頁內容

基本實現

開始依然是先把功能點實現,而後再考慮具體的優化細節web

先加一個配置項,表示爬取頁面深度; 其次就是保存的結果,得有個容器來暫存, 因此在 SimpleCrawlJob 會新增兩個屬性緩存

/**
 * 批量查詢的結果
 */
private List<CrawlResult> crawlResults = new ArrayList<>();


/**
 * 爬網頁的深度, 默認爲0, 即只爬取當前網頁
 */
private int depth = 0;

由於有深度爬取的過程,因此須要修改一下爬取網頁的代碼,新增一個 doFetchNetxtPage方法,進行迭代爬取網頁,這時,結果匹配處理方法也不能如以前的直接賦值了,稍微改一下便可, 改爲返回一個接過實例安全

/**
 * 執行抓取網頁
 */
public void doFetchPage() throws Exception {
    doFetchNextPage(0, this.crawlMeta.getUrl());
    this.crawlResult = this.crawlResults.get(0);
}


private void doFetchNextPage(int currentDepth, String url) throws Exception {
    HttpResponse response = HttpUtils.request(new CrawlMeta(url, this.crawlMeta.getSelectorRules()), httpConf);
    String res = EntityUtils.toString(response.getEntity());
    CrawlResult result;
    if (response.getStatusLine().getStatusCode() != 200) { // 請求成功
        result = new CrawlResult();
        result.setStatus(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());
        result.setUrl(crawlMeta.getUrl());
        this.crawlResults.add(result);
        return;
    }

    result = doParse(res);

    // 超過最大深度, 不繼續爬
    if (currentDepth > depth) {
        return;
    }


    Elements elements = result.getHtmlDoc().select("a[href]");
    for(Element element: elements) {
        doFetchNextPage(currentDepth + 1, element.attr("href"));
    }
}


private CrawlResult doParse(String html) {
    Document doc = Jsoup.parse(html);
    
    Map<String, List<String>> map = new HashMap<>(crawlMeta.getSelectorRules().size());
    for (String rule : crawlMeta.getSelectorRules()) {
        List<String> list = new ArrayList<>();
        for (Element element : doc.select(rule)) {
            list.add(element.text());
        }
    
        map.put(rule, list);
    }
    
    
    CrawlResult result = new CrawlResult();
    result.setHtmlDoc(doc);
    result.setUrl(crawlMeta.getUrl());
    result.setResult(map);
    result.setStatus(CrawlResult.SUCCESS);
    return result;
}

說明

主要的關鍵代碼在 doFetchNextPage 中,這裏有兩個參數,第一個表示當前url屬於爬取的第幾層,爬完以後,判斷是否超過最大深度,若是沒有,則獲取出網頁中的全部連接,迭代調用一遍多線程

下面主要是獲取網頁中的跳轉連接,直接從jsoup的源碼中的example中獲取,獲取網頁中連接的方法app

// 未超過最大深度, 繼續爬網頁中的全部連接
result = doParse(res);
Elements elements = result.getHtmlDoc().select("a[href]");
for(Element element: elements) {
    doFetchNextPage(currentDepth + 1, element.attr("href"));
}

測試case

測試代碼和以前的差很少,惟一的區別就是指定了爬取的深度,返回結果就不截圖了,實在是有點多ide

/**
 * 深度爬
 * @throws InterruptedException
 */
@Test
public void testDepthFetch() throws InterruptedException {
    String url = "https://my.oschina.net/u/566591/blog/1031575";
    CrawlMeta crawlMeta = new CrawlMeta();
    crawlMeta.setUrl(url);


    SimpleCrawlJob job = new SimpleCrawlJob(1);
    job.setCrawlMeta(crawlMeta);
    Thread thread = new Thread(job, "crawlerDepth-test");
    thread.start();


    thread.join();
    List<CrawlResult> result = job.getCrawlResults();
    System.out.println(result);
}

3. 改進

問題

上面雖然是實現了目標,但問題卻有點多:

  • 就好比上面的測試case,發現有122個跳轉連接,順序爬速度有點慢

    14987169282459.jpg

  • 連接中存在重複、頁面內錨點、js等各類狀況,並非都知足需求

  • 最後的結果塞到List中,深度較多時,連接較多時,list可能被撐暴

- 添加連接的過濾

過濾規則,能夠劃分爲兩種,正向的匹配,和逆向的排除

首先是修改配置類 CrawlMeta, 新增兩個配置

/**
 * 正向的過濾規則
 */
@Setter
@Getter
private Set<Pattern> positiveRegex = new HashSet<>();


/**
 * 逆向的過濾規則
 */
@Setter
@Getter
private Set<Pattern> negativeRegex = new HashSet<>();


public Set<Pattern> addPositiveRegex(String regex) {
    this.positiveRegex.add(Pattern.compile(regex));
    return this.positiveRegex;
}


public Set<Pattern> addNegativeRegex(String regex) {
    this.negativeRegex.add(Pattern.compile(regex));
    return this.negativeRegex;
}

而後在遍歷子連接時,判斷一下是否知足需求

// doFetchNextPage 方法

Elements elements = result.getHtmlDoc().select("a[href]");
String src;
for(Element element: elements) {
    src = element.attr("href");
    if (matchRegex(src)) {
        doFetchNextPage(currentDepth + 1, element.attr("href"));
    }
}



// 規則匹配方法
private boolean matchRegex(String url) {
    Matcher matcher;
    for(Pattern pattern: crawlMeta.getPositiveRegex()) {
        matcher = pattern.matcher(url);
        if (matcher.find()) {
            return true;
        }
    }


    for(Pattern pattern: crawlMeta.getNegativeRegex()) {
        matcher = pattern.matcher(url);
        if(matcher.find()) {
            return false;
        }
    }


    return crawlMeta.getPositiveRegex().size() == 0;
}

上面主要是經過正則來進行過濾,暫不考慮正則帶來的開銷問題,至少是解決了一個過濾的問題

可是,可是,若是網頁中的連接是相對路徑的話,會怎麼樣

直接使用 Jsoup來測試一個網頁,看獲取的link地址爲何

// 獲取網頁中的全部連接
@Test
public void testGetLink() throws IOException {
    String url = "http://chengyu.911cha.com/zishu_3_p1.html";

    Connection httpConnection = HttpConnection.connect(url)
            .header("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
            .header("connection", "Keep-Alive")
            .header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36");

    Document doc = httpConnection.get();
    Elements links = doc.select("a[href]");

    print("\nLinks: (%d)", links.size());
}

看下取出的連接

1.png

根據上面的測試,獲取的連接若是是相對地址,則會有問題,須要有一個轉化的過程,這個改動比較簡單,jsoup自己是支持的

改一行便可

// 解析爲documnet對象時,指定 baseUrl
// 上面的代碼結構會作一點修改,後面會說到
Document doc = Jsoup.parse(html, url);


// 獲取連接時,前面添加abs
src = element.attr("abs:href");

3.png

- 保存結果

當爬取的數據量較多時,將結果都保存在內存中,並非一個好的選擇,假色每一個網頁中,知足規則的是有10個,那麼depth=n, 則從第一個網頁出發,最終會獲得

1 + 10 + ... + 10^n = (10^(n+1) - 1) / 9

顯然在實際狀況中是不可取的,所以能夠改造一下,獲取數據後給一個回調,讓用戶本身來選擇如何處理結果,這時 SimpleCrawelJob 的結構基本上知足不了需求了

從新開始設計

1. AbstractJob 類中定義一個回調方法

/**
 * 解析完網頁後的回調方法
 *
 * @param crawlResult
 */
protected abstract void visit(CrawlResult crawlResult);

2. DefaultAbstractCrawlJob 實現爬取網頁邏輯的抽象類

這個類實現爬取網頁的主要邏輯,也就是將以前的SimpleCrwalJob的實現拷貝過來,區別是幹掉了返回結果; 順帶修了一個小bug 😢

/**
 * Created by yihui on 2017/6/29.
 */
@Getter
@Setter
@NoArgsConstructor
public abstract class DefaultAbstractCrawlJob extends AbstractJob {
    /**
     * 配置項信息
     */
    private CrawlMeta crawlMeta;


    /**
     * http配置信息
     */
    private CrawlHttpConf httpConf = new CrawlHttpConf();


    /**
     * 爬網頁的深度, 默認爲0, 即只爬取當前網頁
     */
    protected int depth = 0;


    public DefaultAbstractCrawlJob(int depth) {
        this.depth = depth;
    }


    /**
     * 執行抓取網頁
     */
    public void doFetchPage() throws Exception {
        doFetchNextPage(0, this.crawlMeta.getUrl());
    }


    private void doFetchNextPage(int currentDepth, String url) throws Exception {
        CrawlMeta subMeta = new CrawlMeta(url, this.crawlMeta.getSelectorRules(), this.crawlMeta.getPositiveRegex(), this.crawlMeta.getNegativeRegex());
        HttpResponse response = HttpUtils.request(subMeta, httpConf);
        String res = EntityUtils.toString(response.getEntity());
        CrawlResult result;
        if (response.getStatusLine().getStatusCode() != 200) { // 請求成功
            result = new CrawlResult();
            result.setStatus(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());
            result.setUrl(crawlMeta.getUrl());
            this.visit(result);
            return;
        }


        // 網頁解析
        result = doParse(res, subMeta);
        // 回調用戶的網頁內容解析方法
        this.visit(result);


        // 超過最大深度, 不繼續爬
        if (currentDepth > depth) {
            return;
        }


        Elements elements = result.getHtmlDoc().select("a[href]");
        String src;
        for(Element element: elements) {
            // 確保將相對地址轉爲絕對地址
            src = element.attr("abs:href");
            if (matchRegex(src)) {
                doFetchNextPage(currentDepth + 1, src);
            }
        }
    }


    private CrawlResult doParse(String html, CrawlMeta meta) {
        // 指定baseUrl, 不然利用 abs:href 獲取連接會出錯
        Document doc = Jsoup.parse(html, meta.getUrl());

        Map<String, List<String>> map = new HashMap<>(meta.getSelectorRules().size());
        for (String rule : crawlMeta.getSelectorRules()) {
            List<String> list = new ArrayList<>();
            for (Element element : doc.select(rule)) {
                list.add(element.text());
            }

            map.put(rule, list);
        }


        CrawlResult result = new CrawlResult();
        result.setHtmlDoc(doc);
        result.setUrl(meta.getUrl());
        result.setResult(map);
        result.setStatus(CrawlResult.SUCCESS);
        return result;
    }


    private boolean matchRegex(String url) {
        Matcher matcher;
        for(Pattern pattern: crawlMeta.getPositiveRegex()) {
            matcher = pattern.matcher(url);
            if (matcher.find()) {
                return true;
            }
        }


        for(Pattern pattern: crawlMeta.getNegativeRegex()) {
            matcher = pattern.matcher(url);
            if(matcher.find()) {
                return false;
            }
        }


        return crawlMeta.getPositiveRegex().size() == 0;
    }
}

3. SimpleCrawlJob

重寫這個簡單爬蟲任務的實現,由於主要邏輯在 DefaultAbstractCrawlJob中已經實現了,因此直接繼承過來便可

主要關注的就是 visit 方法,這裏就是爬取網頁以後的回調,這個最簡單的爬蟲任務,就是將結果保存在內存中

/**
 * 最簡單的一個爬蟲任務
 * <p>
 * Created by yihui on 2017/6/27.
 */
@Getter
@Setter
@NoArgsConstructor
public class SimpleCrawlJob extends DefaultAbstractCrawlJob {

    /**
     * 存儲爬取的結果
     */
    private CrawlResult crawlResult;


    /**
     * 批量查詢的結果
     */
    private List<CrawlResult> crawlResults = new ArrayList<>();



    public SimpleCrawlJob(int depth) {
        super(depth);
    }


    @Override
    protected void visit(CrawlResult crawlResult) {
        crawlResults.add(crawlResult);
    }


    public CrawlResult getCrawlResult() {
        if(crawlResults.size() == 0) {
            return null;
        }

        return crawlResults.get(0);
    }
}

4,使用測試

和以前沒有任何區別,先來個簡單的

@Test
public void testDepthFetch() throws InterruptedException {
    String url = "http://chengyu.911cha.com/zishu_3_p1.html";
    CrawlMeta crawlMeta = new CrawlMeta();
    crawlMeta.setUrl(url);
    crawlMeta.addPositiveRegex("http://chengyu.911cha.com/zishu_3_p([0-9]+).html");


    SimpleCrawlJob job = new SimpleCrawlJob(1);
    job.setCrawlMeta(crawlMeta);
    Thread thread = new Thread(job, "crawlerDepth-test");
    thread.start();


    thread.join();
    List<CrawlResult> result = job.getCrawlResults();
    System.out.println(result);
}

運行截圖

1.png

直接使用 DefaultAbstractCrawl 抽象類的回調來進行測試

@Test
public void testSelfCwralFetch() throws InterruptedException {
    String url = "http://chengyu.911cha.com/zishu_3_p1.html";
    CrawlMeta crawlMeta = new CrawlMeta();
    crawlMeta.setUrl(url);
    crawlMeta.addPositiveRegex("http://chengyu.911cha.com/zishu_3_p([0-9]+).html");


    DefaultAbstractCrawlJob job = new DefaultAbstractCrawlJob(1) {
        @Override
        protected void visit(CrawlResult crawlResult) {
            System.out.println(crawlResult.getUrl());
        }
    };
    job.setCrawlMeta(crawlMeta);
    Thread thread = new Thread(job, "crawlerDepth-test");
    thread.start();

    thread.join();
    System.out.println("over");
}

- 爬蟲去重

從上面能夠發現,重複爬取是比較浪費的事情,所以去重是很是有必要的;通常想法是將爬過的url都標記一下,每次爬以前判斷是否已經爬過了

依然先是採用最low的方法,搞一個Set來記錄全部爬取的url,由於具體的爬蟲任務設計的是多線程的,因此這個Set是要求多線程共享的

此外考慮到去重的手段比較多,咱們目前雖然只是採用的內存中加一個緩存表,但不妨礙咱們設計的時候,採用面向接口的方式

1. IStorage 接口

提供存記錄,判斷記錄是否存在的方法

public interface IStorage {

    /**
     * 若爬取的URL不在storage中, 則寫入; 不然忽略
     *
     * @param url 爬取的網址
     * @return true 表示寫入成功, 即以前沒有這條記錄; false 則表示以前已經有記錄了
     */
    boolean putIfNotExist(String url, CrawlResult result);


    /**
     * 判斷是否存在
     * @param url
     * @return
     */
    boolean contains(String url);

}

2. RamStorage 利用Map實現的內存存儲

public class RamStorage implements IStorage {

    private Map<String, CrawlResult> map = new ConcurrentHashMap<>();


    @Override
    public boolean putIfNotExist(String url, CrawlResult result) {
        if(map.containsKey(url)) {
            return false;
        }

        map.put(url, result);
        return true;
    }

    @Override
    public boolean contains(String url) {
        return map.containsKey(url);
    }
}

3. StorageWrapper 封裝類

這個封裝類要求多線程共享,因此咱們採用單例模式,保證只有一個實例

一個最原始的實現方式以下(暫不考慮其中比較猥瑣的storage實例化方式)

public class StorageWrapper {

    private static StorageWrapper instance = new StorageWrapper();


    private IStorage storage;

    public static StorageWrapper getInstance() {
        return instance;
    }


    private StorageWrapper() {
        storage = new RamStorage();
    }


    /**
     * 判斷url是否被爬取過
     *
     * @param url
     * @return
     */
    public boolean ifUrlFetched(String url) {
        return storage.contains(url);
    }


    /**
     * 爬完以後, 新增一條爬取記錄
     * @param url
     * @param crawlResult
     */
    public void addFetchRecord(String url, CrawlResult crawlResult) {
        storage.putIfNotExist(url, crawlResult);
    }
}

這樣一個簡單的保存爬取歷史記錄的容器就有了,那麼在爬取時,就須要事前判斷一下

對應的 DefaultAbstractCrawlJob#doFetchNextPage 方法更新以下

// fixme 非線程安全
private void doFetchNextPage(int currentDepth, String url) throws Exception {
    if (StorageWrapper.getInstance().ifUrlFetched(url)) {
        return;
    }


    CrawlMeta subMeta = new CrawlMeta(url, this.crawlMeta.getSelectorRules(), this.crawlMeta.getPositiveRegex(), this.crawlMeta.getNegativeRegex());
    HttpResponse response = HttpUtils.request(subMeta, httpConf);
    String res = EntityUtils.toString(response.getEntity());
    CrawlResult result;
    if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { // 請求成功
        result = new CrawlResult();
        result.setStatus(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());
        result.setUrl(crawlMeta.getUrl());
        this.visit(result);
        return;
    }


    // 網頁解析
    result = doParse(res, subMeta);
    StorageWrapper.getInstance().addFetchRecord(url, result);

    // 回調用戶的網頁內容解析方法
    this.visit(result);


    // 超過最大深度, 不繼續爬
    if (currentDepth > depth) {
        return;
    }


    Elements elements = result.getHtmlDoc().select("a[href]");
    String src;
    for(Element element: elements) {
        // 確保將相對地址轉爲絕對地址
        src = element.attr("abs:href");
        if (matchRegex(src)) {
            doFetchNextPage(currentDepth + 1, src);
        }
    }
}

若是仔細看上面的方法,就會發如今多線程環境下,依然可能存在重複爬取的狀況

若有兩個CrawlJob任務,若爬取的是同一個url,第一個任務爬取完,尚未回寫到Storage時,第二個任務開始爬,這時,事前判斷沒有記錄,而後經過以後開始爬,這時就依然會出現重複爬的問題

要解決這個問題,一個簡單的方法就是加鎖,在判斷一個url沒有被爬時,到回寫一條爬取結果這段期間,加一個保護鎖

StorageWrapper 更新後以下

/**
 * Created by yihui on 2017/6/29.
 */
public class StorageWrapper {

    private static StorageWrapper instance = new StorageWrapper();


    private IStorage storage;

    private Map<String, Lock> lockMap = new ConcurrentHashMap<>();

    public static StorageWrapper getInstance() {
        return instance;
    }


    private StorageWrapper() {
        storage = new RamStorage();
    }


    /**
     * 判斷url是否被爬取過; 是則返回true; 否這返回false, 並上鎖
     *
     * @param url
     * @return
     */
    public boolean ifUrlFetched(String url) {
        if(storage.contains(url)) {
            return true;
        }

        synchronized (this) {
            if (!lockMap.containsKey(url)) {
                // 不存在時,加一個鎖
                lockMap.put(url, new ReentrantLock());
            }

            this.lock(url);


            if (storage.contains(url)) {
                return true;
            }
//            System.out.println(Thread.currentThread() + " lock url: " + url);
            return false;
        }
    }


    /**
     * 爬完以後, 新增一條爬取記錄
     * @param url
     * @param crawlResult
     */
    public void addFetchRecord(String url, CrawlResult crawlResult) {
        try {
            if (crawlResult != null) {
                storage.putIfNotExist(url, crawlResult);
                this.unlock(url);
            }
        } catch (Exception e) {
            System.out.println(Thread.currentThread().getName() + " result: " + url + " e: " + e);
        }
    }



    private void lock(String url) {
        lockMap.get(url).lock();
    }


    private void unlock(String url) {
        lockMap.get(url).unlock();
    }
}

使用處,稍稍變更以下

private void doFetchNextPage(int currentDepth, String url) throws Exception {
    CrawlResult result = null;
    try {
        // 判斷是否爬過;未爬取,則上鎖並繼續爬取網頁
        if (StorageWrapper.getInstance().ifUrlFetched(url)) {
            return;
        }

        CrawlMeta subMeta = new CrawlMeta(url, this.crawlMeta.getSelectorRules(), this.crawlMeta.getPositiveRegex(), this.crawlMeta.getNegativeRegex());
        HttpResponse response = HttpUtils.request(subMeta, httpConf);
        String res = EntityUtils.toString(response.getEntity());
        if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { // 請求成功
            result = new CrawlResult();
            result.setStatus(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());
            result.setUrl(crawlMeta.getUrl());
            this.visit(result);
            return;
        }


        // 網頁解析
        result = doParse(res, subMeta);
    } finally {
        // 添加一條記錄, 並釋放鎖
        StorageWrapper.getInstance().addFetchRecord(url, result);
    }

    // 回調用戶的網頁內容解析方法
    this.visit(result);


    // 超過最大深度, 不繼續爬
    if (currentDepth > depth) {
        return;
    }


    Elements elements = result.getHtmlDoc().select("a[href]");
    String src;
    for(Element element: elements) {
        // 確保將相對地址轉爲絕對地址
        src = element.attr("abs:href");
        if (matchRegex(src)) {
            doFetchNextPage(currentDepth + 1, src);
        }
    }
}

4. 測試

@Test
public void testSelfCwralFetch() throws InterruptedException {
    String url = "http://chengyu.t086.com/gushi/1.htm";
    CrawlMeta crawlMeta = new CrawlMeta();
    crawlMeta.setUrl(url);
    crawlMeta.addPositiveRegex("http://chengyu.t086.com/gushi/[0-9]+\\.htm$");


    DefaultAbstractCrawlJob job = new DefaultAbstractCrawlJob(1) {
        @Override
        protected void visit(CrawlResult crawlResult) {
            System.out.println("job1 >>> " + crawlResult.getUrl());
        }
    };
    job.setCrawlMeta(crawlMeta);



    String url2 = "http://chengyu.t086.com/gushi/2.htm";
    CrawlMeta crawlMeta2 = new CrawlMeta();
    crawlMeta2.setUrl(url2);
    crawlMeta2.addPositiveRegex("http://chengyu.t086.com/gushi/[0-9]+\\.htm$");
    DefaultAbstractCrawlJob job2 = new DefaultAbstractCrawlJob(1) {
        @Override
        protected void visit(CrawlResult crawlResult) {
            System.out.println("job2 >>> " + crawlResult.getUrl());
        }
    };
    job2.setCrawlMeta(crawlMeta2);



    Thread thread = new Thread(job, "crawlerDepth-test");
    Thread thread2 = new Thread(job2, "crawlerDepth-test2");
    thread.start();
    thread2.start();


    thread.join();
    thread2.join();
}

輸出以下

job2 >>> http://chengyu.t086.com/gushi/2.htm
job1 >>> http://chengyu.t086.com/gushi/1.htm
job1 >>> http://chengyu.t086.com/gushi/3.htm
job2 >>> http://chengyu.t086.com/gushi/4.htm
job1 >>> http://chengyu.t086.com/gushi/5.htm
job1 >>> http://chengyu.t086.com/gushi/6.htm
job1 >>> http://chengyu.t086.com/gushi/7.htm

小結

這一篇的博文有點多,到這裏其實上面一些提出的問題尚未解決,留待下一篇博文來fix掉, 下面則主要說明下本篇的要點

  1. 深度爬取

這裏使用了迭代的思路,爬到一個網頁以後,判斷是否須要中止,不中止,則把該網頁中的連接撈出來,繼續爬;關鍵點

  • 利用 Jsoup 獲取網頁中全部連接(注意相對路徑轉絕對路徑的用法)
  • 循環迭代
  1. 過濾

過濾,主要利用正則來匹配連接;這裏須要注意一下幾點

- 正向過濾
- 負向過濾
  1. 去重

如何保證一個連接被爬了以後,不會被重複進行爬取?

- 記錄爬取歷史
- 注意多線程安全問題
- 加鎖(一把鎖會致使性能下降,這裏採用了一個url對應一個鎖,注意看實現細節,較多的坑)

遺留問題

  1. 失敗重試
  2. 爬網頁中連接不該該串行進行
  3. 頻率控制(太快可能會被反扒幹掉)

源碼地址: https://github.com/liuyueyi/quick-crawler/releases/tag/v0.003

對應tag :v0.003

相關文章
相關標籤/搜索