基于SpringBoot+WebMagic+Mybatis多数据源的Java爬虫工程化实践
2026/9/9 1:42:16 网站建设 项目流程

简介:一套基于 SpringBoot、WebMagic、MyBatis 与多数据源整合的 Java 爬虫框架,面向有 Web 基础、想系统学习爬虫工程化的开发者。工程完整演示 WebMagic 的 Downloader、PageModel、Pipeline 等模块如何与 SpringBoot 注入和事务管理结合,并给出 MyBatis 多数据源配置与切换方案。资源共 134 个文件,含 42 个 Java 源码、44 个 class 编译文件、16 个 XML 映射配置、多份 properties/yml 属性文件,以及 JSP、日志和构建描述等辅助文件,压缩包整体约 64.89MB,已有 1277 人学习。内容覆盖小说爬虫、图片爬虫、屏幕截图抓取等实例,附带数据源配置、ElasticSearch 工具类等工程化实现,便于读者对照源码快速搭建自己的分布式数据采集与多库存储项目。 做爬虫开发这几年,我最大的感悟是:业务方永远不在乎你到底用了什么框架,他们只关心能不能稳定地把数据拿回来、能不能扛住目标站点的反爬、能不能在数据量上来之后依然不崩。但真正落地一个爬虫系统,尤其是要融入公司现有Java技术栈的时候,技术选型反而成了最头疼的事。今天要聊的这个项目,标题已经说得很直白——基于Springboot+WebMagic+Mybatis+多数据源。这是一个非常典型的Java后端爬虫工程化方案,核心思路是:抓取用WebMagic,持久化交给Mybatis,多数据源解决读写分离和异构存储的问题,Springboot负责把所有东西串起来。这套组合适合谁?适合那些已经有用Java技术栈、想把爬虫能力嵌入到现有业务系统里的团队;也适合想从“写脚本爬数据”进阶到“写服务管数据”的开发者。我自己在这条路上踩了不少坑,这篇就把完整的架构思路、实操配置、还有那些文档里不会写的问题排查经验,一次说清楚。

1. 项目整体设计与技术选型

1.1 为什么是WebMagic而不是HttpClient+Jsoup

很多人一提到Java爬虫,第一反应就是HttpClient+Jsoup手撸一套。这确实没问题,对于单页面的小任务,HttpClient发请求、Jsoup解析HTML,代码量不大,逻辑也直观。但一旦任务多起来,你会发现自己在重复造轮子:URL管理、去重、重试、线程池调度、代理切换、结果持久化,这些每个爬虫都要面对的问题,如果每个项目都手写一遍,纯属浪费时间。

WebMagic这个框架我用了两年多,最大的感受是它把爬虫的骨架给你搭好了。它内部有四大组件:Downloader负责下载页面,PageProcessor负责解析页面和发现新链接,Scheduler负责URL调度和去重,Pipeline负责结果输出。你要做的只是写PageProcessor的解析逻辑和Pipeline的存储逻辑,剩下的线程池、重试、URL队列,框架都帮你处理了。更关键的是,WebMagic支持Spider的定制化,任务多了之后,我们可以针对不同的目标站配置不同的Downloader。

这套架构落到这个项目里,业务侧只需要关心两个问题:目标页面的解析规则是什么、抓下来的数据往哪个库写。其余的抓取调度,全部由WebMagic的Spider实例托管,这比用HttpClient手写至少省掉一半的模板代码。

1.2 多数据源不是炫技,是真实需求

这个项目里多数据源的引入,不是为了显得技术牛逼,而是被实际场景逼出来的。我们当时接手的数据源至少分三类:

数据描述存储需求
目标网站抓取的原始HTML快照量大、低频访问、不需要事务,适合走MongoDB或MySQL归档库
解析后的结构化业务数据高频读写、需要事务支持,主库承担
运营后台的统计报表数据只要读、对实时性要求不高,从库做读写分离

如果在Springboot里只配一个数据源,上述三个需求全拴在一个MySQL实例上,用不了多久就会出现连接池争抢、慢查询拖垮主库的情况。拆成多数据源之后,原始数据写进归档库,解析结果写进主库,统计查询走从库,职责分离,互不干扰。

在技术实现上,我们选的是Springboot + Baomidou的dynamic-datasource-spring-boot-starter,这是一个基于AbstractRoutingDataSource做的多数据源切换组件。它的核心原理是用ThreadLocal保存当前线程需要使用的数据源key,在执行SQL前动态切换到对应的DataSource。实测下来,这套方案配置简单、切换性能损耗几乎可以忽略,比自己在Spring里写死多个SqlSessionFactory要省心得多。

2. 核心细节解析与实操要点

2.1 抓取调度模块的隐藏学问

爬虫项目最容易翻车的地方,往往不是解析代码,而是调度策略。用WebMagic做调度时,光会用Spider.create(new MyPageProcessor()).addUrl("https://xxx").run()是不够的,真正线上跑起来有几个细节一定要处理好。

第一个是去重。WebMagic默认的Scheduler是QueueScheduler,基于内存的HashSet做URL去重,单机短任务没问题,但一旦任务挂了重启,这个去重集合就丢了。我建议自定义一个结合Redis的Scheduler,把已访问的URL存进Redis的Set里,key按任务名做前缀,这样爬虫重启后还能接着跑,不会重复抓取。

第二个是线程数和超时控制。WebMagic的thread()方法控制并发线程数,很多人以为开得越大越快,实测下来这是一个误区。目标网站通常有频率限制,线程数开大了反而会触发封IP。我们项目里的经验值是:中小型站点2-4个线程,大型站点且对方反爬较弱时可以开到8个,同时每个请求设置连接超时和读取超时。

Spider.create(new ProductPageProcessor()) .addUrl("https://target-site.com/list/1") .thread(4) .setScheduler(new RedisScheduler("127.0.0.1", 6379)) .addPipeline(new MysqlPipeline()) .start();

第三个是请求头模拟。WebMagic的Site对象可以配置User-Agent、Cookie等参数,我强烈建议每个目标站单独维护一个指纹池,至少要模拟PC端和移动端的UA交替切换,这比固定一个UA被识别出来的概率低得多。

注意:Site的setCycleRetryTimessetRetryTimes一定要设置。如果你不设置重试次数,WebMagic默认情况下遇到异常会直接丢弃这个URL,导致你丢失大量本可以正常抓取的数据。

2.2 解析流程中的典型难点

WebMagic的PageProcessor是每个爬虫的“大脑”,它的核心职责就是把下载器拿到的HTML转成结构化数据。在真实项目里,我一直遵循一个原则:解析逻辑要薄,业务清洗要迟。

什么意思?PageProcessor里只做最基础的字段提取和链接发现,复杂的数据清洗(比如字符串拼接、日期格式统一、金额单位转换)不在这个环节做,而是放到后续的Service层处理。这样做的原因有两点:第一,PageProcessor执行在Spider的线程池里,做太多耗时操作会拖慢整个抓取链路;第二,清洗规则经常变动,放在Service层改起来不用重新部署爬虫任务,只要改业务代码就行。

HTML解析用WebMagic内置的Xsoup工具,支持XPath和CSS选择器两种方式。我的习惯是能CSS就CSS,选择器的可读性比XPath好很多,后面维护的人看了不骂娘。举个例子,抓取商品价格,在页面里可能有多个候选位置,比如原价、折扣价、活动价,我通常会写多个选择器按优先级降级尝试,而不是只写一个写死。

String price = page.getHtml().css("span.price-now", "text").get(); if (price == null) { price = page.getHtml().xpath("//div[@class='promo']/span/text()").get(); } if (price == null) { // 记录解析失败日志,交给补偿任务处理 log.warn("price not found, url: {}", page.getUrl().get()); }

2.3 多数据源下的持久化策略与事务边界

Mybatis在单数据源下的事务管理很清晰,但一引入多数据源,“事务”就变成了一块烫手山芋。因为Spring的声明式事务默认只绑定在主数据源上,如果某个业务操作需要同时写主库和归档库,用@Transactional是管不住两个库的原子性的。

那怎么设计才能避免这个坑?我最终采用的策略是:职责拆库,不强求跨库强一致。核心业务数据写入主库时用本地事务保证;归档数据写入时走单独的服务方法,用自己的事务,并且允许失败重试。从概率上来说,归档库写入失败导致的后果远低于主库数据损坏,所以这种“最终一致”的设计完全够用。

@Service public class CrawlDataServiceImpl implements CrawlDataService { @DS("master") // 主库 @Transactional(rollbackFor = Exception.class) public void saveBizData(BizData data) { bizDataMapper.insert(data); } @DS("archive") // 归档库 public void saveRawHtml(RawHtml html) { rawHtmlMapper.insert(html); } }

Mybatis本身是多数据源切换的敏感点。使用Mybatis时,每个数据源都要有独立的SqlSessionFactory,如果直接共用,你会遇到Mapper执行到错误的库上的问题。dynamic-datasource这个starter帮我们把这一层封装好了,它通过AOP拦截@DS注解自动切换DataSource。注意一点:@DS注解的本质是给当前线程设置一个标识,事务一旦开启,连接就会绑定在当前线程上,所以@DS@Transactional不要写在同一个方法上,否则事务拦截器先执行,数据源切换就失效了。

3. 实操过程与核心环节实现

3.1 工程结构准备

动手写代码前,先把工程的模块划分梳理清楚。我习惯把爬虫项目按职责拆成四个package,这样无论后面是加采集源还是加存储目标,都能最小化改动。

com.example.crawler ├── config // 数据源配置、WebMagic组件配置 │ ├── DataSourceConfig.java │ └── WebMagicConfig.java ├── crawler // 爬虫核心包,放PageProcessor和SpiderBuilder │ └── product │ └── ProductPageProcessor.java ├── entity // 数据实体 ├── pipeline // Pipeline持久化实现 │ └── ProductMysqlPipeline.java ├── mapper // Mybatis Mapper接口 └── service // 业务层,数据清洗、多库写入编排

工程的基础依赖很简单,核心就是:spring-boot-starter-web(提供http能力)、webmagic-core(抓取框架)、mybatis-spring-boot-starter、dynamic-datasource-spring-boot-starter,以及数据库驱动。

<dependency> <groupId>us.codecraft</groupId> <artifactId>webmagic-core</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.6.1</version> </dependency>

有一点要提前提醒:WebMagic 0.9.0是Maven中央仓库里的稳定版,它内部依赖了老版的commons-lang3,在Springboot 2.x及以上工程里偶尔会有类冲突。遇到的时候作排除依赖处理,不要把版本盲目调高或者降级,否则会遇到不可预知的序列化问题。

3.2 多数据源配置详解

配置多数据源,我用的是YAML方式。dynamic-datasource要求主数据源必须命名为primary,其他数据源可以任意取名,但要有明确的语义化命名,这样切库的时候看@DS注解就知道数据落在哪。

spring: datasource: dynamic: primary: master strict: true datasource: master: url: jdbc:mysql://127.0.0.1:3306/biz_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver archive: url: jdbc:mysql://127.0.0.1:3306/archive_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver report-read: url: jdbc:mysql://127.0.0.1:3307/report_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: readonly_user password: readonly123 driver-class-name: com.mysql.cj.jdbc.Driver

配置里这个strict: true很关键。它的作用开启后,如果你用了一个不存在的数据源名称,启动阶段就会直接报错;如果不开,运行时会静默切到主数据源,等你发现数据写错库了,排查又要白费半天。生产环境务必把strict打开,开发阶段倒可以关掉省得折腾。

3.3 WebMagic核心组件接线

配置好数据源之后,最核心的工作就是把WebMagic的抓取链路接到Springboot的IOC容器里。这有一个很多新手特别容易犯的错误:直接在PageProcessor里用@Autowired注入Mapper或者Service。我先说结论:这行不通。因为WebMagic的PageProcessor是内部通过反射方式创建的,Spring容器管理不到它的生命周期,你在里面注入的Bean会是null。

正确做法是:先在Spider构建之前,把依赖通过构造方法传给PageProcessor和Pipeline,把这些组件都声明成Spring的Bean,让Spring来管理依赖。下面是我项目里一个简化后的Pipeline写法。

@Component public class ProductMysqlPipeline implements Pipeline { private final BizDataMapper bizDataMapper; public ProductMysqlPipeline(BizDataMapper bizDataMapper) { this.bizDataMapper = bizDataMapper; } @Override public void process(ResultItems resultItems, Task task) { BizData data = resultItems.get("bizData"); if (data != null) { bizDataMapper.insert(data); } } }

Spider构建的时候,从这个Pipeline的Bean注入即可,整个生命周期都被Spring管理,事务和动态数据源都能正常生效。

再分享一个我在抓取调度上的经验。单站点用单个Spider实例没问题,但如果是多个站点同时采集,建议每个站点单独new一个Spider实例,不要共用一个Spider。因为WebMagic的采集线程池是绑定在Spider实例上的,混用会导致不同站点共享请求队列和去重集合,一旦某个站点下载慢,拖垮的是所有站点的抓取速度。实现上我是在Springboot启动后遍历任务配置列表,为每个任务创建独立的Spider实例,并通过线程池调度启动。

3.4 核心数据流转流程梳理

整个系统的数据流转可以总结为五步:

  1. 任务管理后台发布抓取任务,把起始URL写入任务表。
  2. Spider启动后,PageProcessor解析页面,提取结构化字段,并通过page.addTargetRequests()发现列表页的翻页链接和详情页的链接。
  3. 解析完成的数据暂存在ResultItems中,随着Spider流程到达Pipeline。
  4. Pipeline拿到数据后,调用Service层,根据数据归属路由到对应数据源进行持久化。
  5. 全部抓完后,任务状态更新,触发数据统计和数据质量校验环节。

这个小链路看似简单,但里面的失败补偿机制一定不能省。我们遇到的情况是:列表页抓到了、详情页链接也提取了,中间网络抖动导致部分详情页抓取失败。这时候光靠WebMagic自带的重试机制不够,需要在抓取完成后做一次“缺漏补偿”:把任务的基础数据和预期数量做对比,找出缺失的URL重新爬一遍。这里就会用到Redis队列的备份列表,也是我在调度模块里一直强调去重要上Redis的原因。

4. 常见问题与排查技巧实录

4.1 多数据源切换失效的真相

我刚开始做多数据源的时候,遇到的最典型问题就是:方法上明明加了@DS("archive"),为什么数据还是写到了主库?排查到最后发现,几乎都是同一个原因——在调用入口上加了@Transactional

Spring的事务管理器在事务开启的时候,会先判断当前线程有没有绑定的数据源连接,有就直接复用,没有就绑定默认的主数据源。如果你把@Transactional放在Controller或Service的入口方法上,Spring的事务拦截器先执行,它会把主数据源的连接绑定到当前线程,等到进入@DS标识的方法时,连接已经绑死了,注解自然不生效。

解决方法是把事务的粒度缩小:让带@DS的方法自己处理自己的事务,或者直接去掉外层的事务,改为业务逻辑里手动控制。如果实在需要跨库一致性,那么不要依赖本地事务,考虑引入消息表做最终一致。

场景推荐方案
单库内多个写操作方法内直接@Transactional,去掉@DS或确保@DS@Transactional在同一个方法上且@DS在方法上
多库间写操作拆分事务,核心库强一致,归档库异步弱一致
纯查询操作不需要事务,只加@DS切到对应读库即可

4.2 当表不存在的自动建表

“springboot +mybatis 当表不存在自动建表”这个话题在社区里讨论度很高。早期Mybatis本身不提供建表能力,如果用的MySQL,我一般直接在项目启动时用JDBC连接读取information_schema.TABLES判断是否存在目标表,不存在就执行预先写好的建表SQL。这里有个容易踩的坑:多数据源环境下,连接池初始化时并不实际创建Connection,所以启动检查的代码要拿到一个真实连接后再执行。

@Component public class TableInitializer implements ApplicationRunner { private final DataSource masterDataSource; @Override public void run(ApplicationArguments args) throws Exception { try (Connection conn = masterDataSource.getConnection()) { DatabaseMetaData meta = conn.getMetaData(); try (ResultSet rs = meta.getTables(null, null, "biz_data", null)) { if (!rs.next()) { // 执行建表DDL } } } } }

如果用了Mybatis-Plus,事情就简单多了,它的@TableName配合自动建表插件或者db-init配置就能实现,但要注意多数据源环境下,动态数据源切换后默认连接还是主库,一定要在代码里显式指定数据源去扫描,否则建表建到了错误的实例上。

4.3 WebMagic抓取结果为空或乱码

页面解析结果为空,在爬虫里十有八九是因为页面内容不是服务端渲染的。WebMagic默认使用HttpClient下载页面,对于Ajax渲染的页面,拿到的HTML里根本没有你要的数据。这时候有两个方向:如果页面里有JSON接口,直接用HttpClient拼接参数调用接口拿数据,这种效率高很多;如果必须要渲染完的页面,就得引入Selenium或Playwright做动态渲染下载器。

我遇到的多的是乱码问题。WebMagic默认读取页面声明的charset,但有些站点的响应头不写编码,或者声明了但不标准,结果就是中文变成乱码。这种情况下在Site配置里指定编码是个简单办法,但治标不治本,最稳的还是自定义Downloader,在获取页面内容后先做一次编码探测,再统一转成UTF-8。

public class CharsetDownloader extends HttpClientDownloader { @Override protected Page handleResponse(Request request, String charset, HttpResponse httpResponse) throws IOException { byte[] content = IOUtils.toByteArray(httpResponse.getEntity().getContent()); String html = CharsetDetector.detectAndConvert(content); Page page = new Page(); page.setRawText(html); page.setRequest(request); page.setStatusCode(httpResponse.getStatusLine().getStatusCode()); return page; } }

4.4 Mybatis批量插入的性能调优

爬虫产生的数据往往不是一条条落库的,而是一个批次一个批次地写。Mybatis的批量插入有个经典问题:如果用foreach拼接几千条INSERT语句,SQL的长度会超出MySQL的max_allowed_packet限制,直接报错。我采用的方案是分批插入,每批500-1000条,配合Mybatis的ExecutorType.BATCH模式,实测写入性能比逐条插入高出数倍。

@DS("master") public void batchInsert(List<BizData> list) { SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); try { BizDataMapper mapper = sqlSession.getMapper(BizDataMapper.class); for (BizData data : list) { mapper.insert(data); } sqlSession.flushStatements(); sqlSession.commit(); } finally { sqlSession.close(); } }

需要注意,多数据源下用ExecutorType.BATCH时,一定要确保打开的SqlSession对应的是目标数据源的工厂,否则还是白切。所以这种场景建议优先用编程式事务,不要用声明式@Transactional,这样对连接的管控更精确。

5. 多数据源库表规划与爬虫数据治理

5.1 库表设计的最佳实践

多数据源方案解决了“写到哪”的问题,但“怎么分”才是架构里更该想的。爬虫这个场景下,我有一个很深的体会:数据要分层存储,但也别分得太细。我们之前遇到一个合作方,把原始快照、解析结果、清洗结果、报表汇总拆成了四个库,买了一大堆机器资源,实际用到一半都不到。

按我现在的实践,建议三库就够了:

库名称表规划特点
master主库业务表、任务表、调度记录表数据量可控,支持事务,承载系统核心状态
archive归档库原始HTML快照、抓取日志、请求日志数据量大但无事务需求,定期清理或归档
report读库统计表、维度汇总表供后台报表查询,只读,屏蔽主库压力

表设计上的几个常见决策,我也直接给结论:字符串类型字段建议用varchar,不要图方便全部text;时间字段统一存datetime,不要存字符串,否则后面做时间范围查询都是坑;冗余字段可以多用,但别在爬虫表里做大量join,爬虫数据的核心是写入吞吐,不是查询优雅。

5.2 数据质量校验与清洗流程

爬虫抓回来的数据,能否直接进业务库,一定要经过清洗层的检查。我见过太多项目:抓完直接落库,过几天业务方跑报表发现数据对不上,然后再写一堆SQL补数据。这种修修补补的成本远高于落地清洗逻辑的成本。

清洗流程里有几条铁律:

  • 必填字段缺失(比如标题、URL为空)直接丢弃,记录日志但不入库。
  • 数字字段要做类型强转和范围校验,比如价格不能是负数,销量不能大于一个合理阈值。
  • 时间字段统一格式,站点给的“5分钟前”这类相对时间要先换算成绝对时间再入库。
  • URL要规范化和去参,比如统一去掉utm_source这类统计参数,避免同一条数据因为URL参数不同重复入库。

清洗逻辑放在Service层,用Java写肯定比在SQL里写维护性高很多。如果数据量到千万级别,可以再考虑引入流处理框架去做清洗,项目早期完全没必要。

6. 结合大模型的爬虫新玩法

最近社区里“大模型逆向爬虫”这个词比较火,我在这个项目里也做了一些尝试。传统爬虫的解析规则靠人写XPath或正则,站点改版就要跟着改,维护成本很高。大模型可以帮我们做两件事:

第一,自动生成和修复解析规则。把页面的HTML片段和期望提取的字段样例喂给模型,让它输出对应的XPath或CSS选择器。实测粗规则生成准确率不错,但复杂页面还是需要人工校验调整,它更像是帮我们减少试错次数,不是完全替代。

第二,语义化清洗。以前清洗一条商品数据需要写各种正则和枚举映射,现在直接让模型根据业务规则做标准化输出,比如把“一批”识别成20件,把“小码”映射成S码。这块在模糊数据规范化上提升明显,缺点就是调用大模型有成本和延迟,不能每条数据都走,适合做异常数据兜底处理。

关于大模型和爬虫的结合,我个人的建议是:不要指望大模型解决全部解析问题,成本太高也太慢了。跑批的时候还是用规则解析,遇到解析结果置信度低的部分交给大模型兜底,这是性价比最高的组合方式。

回到这个项目的标题,Springboot+WebMagic+Mybatis+多数据源这套组合,不是说能帮你搞定所有爬虫场景,但它确实是一个能落地、可维护、能扩展的Java工程化方案。整个链路里真正体现功力的地方,往往不是那些框架API怎么用,而是多数据源下事务怎么设计、异常数据怎么兜底、调度策略怎么做到挂了能续跑、任务怎么做到多站点互不干扰。把这些细节想清楚,这个架构跑几年都能很稳。

最后再分享一个操作性很强的经验:爬虫项目上线后,更要关注的不是抓了多少数据,而是少了多少数据。我会在任务结束后做一次总数校验,把Redis里的任务URL总量、实际入库数量、去重后的有效数量做对比,任何一个数字对不上就说明链路里有问题,这时候再配合日志排查,效率会高很多。这套系统上线到现在,靠的就是这个校验机制,帮我们发现了不少隐藏问题。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询