19

使用Redis实现UA池

 4 years ago
source link: http://www.throwable.club/2019/11/14/redis-in-action-ua-pool/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

背景

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

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

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

IfqUfq2.jpg!web

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

uyAveaM.jpg!web

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

具体实现方案

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

UA

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

VFjQFze.jpg!web

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

  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)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK