账号池

 

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来实现。

....应该还有很多吧~(⌒▽⌒)。

总的来说,希望几年之后的我,看到现在这个小东西,应该会狠狠吐槽一下自己吧~

 

 

 

 

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