本文章未经许可禁止转载,禁止任何修改后二次传播!
1.前言
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
说实话,架构名字真难想,就随便起了个;我看论坛很少有架构方面的帖子,正好补充补充论坛内容池:),欢迎大家来讨论。祝吾爱越浓越好。
2.技术架构
XJ爬虫平台支持爬虫任务全生命周期管理,从任务创建、调度、执行,到数据存储、结果同步等环节,实现全流程的可视化动态配置和管控。
XJ爬虫平台基于Java研发,开发框架以SpringBoot+nacos+ELK为基础,采用微服务架构划分多个模块,核心包括由任务管理、数据采集、数据处理、风控对抗、动态渲染、手机群控等模块。整体业务流程为:通过平台任务模块下发任务,爬取模块根据任务配置获取任务数据以及风控配置,开始执行抓取逻辑,过程涉及到对抗统一由风控模块提前配置,并将抓取过程中遇到验证码,加密,设备指纹等对抗手段统一处理,最后完成数据加工、存储和分发等。
2.1.业务架构
大部分爬虫任务由需求方创建,通过任务调度下发到采集模块,采集模块获取对抗数据(代{过}{滤}理ip、打码、加密破解、模拟点击、hook、脚本执行、站点并发限制等参数),开始采集目标数据,最后将数据去重清洗后同步到下游,实现完整的闭环链路。
2.2.模块划分
如上图所示,平台采用微服务架构,根据功能职责划分为多个模块,降低系统复杂度。主要有以下模块:
+清洗模块:通过配置对爬取的数据统一进行 去重、清洗、数据同步、分发 等操作。
2.3.技术架构
XJ爬虫平台系统基于Java开发,开发框架采用springboot。依赖的主要中间件包括:
2.4.主要功能
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 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 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 渲染服务能力
渲染服务除基础的网页渲染外,还实现了验证码打码和模拟登录功能。
3.2.4 渲染效果对比
渲染服务支持页面JS执行,可以获取页面动态加载的内容,相对于使用http请求爬取拿到的内容更全,爬取内容覆盖率提升30%以上。
3.3.手机群控工具
随着业务的发展,移动端的爬取需求越来越多,很多数据也只针对移动端App开放。由于每个App的请求加密规则、风控规则都不一致,定制爬取开发成本很高。因此我选择自研群控方案,用来支持移动端内容的通用化抓取,以此减少逆向破解成本,降低爬虫移动端抓取成本。群控工具支持与移动端App进行交互控制,已实现移动端内容抓取、设备指纹管理制造机等功能,支撑各类移动端爬取需求 。
3.3.1 群控系统实现原理
XJ群控系统基于小米4 android6.0.1研发,兼容1+、谷歌等手机,支持android8、9系统版本。
XJ群控系统的交互流程如下所示:
关键点包括:
3.3.2 群控管理系统
为了降低群控工具的使用和管理成本,已将所有设备管理操作集成在XJ爬虫平台上,并集成了部分设备控制功能。
3.3.3 群控工具对比
一般群控是针对某类特定场景定制化开发,聚焦于实现批量控制功能,直接使用有一定的改造成本。XJ群控系统完全自研开发,,灵活适配各种业务。
XJ群控系统除实现批量控制,在系统稳定性和扩展性上也做了较多优化工作。首先支持根据业务进行资源隔离,根据业务分配设备资源,实现每个设备独立运行任务,互不干扰。其XJ群控系统实现心跳保活功能,可实现群控强保活,设备永不掉线。此外XJ群控系统也支持动态发包、执行脚本等功能,灵活性高。
4.总结
总的来说XJ爬虫平台的搭建极大的提高了爬虫任务开发效率,降低了研发及维护成本。风控、群控、动态编译、渲染服务等技术的引入,极大的扩展了爬虫系统的能力,更好的支撑XJ业务发展。
4.1. 平台价值
研发提效:
扩展性强:
4.2吹嘘阶段
这个平台也是经过自己不断的迭代,支持业务数据早已过亿,服务性能上完全吼得住,电商、舆情、APP、企业、游戏,相关业务目前都能够快速支持。当然平台还有很多需要去做的,比如 数据化、工具化,只有让研发速度不断加快,才能多赖论坛吹水。
5.引用开源
xxl-glue、xxl-job、guns