最近忙于业务开发、交接和游戏,加上碰上了不定时出现的犹豫期和困惑期,荒废学业了一段时间 。天冷了,要重新拾起开始下阶段的学习了 。之前接触到的一些数据搜索项目,涉及到请求模拟,基于反爬需要使用随机的 User Agent,于是使用 redis 实现了一个十分简易的 UA 池 。
背景
最近的一个需求,有模拟请求的逻辑,要求每次请求的请求头中的 User Agent 要满足下面几点:
- 每次获取的 User Agent 是随机的 。
- 每次获取的 User Agent (短时间内)不能重复 。
- 每次获取的 User Agent 必须带有主流的操作系统信息(可以是 Uinux 、 windows 、 IOS 和Android/ target=_blank class=infotextkey>安卓等等) 。
文章插图
在设计 UA 池的时候,它的数据结构和环形队列十分类似:
文章插图
上图中,假设不同颜色的 UA 是完全不同的 UA,它们通过洗牌算法打散放进去环形队列中,实际上每次取出一个 UA 之后,只需要把游标 cursor 前进或者后退一格即可(甚至可以把游标设置到队列中的任意元素) 。最终的实现就是:需要通过中间件实现分布式队列(只是队列,不是消息队列) 。
具体实现方案
毫无疑问需要一个分布式数据库类型的中间件才能存放已经准备好的 UA,第一印象就感觉 Redis 会比较合适 。接下来需要选用 Redis 的数据类型,主要考虑几个方面:
UA支持这几个方面的 Redis 数据类型就是 List,不过注意 List 本身不能去重,去重的工作可以用代码逻辑实现 。然后可以想象客户端获取 UA 的流程大致如下:
文章插图
结合前面的分析,编码过程有如下几步:
- 准备好需要导入的 UA 数据,可以从数据源读取,也可以直接文件读取 。
- 因为需要导入的 UA 数据集合一般不会太大,考虑先把这个集合的数据随机打散,如果使用 JAVA 开发可以直接使用 Collections#shuffle() 洗牌算法,当然也可以自行实现这个数据随机分布的算法,这一步对于一些被模拟方会严格检验 UA 合法性的场景是必须的。
- 导入 UA 数据到 Redis 列表中 。
- 编写 RPOP + LPUSH 的 Lua 脚本,实现分布式循环队列 。
引入 Redis 的高级客户端 Lettuce 依赖:
<dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>5.2.1.RELEASE</version></dependency>编写 RPOP + LPUSH 的 Lua 脚本,Lua 脚本名字暂称为 L_RPOP_LPUSH.lua,放在 resources/scripts/lua 目录下:
local key = KEYS[1]local value = https://www.isolves.com/it/sjk/Redis/2019-11-22/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)); }); }}
推荐阅读
- 常用排序算法之JavaScript实现
- 使用VPN“翻墙”违法吗?
- IOS超级签名原理及实现
- 江西绿茶品牌效应凸显 茶业实现产值36亿
- 无绳电熨斗好用吗 无绳电熨斗使用方法
- 脱盆的旧土能养花吗?如何改良重复使用,教你几招
- 生态板裂纹还会继续裂吗,生态板表面开裂可以使用吗
- 使用激素快速祛痘 关于祛痘的4大误区不要中招了
- 暖脚器使用方法及注意事项
- 水暖毯正确使用方法