賬號池

 

Bad times make a good man.

最近埋頭在爬蟲裏,可謂是苦不堪言,常常會有這樣一種感覺,

面前有一座山,你鼓足了勁爬上了山頂,在即將登上山頂之時自己不由得開始手舞足蹈,

期盼看到山那頭的景色,但天不遂人願,到了山頂才發現那頭還有一座更高的山。

有些網站呢, 既沒有風控策略,也沒有登錄攔截,整個頁面也沒有渲染。

有些網站吧,非要自己整一套前端框架,數據放在meta塊裏。

有些網站吧, 加上了風控策略。

最可恨的是有些網站,還加上了登錄攔截。

 

俗話說得好,上有政策,下有對策。

說了這麼多沒有用的廢話,那麼小子我言歸正傳,今天想跟大家一起分享一個自己寫的賬號池,用來解決某些網站需要登錄才能抓取的困難。(由於網站加上了風控策略,一個賬號頻繁訪問或者有規律的去請求訪問就會導致被封),那麼賬號池的作用就是最大程度的減少同一個賬號抓取網站的頻次)

整個模型如下圖:

簡單的來說就是

兩個池子 一個池子放有效的數據,

另一個池子放過期的數據。

有效池中會有一個標記位表示最低容量,

當容量低於這個閾值時,會從過期池取數據出來直到超過這個閾值的2倍。

/**
 * Created by tiancan on 2018/8/29.
 */
public abstract class BinaryCyclePool<T> {

    protected static int minSize = 20;

    protected static boolean supplementTag = false;

    protected Map<T, Object> currentMap;

    protected Queue<T> expireQueue;

    protected List<T> indexList;

    BinaryCyclePool() {
        currentMap = new ConcurrentHashMap<T, Object>();
        expireQueue = new ConcurrentLinkedQueue<T>();
        indexList = new CopyOnWriteArrayList<T>();
    }

    public Object get(T t) {
        return currentMap.get(t);
    }

    public void put(T t, Object obj) {
        currentMap.put(t, obj);
        indexList.add(t);
    }

    public void remove(T t) {
        currentMap.remove(t);
        indexList.remove(t);
    }

    abstract void transferCurrentToExpire(T t);

    abstract void transferExpireToCurrent();

}

或許有點抽象,那麼我們看看代碼是如何將模型應用到實際生產中的,

首先,在基類BinaryCyclePool中,

維護了一個int類型的閾值,一個map,一個queue以及一個list。

queue裏存放的是過期的賬號,

map裏存放的是有效的賬號,

list存放的是一個針對於map的索引,爲了隨機讀取map。

實現了get、put以及remove方法。

get操作直接從map裏拿到指定key的value,

put、remove操作會同時更新map以及索引list,

以及兩個待子類實現的抽象方法

transferCurrentToExpire(T t) 和 transferExpireToCurrent()

 

再看看具體實現類

這裏我拿某個網站舉例實現某個網站的賬號池。

/**
 * Created by tiancan on 2018/8/29.
 */
public class AccountBinaryCyclePool<T> extends BinaryCyclePool<T> {

    public AccountBinaryCyclePool(int size) {
        super();
        minSize = size;
    }

    private ThreadLocal<KeyAndValue<T>> keyAndValueThreadLocal = new ThreadLocal<KeyAndValue<T>>();

    public Object getRandom() {
        if(null == indexList || indexList.size() == 0) {
            return null;
        }
        T t = indexList.get((int)(Math.random() * indexList.size()));
        KeyAndValue<T> keyAndValue = new KeyAndValue<T>(t, get(t));
        keyAndValueThreadLocal.set(keyAndValue);
        return keyAndValue.getObject();
    }

    public void transferCurrentToExpire(T key) {
        if (currentMap.get(key) != null) {
            expireQueue.add(key);
        }
        keyAndValueThreadLocal.remove();
        remove(key);
        if (currentMap.size() < minSize) {
            transferExpireToCurrent();
            supplementTag = !supplementTag;
        }
        if(supplementTag) {
            if(currentMap.size() < (2 * minSize)) {
                transferExpireToCurrent();
            }else {
                supplementTag = !supplementTag;
            }
        }
    }

    public void transferExpireToCurrent() {
        T key = expireQueue.poll();

        if (key instanceof UserAccount && null != key) {
            currentMap.put(key, setCookies((UserAccount) key));
            indexList.add(key);
        }
    }

    public ThreadLocal<KeyAndValue<T>> getKeyAndValueThreadLocal() {
        return keyAndValueThreadLocal;
    }

    public void setKeyAndValueThreadLocal(ThreadLocal<KeyAndValue<T>> keyAndValueThreadLocal) {
        this.keyAndValueThreadLocal = keyAndValueThreadLocal;
    }
    
}

首先在子類中定義了一個ThreadLocal,作爲多線程從池子裏取值時能拿到當前線程所拿到的賬號信息。

包括當前線程拿到的賬號,密碼以及cookie。

由於賬號池的目的就是每個線程拿到不同的賬號的cookie信息去請求網站,

getRandom方法,隨機從map裏取出一個賬號的cookie。

transferCurrentToExpire方法,將map裏失效的賬號移除map,放入到過期queue中。並且刪除索引,在這個過程中會判斷當前map的容量,如果低於閾值,會調用transferExpireToCurrent,並且會將開始調整標誌位記爲true,再判斷調整標誌位和容量是否達到閾值的兩倍,再次調用transferExpireToCurrent方法,如果容量已達到閾值兩倍,恢復調整標記位爲false。

transferExpireToCurrent方法, 將過期queue中的頭部節點彈出,通過過期的賬號重新去拿到最新的cookie,轉移到map中,並且更新索引。

至此,整個賬號池在多線程抓取的爬蟲框架中能非常完美的嵌入使用。

step1:初始化抓取的時候將賬號導入到內存中。

step2:在當前線程拿到頁面錯誤時,通過threadlocal中存放的當前賬號信息對應上map中,移去失效的賬號,重新隨機獲取賬號cookie。

 

 

優化點:

1.實現一個lru的反例,最近最多訪問算法,在每次從map裏拿到賬號的同時,判斷lru緩存中是否有該賬號,如果有,則重新獲取。(進一步減少同一賬號訪問頻次)

2.demo僅能用於單機,分佈式可利用redis來實現。

....應該還有很多吧~(⌒▽⌒)。

總的來說,希望幾年之後的我,看到現在這個小東西,應該會狠狠吐槽一下自己吧~

 

 

 

 

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