前提

最近忙于业务开发、交接和游戏,加上碰上了不定时出现的犹豫期和困惑期,荒废学业了一段时间。天冷了,要重新拾起开始下阶段的学习了。之前接触到的一些数据搜索项目,涉及到请求模拟,基于反爬需要使用随机的User Agent,于是使用Redis实现了一个十分简易的UA池。

背景

最近的一个需求,有模拟请求的逻辑,要求每次请求的请求头中的User Agent要满足下面几点:

  • 每次获取的User Agent是随机的。
  • 每次获取的User Agent(短时间内)不能重复。
  • 每次获取的User Agent必须带有主流的操作系统信息(可以是UinuxWindowsIOS和安卓等等)。

这里三点都可以从UA数据的来源解决,实际上我们应该关注具体的实现方案。简单分析一下,流程如下:

在设计UA池的时候,它的数据结构和环形队列十分类似:

上图中,假设不同颜色的UA是完全不同的UA,它们通过洗牌算法打散放进去环形队列中,实际上每次取出一个UA之后,只需要把游标cursor前进或者后退一格即可(甚至可以把游标设置到队列中的任意元素)。最终的实现就是:需要通过中间件实现分布式队列(只是队列,不是消息队列)。

具体实现方案

毫无疑问需要一个分布式数据库类型的中间件才能存放已经准备好的UA,第一印象就感觉Redis会比较合适。接下来需要选用Redis的数据类型,主要考虑几个方面:

  • 具备队列性质。
  • 最好支持随机访问。
  • 元素入队、出队和随机访问的时间复杂度要低,毕竟获取UA的接口访问量会比较大。

支持这几个方面的Redis数据类型就是List,不过注意List本身不能去重,去重的工作可以用代码逻辑实现。然后可以想象客户端获取UA的流程大致如下:

结合前面的分析,编码过程有如下几步:

  1. 准备好需要导入的UA数据,可以从数据源读取,也可以直接文件读取。
  2. 因为需要导入的UA数据集合一般不会太大,考虑先把这个集合的数据随机打散,如果使用Java开发可以直接使用Collections#shuffle()洗牌算法,当然也可以自行实现这个数据随机分布的算法,这一步对于一些被模拟方会严格检验UA合法性的场景是必须的
  3. 导入UA数据到Redis列表中。
  4. 编写RPOP + LPUSHLua脚本,实现分布式循环队列。

编码和测试示例

引入Redis的高级客户端Lettuce依赖:

<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>5.2.1.RELEASE</version>
</dependency>

编写RPOP + LPUSHLua脚本,Lua脚本名字暂称为L_RPOP_LPUSH.lua,放在resources/scripts/lua目录下:

local key = KEYS[1]
local value = redis.call('RPOP', key)
redis.call('LPUSH', key, value)
return value

这个脚本十分简单,但是已经实现了循环队列的功能。剩下来的测试代码如下:

public class UaPoolTest {

    private static RedisCommands<String, String> COMMANDS;

    private static AtomicReference<String> LUA_SHA = new AtomicReference<>();
private static final String KEY = "UA_POOL"; @BeforeClass
public static void beforeClass() throws Exception {
// 初始化Redis客户端
RedisURI uri = RedisURI.builder().withHost("localhost").withPort(6379).build();
RedisClient redisClient = RedisClient.create(uri);
StatefulRedisConnection<String, String> connect = redisClient.connect();
COMMANDS = connect.sync();
// 模拟构建UA池的原始数据,假设有10个UA,分别是UA-0 ... UA-9
List<String> uaList = Lists.newArrayList();
IntStream.range(0, 10).forEach(e -> uaList.add(String.format("UA-%d", e)));
// 洗牌
Collections.shuffle(uaList);
// 加载Lua脚本
ClassPathResource resource = new ClassPathResource("/scripts/lua/L_RPOP_LPUSH.lua");
String content = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
String sha = COMMANDS.scriptLoad(content);
LUA_SHA.compareAndSet(null, sha);
// Redis队列中写入UA数据,数据量多的时候可以考虑分批写入防止长时间阻塞Redis服务
COMMANDS.lpush(KEY, uaList.toArray(new String[0]));
} @AfterClass
public static void afterClass() throws Exception {
COMMANDS.del(KEY);
} @Test
public void testUaPool() {
IntStream.range(1, 21).forEach(e -> {
String result = COMMANDS.evalsha(LUA_SHA.get(), ScriptOutputType.VALUE, KEY);
System.out.println(String.format("第%d次获取到的UA是:%s", e, result));
});
}
}

某次运行结果如下:

第1次获取到的UA是:UA-0
第2次获取到的UA是:UA-8
第3次获取到的UA是:UA-2
第4次获取到的UA是:UA-4
第5次获取到的UA是:UA-7
第6次获取到的UA是:UA-5
第7次获取到的UA是:UA-1
第8次获取到的UA是:UA-3
第9次获取到的UA是:UA-6
第10次获取到的UA是:UA-9
第11次获取到的UA是:UA-0
第12次获取到的UA是:UA-8
第13次获取到的UA是:UA-2
第14次获取到的UA是:UA-4
第15次获取到的UA是:UA-7
第16次获取到的UA是:UA-5
第17次获取到的UA是:UA-1
第18次获取到的UA是:UA-3
第19次获取到的UA是:UA-6
第20次获取到的UA是:UA-9

可见洗牌算法的效果不差,数据相对分散。

小结

其实UA池的设计难度并不大,需要注意几个要点:

  • 一般主流的移动设备或者桌面设备的系统版本不会太多,所以来源UA数据不会太多,最简单的实现可以使用文件存放,一次读取直接写入Redis中。
  • 注意需要随机打散UA数据,避免同一个设备系统类型的UA数据过于密集,这样可以避免触发模拟某些请求时候的风控规则。
  • 需要熟悉Lua的语法,毕竟Redis的原子指令一定离不开Lua脚本。

(本文完 c-2-d e-a-20191114)

原文链接

使用Redis实现UA池的更多相关文章

  1. selenium、UA池、ip池、scrapy-redis的综合应用案例

    案例: 网易新闻的爬取: https://news.163.com/ 爬取的内容为一下4大板块中的新闻内容 爬取: 特点: 动态加载数据  ,用 selenium 爬虫 1. 创建项目 scrapy ...

  2. Redis客户端连接池

    使用场景 对于一些大对象,或者初始化过程较长的可复用的对象,我们如果每次都new对象出来,那么意味着会耗费大量的时间. 我们可以将这些对象缓存起来,当接口调用完毕后,不是销毁对象,当下次使用的时候,直 ...

  3. redis运用连接池报错解决

    redis使用连接池报错解决redis使用十几小时就一直报异常 redis.clients.jedis.exceptions.JedisConnectionException: Could not g ...

  4. selenium在scrapy中的使用、UA池、IP池的构建

    selenium在scrapy中的使用流程 重写爬虫文件的构造方法__init__,在该方法中使用selenium实例化一个浏览器对象(因为浏览器对象只需要被实例化一次). 重写爬虫文件的closed ...

  5. 14.UA池和代理池

    今日概要 scrapy下载中间件 UA池 代理池 今日详情 一.下载中间件 先祭出框架图: 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件. - ...

  6. UA池和代理池

    scrapy下载中间件 UA池 代理池 一.下载中间件 先祭出框架图: 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件. - 作用: (1)引擎 ...

  7. UA池和代理池在scrapy中的应用

    一.下载中间件 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件. - 作用: (1)引擎将请求传递给下载器过程中, 下载中间件可以对请求进行一系 ...

  8. 爬虫开发13.UA池和代理池在scrapy中的应用

      今日概要 scrapy下载中间件 UA池 代理池 今日详情 一.下载中间件 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件. - 作用: ( ...

  9. scrapy下载中间件,UA池和代理池

    一.下载中间件 框架图: 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件. - 作用: (1)引擎将请求传递给下载器过程中, 下载中间件可以对请 ...

随机推荐

  1. VS2019 开发Django(十一)------表单

    导航:VS2019开发Django系列 今天是中华人民共和国成立70周年的日子,普天同庆,看阅兵看得满腔热血,热泪盈眶,祖国都这么优秀了,我要更加努力才行啊! 这个Django系列的文章,没有很深入的 ...

  2. java嵌套接口

    java嵌套接口 package object; class A { //嵌套在类中的接口,可以被private,protected,default和public四种权限修饰 interface B ...

  3. Rancher 2.3.3发布!默认支持K8S 1.16

    2019年11月28日,Rancher Labs发布了Rancher全新版本2.3.3,该版本默认支持Kubernetes1.16,此外还带来了其他功能与优化. 目前,Rancher的Latest和S ...

  4. Python面向对象-枚举类型enum

    枚举类型:在实际问题中,有些变量的值被限定在一个有限的范围内.例如:一个星期有且只有7天,一年有且只有十二个月,一个班每周有6门课程等等.如果把这些量说明为整型.字符串或者其他类型显然是不合适.编程界 ...

  5. 关于ConfigurationSection自定义config的简单使用

    1.1.自定义config结构(参考对应颜色标注),放到configuration根节点下: <test> <testInfos> <" /> <& ...

  6. Zabbix Server 3.2

    软件环境 Centos7.3 LAMP Zabbix 3.2  1. Installing repository configuration package Install the repositor ...

  7. Linux中防火墙命令

    #启动   systemctl start firewalld #开机启动   systemctl enable firewalld #停止   systemctl stop firewalld #禁 ...

  8. linux for games; steamos; fedora game distribution

    最近对linux 游戏发行版系统产生了兴趣,下面简要记录一些链接: https://itsfoss.com/linux-gaming-distributions/ (9 款游戏系统) https:// ...

  9. CodeForces - 5C(思维+括号匹配)

    题意 https://vjudge.net/problem/CodeForces-5C 给出一个括号序列,求出最长合法子串和它的数量. 合法的定义:这个序列中左右括号匹配. 思路 这个题和普通的括号匹 ...

  10. SpringCloud断路器(Hystrix)

    一.为什么需要 Hystrix? 在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用(RPC).为了保证其高可用,单个服务又必须集群部署.由于网络原因或者自身的原因,服务并不能保 ...