使用Redis实现UA池的方案
最近忙于业务开发、交接和游戏,加上碰上了不定时出现的犹豫期和困惑期,荒废学业了一段时间。天冷了,要重新拾起开始下阶段的学习了。之前接触到的一些数据搜索项目,涉及到请求模拟,基于反爬需要使用随机的UserAgent,于是使用Redis实现了一个十分简易的UA池。
背景
最近的一个需求,有模拟请求的逻辑,要求每次请求的请求头中的UserAgent要满足下面几点:
- 每次获取的UserAgent是随机的。
- 每次获取的UserAgent(短时间内)不能重复。
- 每次获取的UserAgent必须带有主流的操作系统信息(可以是Uinux、Windows、IOS和安卓等等)。
这里三点都可以从UA数据的来源解决,实际上我们应该关注具体的实现方案。简单分析一下,流程如下:
在设计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依赖:
io.lettuce lettuce-core 5.2.1.RELEASE
编写RPOP+LPUSH的Lua脚本,Lua脚本名字暂称为L_RPOP_LPUSH.lua,放在resources/scripts/lua目录下:
localkey=KEYS[1] localvalue=redis.call('RPOP',key) redis.call('LPUSH',key,value) returnvalue
这个脚本十分简单,但是已经实现了循环队列的功能。剩下来的测试代码如下:
publicclassUaPoolTest{ privatestaticRedisCommandsCOMMANDS; privatestaticAtomicReference LUA_SHA=newAtomicReference<>(); privatestaticfinalStringKEY="UA_POOL"; @BeforeClass publicstaticvoidbeforeClass()throwsException{ //初始化Redis客户端 RedisURIuri=RedisURI.builder().withHost("localhost").withPort(6379).build(); RedisClientredisClient=RedisClient.create(uri); StatefulRedisConnection connect=redisClient.connect(); COMMANDS=connect.sync(); //模拟构建UA池的原始数据,假设有10个UA,分别是UA-0...UA-9 List uaList=Lists.newArrayList(); IntStream.range(0,10).forEach(e->uaList.add(String.format("UA-%d",e))); //洗牌 Collections.shuffle(uaList); //加载Lua脚本 ClassPathResourceresource=newClassPathResource("/scripts/lua/L_RPOP_LPUSH.lua"); Stringcontent=StreamUtils.copyToString(resource.getInputStream(),StandardCharsets.UTF_8); Stringsha=COMMANDS.scriptLoad(content); LUA_SHA.compareAndSet(null,sha); //Redis队列中写入UA数据,数据量多的时候可以考虑分批写入防止长时间阻塞Redis服务 COMMANDS.lpush(KEY,uaList.toArray(newString[0])); } @AfterClass publicstaticvoidafterClass()throwsException{ COMMANDS.del(KEY); } @Test publicvoidtestUaPool(){ IntStream.range(1,21).forEach(e->{ Stringresult=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脚本。
总结
以上所述是小编给大家介绍的使用Redis实现UA池的方案,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。