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来实现。
....应该还有很多吧~(⌒▽⌒)。
总的来说,希望几年之后的我,看到现在这个小东西,应该会狠狠吐槽一下自己吧~