基于SpringBoot+WebMagic+Mybatis多数据源的Java爬虫工程化实践
简介一套基于 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技术栈的时候技术选型反而成了最头疼的事。今天要聊的这个项目标题已经说得很直白——基于SpringbootWebMagicMybatis多数据源。这是一个非常典型的Java后端爬虫工程化方案核心思路是抓取用WebMagic持久化交给Mybatis多数据源解决读写分离和异构存储的问题Springboot负责把所有东西串起来。这套组合适合谁适合那些已经有用Java技术栈、想把爬虫能力嵌入到现有业务系统里的团队也适合想从“写脚本爬数据”进阶到“写服务管数据”的开发者。我自己在这条路上踩了不少坑这篇就把完整的架构思路、实操配置、还有那些文档里不会写的问题排查经验一次说清楚。1. 项目整体设计与技术选型1.1 为什么是WebMagic而不是HttpClientJsoup很多人一提到Java爬虫第一反应就是HttpClientJsoup手撸一套。这确实没问题对于单页面的小任务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的setCycleRetryTimes和setRetryTimes一定要设置。如果你不设置重试次数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[classpromo]/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 groupIdus.codecraft/groupId artifactIdwebmagic-core/artifactId version0.9.0/version /dependency dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver archive: url: jdbc:mysql://127.0.0.1:3306/archive_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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 核心数据流转流程梳理整个系统的数据流转可以总结为五步任务管理后台发布抓取任务把起始URL写入任务表。Spider启动后PageProcessor解析页面提取结构化字段并通过page.addTargetRequests()发现列表页的翻页链接和详情页的链接。解析完成的数据暂存在ResultItems中随着Spider流程到达Pipeline。Pipeline拿到数据后调用Service层根据数据归属路由到对应数据源进行持久化。全部抓完后任务状态更新触发数据统计和数据质量校验环节。这个小链路看似简单但里面的失败补偿机制一定不能省。我们遇到的情况是列表页抓到了、详情页链接也提取了中间网络抖动导致部分详情页抓取失败。这时候光靠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(ListBizData 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码。这块在模糊数据规范化上提升明显缺点就是调用大模型有成本和延迟不能每条数据都走适合做异常数据兜底处理。关于大模型和爬虫的结合我个人的建议是不要指望大模型解决全部解析问题成本太高也太慢了。跑批的时候还是用规则解析遇到解析结果置信度低的部分交给大模型兜底这是性价比最高的组合方式。回到这个项目的标题SpringbootWebMagicMybatis多数据源这套组合不是说能帮你搞定所有爬虫场景但它确实是一个能落地、可维护、能扩展的Java工程化方案。整个链路里真正体现功力的地方往往不是那些框架API怎么用而是多数据源下事务怎么设计、异常数据怎么兜底、调度策略怎么做到挂了能续跑、任务怎么做到多站点互不干扰。把这些细节想清楚这个架构跑几年都能很稳。最后再分享一个操作性很强的经验爬虫项目上线后更要关注的不是抓了多少数据而是少了多少数据。我会在任务结束后做一次总数校验把Redis里的任务URL总量、实际入库数量、去重后的有效数量做对比任何一个数字对不上就说明链路里有问题这时候再配合日志排查效率会高很多。这套系统上线到现在靠的就是这个校验机制帮我们发现了不少隐藏问题。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →