使用Redis实现UA池
前提
最近忙于业务开发、交接和游戏,加上碰上了不定时出现的犹豫期和困惑期,荒废学业了一段时间。天冷了,要重新拾起开始下阶段的学习了。之前接触到的一些数据搜索项目,涉及到请求模拟,基于反爬需要使用随机的User Agent
,于是使用Redis
实现了一个十分简易的UA
池。
背景
最近的一个需求,有模拟请求的逻辑,要求每次请求的请求头中的User Agent
要满足下面几点:
- 每次获取的
User Agent
是随机的。 - 每次获取的
User Agent
(短时间内)不能重复。 - 每次获取的
User Agent
必须带有主流的操作系统信息(可以是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
依赖:
<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 = 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)
原文链接
- Github Page:http://throwable.club/2019/11/14/redis-in-action-ua-pool/
- Coding Page:http://throwable.coding.me/2019/11/14/redis-in-action-ua-pool/
使用Redis实现UA池的更多相关文章
- selenium、UA池、ip池、scrapy-redis的综合应用案例
案例: 网易新闻的爬取: https://news.163.com/ 爬取的内容为一下4大板块中的新闻内容 爬取: 特点: 动态加载数据 ,用 selenium 爬虫 1. 创建项目 scrapy ...
- Redis客户端连接池
使用场景 对于一些大对象,或者初始化过程较长的可复用的对象,我们如果每次都new对象出来,那么意味着会耗费大量的时间. 我们可以将这些对象缓存起来,当接口调用完毕后,不是销毁对象,当下次使用的时候,直 ...
- redis运用连接池报错解决
redis使用连接池报错解决redis使用十几小时就一直报异常 redis.clients.jedis.exceptions.JedisConnectionException: Could not g ...
- selenium在scrapy中的使用、UA池、IP池的构建
selenium在scrapy中的使用流程 重写爬虫文件的构造方法__init__,在该方法中使用selenium实例化一个浏览器对象(因为浏览器对象只需要被实例化一次). 重写爬虫文件的closed ...
- 14.UA池和代理池
今日概要 scrapy下载中间件 UA池 代理池 今日详情 一.下载中间件 先祭出框架图: 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件. - ...
- UA池和代理池
scrapy下载中间件 UA池 代理池 一.下载中间件 先祭出框架图: 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件. - 作用: (1)引擎 ...
- UA池和代理池在scrapy中的应用
一.下载中间件 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件. - 作用: (1)引擎将请求传递给下载器过程中, 下载中间件可以对请求进行一系 ...
- 爬虫开发13.UA池和代理池在scrapy中的应用
今日概要 scrapy下载中间件 UA池 代理池 今日详情 一.下载中间件 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件. - 作用: ( ...
- scrapy下载中间件,UA池和代理池
一.下载中间件 框架图: 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件. - 作用: (1)引擎将请求传递给下载器过程中, 下载中间件可以对请 ...
随机推荐
- 使用python实现数组、链表、队列、栈
引言 什么是数据结构? 数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成. 简单来说,数据结构就是设计数据以何种方式组织并存储在计算机中. 比如:列表,集合和字 ...
- Soc常见问题
SOC常见问题解答 1.SOC FPGA中的ARM是软核还是硬核?ARM核的外设是软核还是硬核? SOC FPGA 中的ARM核是硬核.所以简称HPS,Hardware Processor Syste ...
- Vue基础简介
目录 vue基础 一.导入vue 二.vue挂载点 三.vue变量 四.vue事件 五.vue文本指令 六.vue事件指令 七.vue属性指令 vue基础 一.导入vue 补充:vue的语句以及导入j ...
- dev gridcontrol简单的动态设置动态表头
1)使用BandedGridView控件(此处演示BandedGridView控件包含8个列)2)往BandedGridView控件里添加GridBand控件(此处演示添加了4个) 3)///设置添加 ...
- LeetCode刷题总结-哈希表篇
本文总结在LeetCode上有关哈希表的算法题,推荐刷题总数为12题.具体考察的知识点如下图: 1.数学问题 题号:149. 直线上最多的点数,难度困难 题号:554. 砖墙,难度中等(最大最小边界问 ...
- Microsemi Libero使用技巧——使用FlashPro单独下载程序
前言 在工程代码编译完成之后,如果需要给某个芯片下载程序时,或者是工厂量产烧录程序时,我们不需要把整个工程文件给别人,而只需要把生成的下载文件给别人,然后使用FlashPro就可以单独下载程序文件了. ...
- B站上传字幕问题解决
博客:blog.shinelee.me | 博客园 | CSDN B站上传字幕时,如果srt文件中出现如下空行,则会报错,仅上传了空行前的部分 于是写了个python脚本,如下: import pys ...
- skyline加载arcgis发布的wms服务
function AddWMSLayer(LayerName) {var _WMSUrl =“http://10.0.4.141:6080/arcgis/services/poss1/MapServe ...
- 林克的小本本之——记一些基础的linux命令
查看shell cat /etc/shells 查看系统支持的shell echo $SHELL 查看目前正在使用的shell 快捷键 Ctrl+a 跳到行首 Ctrl+e 跳到行尾 Ctrl+u 删 ...
- VS Code Remote,在服务器上开发程序,开启全新开发模式
一直使用Idea开发java 程序,头疼的是太太太占用内存了,笔记本电脑经常卡爆,在服务器开发的话又太麻烦,VS Code Remote的带来,解决了这一烦恼.下面来实战一下. VS Code Rem ...