声明
本文章未经许可禁止转载,禁止任何修改后二次传播!
1.前言
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
说实话,架构名字真难想,就随便起了个;我看论坛很少有架构方面的帖子,正好补充补充论坛内容池:),欢迎大家来讨论。祝吾爱越浓越好。
2.技术架构
XJ爬虫平台支持爬虫任务全生命周期管理,从任务创建、调度、执行,到数据存储、结果同步等环节,实现全流程的可视化动态配置和管控。
XJ爬虫平台基于Java研发,开发框架以SpringBoot+nacos+ELK为基础,采用微服务架构划分多个模块,核心包括由任务管理、数据采集、数据处理、风控对抗、动态渲染、手机群控等模块。整体业务流程为:通过平台任务模块下发任务,爬取模块根据任务配置获取任务数据以及风控配置,开始执行抓取逻辑,过程涉及到对抗统一由风控模块提前配置,并将抓取过程中遇到验证码,加密,设备指纹等对抗手段统一处理,最后完成数据加工、存储和分发等。
2.1.业务架构
大部分爬虫任务由需求方创建,通过任务调度下发到采集模块,采集模块获取对抗数据(代{过}{滤}理ip、打码、加密破解、模拟点击、hook、脚本执行、站点并发限制等参数),开始采集目标数据,最后将数据去重清洗后同步到下游,实现完整的闭环链路。
2.2.模块划分
如上图所示,平台采用微服务架构,根据功能职责划分为多个模块,降低系统复杂度。主要有以下模块:
- 任务模块:任务的统一配置与调度,包括任务的创建、修改、删除、查询等功能,底层基于xxl-job框架实现任务调度,同时还增加任务计算功能保障抓取内容时效性。
- 爬取模块:处理所有任务抓取逻辑,承接上游下发任务,通过风控模块获取对抗参数配置,实现抓取解析,并将数据推送至下游。
- 风控模块:爬虫防反爬对抗能力配置化,对IP限制、加密规则、cookie、token、设备指纹等对抗手段进行统一管理。
- 渲染模块:模拟浏览器渲染页面,获取更全的页面动态内容。同时支持模拟登录,并利用opencv进行打码(滑块、图片旋转,短信等验证码)进行自动验证。
- 群控模块:对移动端抓取任务统一调度管理,处理移动端数据hook,模拟点击等数据采集操作。
+清洗模块:通过配置对爬取的数据统一进行 去重、清洗、数据同步、分发 等操作。
2.3.技术架构
XJ爬虫平台系统基于Java开发,开发框架采用springboot。依赖的主要中间件包括:
- 数据存储:mysql主要做数据持久化,es主要用于日志数据与部分业务数据存储(统计),redis主要做各个站点任务队列。
- 中间件:kafka主要用于爬虫内部模块数据传递,zk主要用于动态脚本逻辑更新广播通知,nacos主要用于服务注册与动态配置使用。
2.4.主要功能
- 风控策略动态配置:针对各个站点风控动态配置对抗规则,提高对抗效率,支持对ip,自动打码,请求头等爬取参数进行统一配置。
- 数据源对接配置:动态配置爬虫与需求方的数据交互方式,支持通过http或者kafka进行数据同步
- 动态任务配置
- 爬取站点配置:动态配置目标站点实现爬取逻辑脚本、爬取并发控制、数据判重规则等。
- 爬取任务创建:通过页面动态创建爬取任务,配置爬取逻辑。降低爬虫研发成本,使业务方更快实现抓取任务下发,并快速获得目标数据。
3.核心技术点
3.1.动态代码执行
3.1.1 引入背景
XJ爬虫平台基于Java语言实现,由于承接的业务不断增加,爬取任务的迭代也越来越频繁,传统的迭代模式下,每次添加爬取任务,都需要开发代码、发布上线,很难满足业务快速迭代的诉求。因此XJ爬取平台引入Java动态编译能力来支持爬虫任务的敏捷式开发流程。平台支持动态代码编译后,发布耗时几乎可以忽略不计,极大的提高了爬虫迭代能力以及业务响应能力。
3.1.2 动态编译原理
Java动态编译是指在运行时动态地将 Java 源代码编译成 Java 字节码,并加载到 JVM 中执行的过程。Java 动态编译通常使用 Java Compiler API 实现。通过 Java Compiler API,可以在运行时将 Java 源代码编译成字节码,然后使用 ClassLoader 动态加载字节码,并在 JVM 中执行。Java 动态编译的优点是可以在运行时动态生成代码,从而实现动态性和灵活性。在爬虫场景下可以通过动态编译生成的代码来执行新的爬取任务,而无需系统重启。
基于Java动态编译原理,XJ爬虫平台主要实现了一个定制的类加载器,其目的是在类加载时对每个爬虫脚本进行统一的增强,通过代码插桩,增加一些通用的处理逻辑,比如统一增加执行日志打印等,从而降低爬虫脚本的开发维护成本。XJ爬取脚本动态编译流程图如下所示:
自定义类加载器的实现片段示例代码如下所示:
// 项目启动时,从classPool获取到[groovy.lang.GroovyClassLoader$parseClass(String)]对应的方法声明将其替换成自定义的编译方式
// 主要目的是groovy编译方式不支持部分语法,且爬虫目前脚本研发脚本是继承父类统一管理,因此需要自定义编译方式,来进行一些代码插桩对子类logName和全局变量进行控制。
// 获取类池
ClassPool classPool = ClassPool.getDefault();
// 以当前线程对应的类加载器做为类加载器
classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
// 替换GroovyClassLoader.parseClass编译方法
CtClass groovyClassLoader = classPool.get("groovy.lang.GroovyClassLoader");
CtMethod declaredMethod = groovyClassLoader.getDeclaredMethod("parseClass", new CtClass[]{classPool.get("java.lang.String")});
// 设置自定义编译逻辑
declaredMethod.setBody("{return com.xxx.utils.AopUtil.ins($1);}");
groovyClassLoader.toClass();
3.1.3 脚本动态执行
爬虫脚本动态加载执行的基本实现过程如下,首先获取java源文件,利用自定义JavaStringCompiler 进行字节码转换。其次通过代码插桩对特殊方法进行增强处理后,再通过自定义类加载器得到可运行class对象。最后基于反射可对这些对象进行操作,从而执行爬取任务。
核心片段实现示例代码如下:
// AopUtil.ins
// 1.通过java字符串编译器将java源文件转换成
JavaStringCompiler compiler = new JavaStringCompiler();
// key=包名+类名,value=源文件编译后字节码
Map<String, byte[]> results = compiler.compile(className + ".java", source);
// 获取类池
ClassPool classPool = new ClassPool(false);
classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
// 将项目jar包加入类池的类路径中
for (String jarPath : JarUtil.jarsList) {
classPool.appendClassPath(jarPath);
}
// 添加当前类
results.forEach((k, v) -> classPool.appendClassPath(new ByteArrayClassPath(k, v)));
// 对固定方法插桩
for (CtMethod method : ctClass.getDeclaredMethods()) {
if (method.getName().equals("main")) {
continue;
}
boolean isStatic = Modifier.isStatic(method.getModifiers());
if (isStatic) {
continue;
}
if(method.getName().equals("xxxxx")){
method.insertBefore("逻辑代码插桩");
}
}
// 使用自定义类加载器进行类加载,完成类的动态编译加载
Class<?> aClass = getClassObject(packageName + "." + className, results, compiler);
--------------------------------------------------------------------------------------------------
// 通常来说jvm默认都是当前线程的类加载器来做完类的加载器,由于我们的脚本是要经过多次编译的,
// 所以我们固定一个脚本一个类加载器,从而避免内存泄漏问题
private static Class<?> getClassObject(String name , Map<String, byte[]> results , JavaStringCompiler compiler) throws Exception {
synchronized (javaNameLoadMap) {
MemoryClassLoader classLoader = null;i
f (javaNameLoadMap.containsKey(name)) {
log.info("name={} ,map中包含该classLoad 清理再重新new", name);
classLoader = javaNameLoadMap.get(name);
classLoader.close();
classLoader = null;
}
classLoader = new MemoryClassLoader(name, results);
javaNameLoadMap.put(name, classLoader);C
lass<?> aClass = compiler.loadClass(name, classLoader);re
turn aClass;
}
}
3.1.4 脚本在线编辑
平台支持对所有脚本进行在线编辑和管理,目前脚本支持的语言只有java、Kotlin,后续可以支持python,js等各种语言,降低开发成本。
爬虫脚本代码管理界面:
3.1.4 自动实时更新
管理后台编辑替换脚本后,基于ZK广播通知各个爬取节点,重新拉取最新脚本并重新编译替换,实现爬取服务自动更新,延迟毫秒级别基本可忽略。
3.2.浏览器渲染服务
随着爬虫对抗技术的不断升级,爬虫本身的研发维护成本逐渐上升,因此我也在不断研究 WEB端、移动端等通杀方式,以此降低站点破解开发成本,快速支持业务的稳定持续发展。通杀方案还在研发测试中。目前WEB端大部分用selenium,我专门对其性能以及浏览器指纹进行优化。
3.2.1 渲染服务架构
渲染服务作为底层的爬取资源,需满足高并发高可用的要求。为了方便扩容,渲染服务支持集群方式部署,对外暴露Http服务,基于NGINX将请求分发到不同节点。单个节点内实现资源池管理,并支持根据业务类型进行资源隔离,避免业务直接相互影响。
系统架构如下图所示:
3.2.2 渲染服务能力
渲染服务除基础的网页渲染外,还实现了验证码打码和模拟登录功能。
- 验证码打码服务:opencv、dddocr等基础验证码识别(支持点选、文字、方向、短信验证码等识别)
- 模拟登录:配置账号密码后自动页面操作实现登录,遇到验证码会自动进行打码。
3.2.4 渲染效果对比
渲染服务支持页面JS执行,可以获取页面动态加载的内容,相对于使用http请求爬取拿到的内容更全,爬取内容覆盖率提升30%以上。
3.3.手机群控工具
随着业务的发展,移动端的爬取需求越来越多,很多数据也只针对移动端App开放。由于每个App的请求加密规则、风控规则都不一致,定制爬取开发成本很高。因此我选择自研群控方案,用来支持移动端内容的通用化抓取,以此减少逆向破解成本,降低爬虫移动端抓取成本。群控工具支持与移动端App进行交互控制,已实现移动端内容抓取、设备指纹管理制造机等功能,支撑各类移动端爬取需求 。
3.3.1 群控系统实现原理
XJ群控系统基于小米4 android6.0.1研发,兼容1+、谷歌等手机,支持android8、9系统版本。
XJ群控系统的交互流程如下所示:
关键点包括:
- 事件监听:利用android自带无障碍事件对手机各类事件实现监听,并通过事件指令达到对手机的操作与控制
- 指令接收:根据指令做不同的操作,指令类型(shell命令、上传命令、下载命令、安装命令、执行脚本、更新程序、重启、截图、卸载、启动任务、停止任务,测试命令,脚本发送等
- 客户端保活方式:
- 将Service设置为前台服务而不显示通知
- 在 Service 的 onStartCommand 方法里返回 START_STICKY
- 覆盖 Service 的 onDestroy/onTaskRemoved 方法, 保存数据到磁盘, 然后重新拉起服务
- 监听 8 种系统广播 :在网络连接改变, 用户屏幕解锁, 电源连接 / 断开, 系统启动完成, 安装 / 卸载软件包时拉起 Service
- 开启守护服务 : 定时检查服务是否在运行,如果不在运行就拉起来
- 守护 Service 组件的启用状态, 使其不被 MAT 等工具禁用
3.3.2 群控管理系统
为了降低群控工具的使用和管理成本,已将所有设备管理操作集成在XJ爬虫平台上,并集成了部分设备控制功能。
3.3.3 群控工具对比
一般群控是针对某类特定场景定制化开发,聚焦于实现批量控制功能,直接使用有一定的改造成本。XJ群控系统完全自研开发,,灵活适配各种业务。
XJ群控系统除实现批量控制,在系统稳定性和扩展性上也做了较多优化工作。首先支持根据业务进行资源隔离,根据业务分配设备资源,实现每个设备独立运行任务,互不干扰。其XJ群控系统实现心跳保活功能,可实现群控强保活,设备永不掉线。此外XJ群控系统也支持动态发包、执行脚本等功能,灵活性高。
4.总结
总的来说XJ爬虫平台的搭建极大的提高了爬虫任务开发效率,降低了研发及维护成本。风控、群控、动态编译、渲染服务等技术的引入,极大的扩展了爬虫系统的能力,更好的支撑XJ业务发展。
4.1. 平台价值
研发提效:
- 相比传统多项目维护转变为多脚本维护,研发不再需要熟悉完整项目即可参与业务开发,降低开发门槛。
- 通过配置爬取脚本及任务的方式,大大降低爬取任务的代码量,一个站点一个项目到现在可以减少近60倍代码量(从老的项目改造后对比发现);大大减少了重复造轮子的情况。
- 通过平台各项配置工具,脚本管理成本降低20%。脚本管理功能为新任务的开发和维护增加了灵活性,开发效率提高30%以上。
扩展性强:
- 对比外部平台crawlab、eimiCrawler、Spiderman、crawler4j来说,XJ爬虫平台 不仅支持爬虫管理,任务管理等基础功能,也更加符合敏捷式开发流程,助力爬虫快速迭代。
- 在脚本研发语言上外部平台只能支持一种研发语言,而XJ爬虫平台支持的语言有java、kotlin等,同时支持扩展新语言。
- 增加风控模块、群控功能,将二者融入业务使其成为一环,通过风控与群控的加持,极大的丰富爬虫效率与对抗手段。
4.2吹嘘阶段
这个平台也是经过自己不断的迭代,支持业务数据早已过亿,服务性能上完全吼得住,电商、舆情、APP、企业、游戏,相关业务目前都能够快速支持。当然平台还有很多需要去做的,比如 数据化、工具化,只有让研发速度不断加快,才能多赖论坛吹水。
5.引用开源
xxl-glue、xxl-job、guns