ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java爬虫实战:银行理财产品数据采集与反爬策略全解析

Java爬虫实战:银行理财产品数据采集与反爬策略全解析 1. 项目概述与需求拆解1.1 这个项目解决什么问题先说结论这个java爬虫项目目标就是把银行理财产品的数据从公开页面抓下来存成结构化数据方便后续做收益对比、产品筛选或者监控提醒。银行理财数据有个特点——更新频繁、字段复杂、分散在各大银行官网和第三方理财平台。手动去查不仅慢而且容易漏。写个爬虫定时跑一遍把产品名称、发行银行、预期收益率、风险等级、起购金额、期限、募集期这些关键字段自动抓下来属于典型的用程序代替重复人工场景。我在接到这个需求的时候第一反应就是这活儿没那么简单。银行网站的反爬策略、数据渲染方式、字段命名差异每一样都是坑。所以这篇博文会把整个项目的设计思路、核心代码、踩坑实录完整拆开讲适合有一定Java基础、想入门爬虫或者正在做类似数据采集项目的朋友参考。1.2 为什么用Java而不是Python我知道现在一提爬虫大家第一反应就是Python。确实requests加BeautifulSoup写起来快Scrapy框架也成熟。但我这个项目最终选了Java有几个实际考量公司技术栈就是Java项目要接入现有的Spring Boot服务体系用Java写爬虫可以直接复用现有的HttpClient、JSON解析、数据库访问层不需要额外维护一套Python服务。并发控制更顺手银行理财数据源不止一个几个平台同时抓取是很常见的需求。Java的线程池、Future、CompletableFuture用起来非常成熟性能和可控性都更好。部署运维省心打成一个Jar包丢到服务器上就能跑跟现有的定时任务、监控告警系统无缝对接。当然如果你是个人学习、就抓一两个小网站Python确实上手更快。但涉及银行这种数据量大、反爬严格、需要长期维护的场景Java的工程化优势会越来越明显。1.3 核心技术点一览这个项目涉及的技术点我列了一张清单后面会逐个展开技术点用途难度HttpClient发送HTTP请求获取页面数据低Jsoup解析HTML提取DOM节点数据低Fastjson / Jackson解析JSON格式的接口返回低线程池多数据源并发抓取中代理IP池绕过IP频率限制中Spring Boot Quartz定时任务调度中MyBatis / JPA数据持久化存储低2. 数据源分析与抓取策略设计2.1 数据源选择的门道银行理财数据表面上看就是产品名称收益率这么简单实际抓起来会发现数据源分好几类每一类的抓取难度都不一样。第一类是银行官网直销页面。这类页面通常是最权威的但问题在于——有的银行是服务端渲染HTML里直接有完整数据这种最好抓有的银行是前端Vue/React渲染数据通过Ajax接口异步加载这种就需要找到背后的JSON接口还有的银行页面用了大量JS混淆连接口都藏得深。我实际调研下来国有大行的官网反爬相对宽松股份制银行次之城商行反而偶尔会有一些奇怪的验证逻辑。第二类是第三方理财平台。比如各类基金销售平台、理财超市它们把多家银行的产品聚合在一起数据格式统一、字段齐全抓取效率最高。但这类平台的反爬通常比银行官网严格因为它们靠数据吃饭对爬虫的容忍度很低。UA校验、IP频率限制、滑块验证样样都有。第三类是数据服务商的开放接口。有些平台提供官方API注册后就能按接口规范拉数据。这是最省事的方案但需要商务合作或者有账号权限不是所有场景都能用。我最后采用的策略是以银行官网为主以第三方平台为辅优先找JSON接口找不到再解析HTML。这样能最大程度平衡数据准确性和抓取可行性。2.2 抓取频率与调度策略爬虫的频率设计直接决定了你能不能长期稳定跑下去。我见过太多人一上来就高频抓取结果第二天IP就被封了然后跑来问怎么办。我的经验是对银行数据单源请求间隔不低于3秒单日单源抓取次数控制在合理范围。银行理财产品的更新频率本身就不高一天抓一次完全足够没必要没事就去骚扰人家服务器。如果你是做日内收益率波动的监控那可以上午一次、下午一次间隔拉长到几个小时。具体到定时调度我用的是Spring Boot集成Quartz。配置一个Cron表达式比如每天上午9点跑一次跑完自动入库然后发一条通知到企业微信或者钉钉群告诉我今日采集完成新增X条更新Y条。这套下来基本就属于无人值守的状态了。调度的Cron表达式长这样// 每天上午9点执行一次 Scheduled(cron 0 0 9 * * ?) public void dailyCollectTask() { collectService.executeAllSources(); }2.3 抓取策略的取舍做爬虫最忌讳的就是一个方案走到黑。我的习惯是给每个数据源都设计a、b两套方案。a方案是直接请求JSON接口。浏览器F12打开开发者工具Network面板里找XHR请求翻翻接口返回的数据结构。如果接口返回的是标准的JSON格式且没有加密参数那恭喜你这是最省力的路径。用HttpClient请求一下Fastjson解析一下数据就拿到了。b方案是解析HTML。如果接口加密了或者数据混在页面里没有独立接口那就退而求其次用Jsoup直接解析HTML页面。虽然效率低一点但胜在通用。实操中还有c方案就是用Selenium或Playwright模拟浏览器操作专门对付那种JS渲染严重、接口加密复杂的页面。这个方案最重资源消耗大速度慢但确实是最后一道保险。我一般只有在a、b方案都行不通的情况下才上它。3. 核心代码实现与踩坑实录3.1 HTTP请求模块的实现请求模块是整个爬虫的地基。别小看这一步请求头的伪装、超时时间的设置、重试机制的实现每一项都影响最终的抓取成功率。我用自己的代码为例先展示一个最基础的请求封装public class HttpUtil { private static final HttpClient HTTP_CLIENT HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); public static String get(String url) throws Exception { HttpRequest request HttpRequest.newBuilder() .uri(URI.create(url)) .timeout(Duration.ofSeconds(15)) .header(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36) .header(Accept, text/html,application/json) .header(Accept-Language, zh-CN,zh;q0.9) .GET() .build(); HttpResponseString response HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() 200) { return response.body(); } throw new RuntimeException(请求失败状态码 response.statusCode()); } }这里有几个细节值得说明UA一定要伪装成真实浏览器。有些网站对非浏览器UA直接拒绝服务返回403。我用的这个UA串是Chrome浏览器的标准UA大部分情况下都能过。如果遇到更严格的校验还需要补上Cookie、Referer等Header信息。超时时间一定要设置。银行网站的响应速度经常不稳定有时候一个页面能卡好几十秒。如果没有超时设置爬虫线程可能会被一个坏请求拖死连带整个任务队列都卡住。10秒连接超时、15秒读取超时是我多次测试后觉得比较合适的值。重试机制很有必要。网络抖动是常态遇到超时或者504不要立刻放弃重试2-3次往往就有惊喜。但要注意重试间隔要有退避策略比如第一次等2秒第二次等5秒第三次等10秒而不是死循环立刻重试。3.2 JSON解析与字段提取银行理财产品的JSON接口数据结构通常长这样{ code: 000000, message: success, data: { total: 156, list: [ { prdCode: ZZ120241001, prdName: 某某银行2024年第100期理财产品, yieldRate: 3.25, riskLevel: R2, startAmount: 10000, termDays: 180, saleStart: 2024-06-01, saleEnd: 2024-06-07 } ] } }用Fastjson解析这样的结构非常简单public ListFinancialProduct parseProductList(String jsonStr) { JSONObject root JSON.parseObject(jsonStr); if (!000000.equals(root.getString(code))) { throw new RuntimeException(接口返回异常 root.getString(message)); } JSONObject data root.getJSONObject(data); JSONArray list data.getJSONArray(list); return list.toJavaList(FinancialProduct.class); }有一点要注意银行接口的字段命名五花八门同一个预期收益率有的叫yieldRate有的叫expReturn有的叫yearIncomeRate。建议写一个字段映射工具类把不同来源的字段统一映射成我们自己的标准命名。还有字段值的格式也需要统一处理。比如收益率有的平台给的是百分比数值3.25有的给的是小数0.0325还有的给的是字符串3.25%。这些都需要在解析的时候做清洗统一。3.3 HTML解析兜底方案JSON接口行不通的时候就要靠Jsoup解析HTML了。我举一个例子假设银行页面的产品列表长这样div classproduct-item span classname某某理财产品/span span classrate3.10%/span span classrisk中低风险/span span classamount1万元起购/span /divJsoup解析代码public ListFinancialProduct parseHtml(String html) { Document doc Jsoup.parse(html); Elements items doc.select(.product-item); ListFinancialProduct list new ArrayList(); for (Element item : items) { FinancialProduct p new FinancialProduct(); p.setPrdName(item.selectFirst(.name).text().trim()); String rate item.selectFirst(.rate).text().replace(%, ).trim(); p.setYieldRate(new BigDecimal(rate)); p.setRiskLevel(parseRisk(item.selectFirst(.risk).text())); p.setStartAmount(parseAmount(item.selectFirst(.amount).text())); list.add(p); } return list; }经验之谈HTML解析最大的坑不是选择器写不出来而是网站的改版。银行页面经常优化调整有时候改一个class名你整个解析逻辑就废了。所以我的做法是解析逻辑单独封装页面结构发生变化时只需要改一个方法不需要动整个爬虫框架。另外一个坑是有的页面数据是分页加载的。你需要先解析出总页数然后循环请求每一页。这里要注意循环请求的间隔别太短每页之间至少间隔1-2秒否则很容易触发反爬。3.4 并发抓取设计多个数据源同时抓取用串行方式一个个跑太慢了。以我的经验3个以上数据源就应该考虑并发。Java实现并发抓取我推荐用线程池加Future代码写起来干净可控性好public ListFinancialProduct concurrentCollect() throws Exception { ExecutorService pool Executors.newFixedThreadPool(4); ListCallableListFinancialProduct tasks new ArrayList(); tasks.add(() - collectFromBankA()); tasks.add(() - collectFromBankB()); tasks.add(() - collectFromPlatformC()); ListFutureListFinancialProduct futures pool.invokeAll(tasks); ListFinancialProduct result new ArrayList(); for (FutureListFinancialProduct future : futures) { try { result.addAll(future.get()); } catch (ExecutionException e) { log.error(某个数据源采集失败, e.getCause()); } } pool.shutdown(); return result; }这里有几个容易踩的坑线程池核心线程数不要贪多。有些新人一看有5个数据源就开10个线程结果把对方网站请求压力搞得很大IP很快被限制。我的经验是线程数不要超过数据源数量一般4到6个就够了。爬虫这种IO密集型的任务线程太多反而会让自己机器的CPU上下文切换变高而且对目标站点的压力也不好。线程池参数需要结合Xxl-Job或Spring Task指定线程大小。如果是用Spring的Async记得单独定义一个线程池Bean不要用默认的SimpleAsyncTaskExecutor那个是来一个任务new一个线程完全不可控。Future.get会阻塞等待如果某一个数据源响应特别慢会拖慢整个流程。解决方案有两个一个是Future.get设置超时时间比如10秒另一个是用CompletableFuture配合allOf实现等待所有任务完成但有一个失败也不影响其他任务的结果收集。3.5 代理IP池的使用银行理财数据抓取IP被封的概率说大不大、说小不小。如果你就抓一两个数据源、频率也不高基本用不到代理。但如果你的爬虫要跑很多数据源、一天抓好几次那还是建议准备一个代理IP池。代理IP池的核心逻辑很简单public class ProxyManager { private QueueProxy proxyPool new ConcurrentLinkedQueue(); /** * 从代理服务商接口拉取可用IP放入本地队列 */ public void refreshProxies() { String apiUrl http://代理服务商提供的API地址; String result HttpUtil.get(apiUrl); ListProxy proxies parseProxyList(result); proxyPool.clear(); proxyPool.addAll(proxies); } public Proxy getProxy() { // 轮询取用用完放回 Proxy proxy proxyPool.poll(); if (proxy ! null) { proxyPool.offer(proxy); } return proxy; } }这里要提醒一句代理IP的质量比数量重要。有些免费代理列表看着一大堆实际用起来全是废的——要么连不上要么速度极慢用了反而拖垮整个抓取效率。我的建议是找稳定的付费代理服务商价格不贵省心很多。另外代理IP不是每请求一个页面就换一个那样反而容易触发异常检测。我的策略是同一个数据源整个抓取过程使用同一个代理IP除非被拒绝才切换。这样模拟的是一个固定IP的正常访问行为更不容易被识别。4. 数据入库与更新策略4.1 表结构设计抓到数据之后存什么、怎么存直接决定了后续的数据分析和报表展示是否方便。我用的表结构供大家参考CREATE TABLE t_financial_product ( id bigint(20) NOT NULL AUTO_INCREMENT, prd_code varchar(64) DEFAULT NULL COMMENT 产品代码, prd_name varchar(256) DEFAULT NULL COMMENT 产品名称, bank_name varchar(128) DEFAULT NULL COMMENT 发行银行, yield_rate decimal(8,4) DEFAULT NULL COMMENT 预期年化收益率/%, risk_level varchar(16) DEFAULT NULL COMMENT 风险等级, start_amount decimal(12,2) DEFAULT NULL COMMENT 起购金额/元, term_days int(11) DEFAULT NULL COMMENT 期限天数, sale_start date DEFAULT NULL COMMENT 募集开始日, sale_end date DEFAULT NULL COMMENT 募集结束日, source_url varchar(512) DEFAULT NULL COMMENT 来源页面, collect_time datetime DEFAULT NULL COMMENT 采集时间, update_time datetime DEFAULT NULL COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_prd_code (prd_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里要注意一个关键点产品代码的唯一索引设计。同一个理财产品每次抓取的收益率、募集期是变化的但产品代码是不变的。所以用prd_code作为唯一键可以方便后续做更新操作——如果prd_code已经存在就更新收益率和募集期如果不存在就插入新记录。4.2 增量更新与去重逻辑爬虫最忌讳的就是每次全量插入跑几天下来数据全重复了。我的更新策略是先查后插或直接更新public void saveOrUpdateProduct(FinancialProduct product) { FinancialProduct exist mapper.selectByPrdCode(product.getPrdCode()); if (exist null) { mapper.insert(product); } else { exist.setYieldRate(product.getYieldRate()); exist.setRiskLevel(product.getRiskLevel()); exist.setSaleStart(product.getSaleStart()); exist.setSaleEnd(product.getSaleEnd()); exist.setUpdateTime(new Date()); mapper.update(exist); } }这个逻辑看起来简单但有一个潜在问题并发环境下两个线程同时查到不存在然后同时插入就会触发唯一键冲突。解决方式有两种一种是给数据库操作用ON DUPLICATE KEY UPDATE语法直接交给数据库处理冲突INSERT INTO t_financial_product (prd_code, prd_name, bank_name, yield_rate, ...) VALUES (#{prdCode}, #{prdName}, #{bankName}, #{yieldRate}, ...) ON DUPLICATE KEY UPDATE yield_rate VALUES(yield_rate), risk_level VALUES(risk_level), update_time NOW()另一种是在Java代码里加分布式锁或者同步锁但考虑到爬虫这种场景数据库级别的冲突处理已经足够了。4.3 数据校验与清洗银行数据虽然是公开的但抓下来的数据也要做好校验避免脏数据污染整个数据库。我总结了几个常见的脏数据场景和处理方式收益率为空或为0。有些产品是浮动收益页面显示以实际收益为准没有具体数值。这种情况我建议不直接丢弃而是存一个特殊标记比如-1表示未知省得影响后续的均值计算。产品名称重复但代码不同。有的银行不同期次产品名称非常相似只有代码不同。这种情况下需要人工核对是名称真的重复还是代码规则不一致。我一般会用prd_code作为唯一标识名称为辅这样即使名称重复也不会影响数据质量。募集期已经结束的产品。抓到的产品可能已经过了募集期理论上不应该再展示给用户。我在入库前会做一个过滤sale_end小于当前日期的标记为已过期后续查询时默认不展示。5. 常见问题与反爬应对5.1 遇到403 Forbidden怎么办403是爬虫最常见的响应状态码之一通常意味着服务器拒绝了你的请求。我排查403的顺序是第一步检查UA有没有设置。很多初级爬虫失败就是忘了设置UA服务器一看请求头里没有浏览器标识直接拒绝。把UA改成真实浏览器的UA通常就能解决。第二步检查是否需要Cookie。有些网站首次访问需要在响应里种Cookie后续请求要带着Cookie才能访问。这种情况需要用HttpClient的CookieManager来管理Cookie或者手动从浏览器复制一份Cookie放在请求头里携带。第三步检查请求频率。如果之前一段时间请求太频繁IP可能已经被限制了。这时候暂停几分钟再试或者换代理IP。第四步如果以上都没用那大概率是需要Referer校验。有些网站会检查请求的来源地址如果Referer不是它自己的域名就会拒绝服务。把Referer设置成目标网站的首页地址很多问题就迎刃而解了。5.2 页面数据是JS动态渲染的怎么处理现在很多银行网站都是前后端分离架构页面数据靠JS异步加载。你直接请求URL拿到的HTML里面是一个空壳啥数据都没有。这种场景最关键的是找到它背后的数据接口。怎么找按F12打开开发者工具切到Network面板刷新页面然后筛选XHR请求。一个个看返回内容找到返回JSON数据的那个请求记录下它的URL、请求方式、参数列表。通常这个接口就是数据的真实来源。找到了接口之后如果请求参数很简单直接用HttpClient模拟就行。但如果参数里有加密字段比如sign、token之类的那就比较麻烦了。这时候有两条路一是通过代码模拟加密逻辑。但前提是你得能看懂它的加密算法——通常是MD5、SHA、Base64加盐之类。这个工作量说大不大说小不小看具体加密复杂度。二是直接上Selenium。Selenium完整驱动一个浏览器去访问页面等JS执行完再抓取数据从逻辑上规避了加密参数的问题。代价就是性能差、资源占用高适合数据量不大的场景。对于银行理财产品这种页面我个人的经验是大概一半的网站能找到公开无加密的JSON接口另外一半需要Selenium或者模拟加密。具体碰到什么情况看运气也看耐心。5.3 采集频率过高被限制IP这是我踩过最实在的坑了。刚开始做爬虫的时候心气高想着快点把数据抓完设置了很短的间隔1秒一次甚至更快。结果没跑几分钟IP就被封了搞得整个项目进度都延期。被限制之后怎么判断两个信号一是请求突然开始大量返回403或者验证码二是有些页面开始跳转到安全验证页面。如果出现这两种情况基本就是被盯上了。解决办法除了降低频率、换代理IP之外还有一个务实的思路和对方网站做朋友。有些银行的理财页面如果请求频率控制得当其实是允许数据访问的。我的经验是单源单IP的请求间隔保持在3-5秒每天抓取频率控制在几次以内长期运行基本不会出问题。5.4 编码乱码问题很多银行网站还是用的GBK或者GB2312编码。你用UTF-8解析中文全变乱码。解决方案很简单用Jsoup的话指定编码解析Document doc Jsoup.parse(html, GBK);如果是HttpClient拿到的字符串可以在获取响应的时候指定解码字符集HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));但这里有个坑服务器告诉你的charset可能和实际内容编码不一致。我的习惯是先用正则解析HTML中的charset声明如果解析不到再尝试GBK解析还不行就用UTF-8。做一个自动检测的编码处理工具算是比较稳妥的方案。6. 项目扩展与深度思考6.1 从单机爬虫到分布式爬虫当抓取数据源数量增加、单个数据源的页面数量增多时单机爬虫的性能瓶颈就显现出来了。分布式爬虫并不是一个新东西核心思路就是任务分发、并行执行、结果汇总。Java生态里可以用Redis做任务队列用一个Scheduler服务管理任务分发多台机器各自跑Worker节点执行抓取任务。每个Worker抓到的数据统一写入中心数据库或者消息队列。但这个方案对银行理财数据来说我认为是杀鸡用牛刀了。银行理财产品的数据源总共就那么多单机完全能扛住。分布式爬虫更适合那种几百万级URL的大规模数据采集场景比如电商全站商品抓取、社交媒体数据抓取。如果你只是爬银行理财数据单机加合理调度足够了。6.2 AI技术与爬虫的结合最近很多人讨论AI和爬虫的关系。我的看法是AI不是替代爬虫而是让爬虫变得更加智能。传统的爬虫是规则驱动的——你告诉他怎么解析他就怎么解析。但网页结构一变规则就失效。而基于大模型或机器学习的方式可以从大量页面样本中学习到数据的规律即使页面结构变化依然能准确抽取目标字段。这在处理银行理财这种字段命名不统一、页面结构多变的场景价值非常明显。我后续打算尝试的方案是用大模型做字段抽取的后备方案。当正则和JSON解析失效时把页面文本交给大模型让它把关键字段提取出来。虽然有一定成本但相比人工去适配网站改版效率提升是数量级的。不过这条路径还没完全跑通目前还处于实验阶段这里就不展开讲了。6.3 合规使用爬虫数据最后必须聊一聊合规问题。爬虫抓取数据有条件限制个别场景甚至可能踩法律红线。我的原则很简单第一只抓公开数据不绕过登录验证、不破解加密算法去获取非公开数据。理财产品信息本身是公开的抓取这类数据风险相对较低。第二抓取频率保持克制不给目标网站造成压力。把对方服务器搞挂了既对别人不负责对自己也没好处。第三抓到的数据自己分析使用可以不要把原始数据打包出售给做数据生意的人这是明确的高风险行为。关于银行理财数据还有一个值得注意的高级话题通过爬虫把数据积攒起来后可以做产品对比分析、历史收益率走势预测等增值服务。有投资分析需求的朋友会发现长期积累的理财数据非常珍贵这里面的价值远比抓一次看看要大得多。我自己的做法是把数据存到ES里配合Kibana做可视化分析一眼就能看出不同银行、不同期限产品的收益率分布情况这是手动去官网对比完全做不到的效率。也算是一点私心这个项目做到后面与其说是爬虫技术练习不如说是给自己搭建了一个理财信息聚合工具一举两得的事。
返回列表