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來實現。
....應該還有很多吧~(⌒▽⌒)。
總的來說,希望幾年之後的我,看到現在這個小東西,應該會狠狠吐槽一下自己吧~