请大家关注一下地球号,这个可以比较快的接收到最新的文章: Java量化系列(三十一) 别再手动整理板块数据了!Java 批量爬取某券商板块列表,3 种分类自动入库
你是否在做量化系统开发时,遇到过这些痛点?🤔 手动整理板块数据耗时耗力,容易遗漏最新板块;板块分类混乱(行业、概念、地域傻傻分不清),导致数据管理杂乱;手动更新板块信息不及时,影响策略回测准确性。
今天这篇系列第三十一篇,就彻底解决这些问题 —— 从板块,概念,地区 基础认知,到数据库设计,再到东方财富板块列表批量爬取实战(分页 + 接口解析 + 自动入库),全程聚焦技术实现,帮你搭建高效的板块数据自动化管理模块。所有内容均为技术学习用途,不涉及任何投资建议,可放心复用。
一、先搞懂:3 种核心板块分类(对应技术枚举设计)
在量化系统中,板块本质是 “时序数据的分类标签”,不同分类对应不同的分析维度。结合代码中的BKType枚举,核心分为 3 类,覆盖绝大多数分析场景:
| 分类类型 | 枚举值(code) | 核心含义 | 典型示例 |
|---|---|---|---|
| 行业板块 | 1(BK) | 按行业属性划分,反映某一行业的整体趋势 | 新能源、半导体、生物医药 |
| 概念板块 | 2(GN) | 按特定概念 / 主题划分,反映市场热点方向 | 人工智能、光伏、数字经济 |
| 地域板块 | 3(DY) | 按地域属性划分,反映区域经济相关趋势 | 长三角、粤港澳、京津冀 |
💡 技术设计亮点:通过BKType枚举统一管理分类,后续新增分类(如 “主题板块”)只需扩展枚举,无需修改核心业务逻辑,符合 “开闭原则”。
二、数据存储设计:板块信息表(stock_bk)结构解析
板块数据需要持久化存储,方便后续查询、筛选和关联分析。下面拆解stock_bk表的设计思路,适配 3 种分类的存储需求:
2.1 表结构核心字段说明
CREATE TABLE `stock_bk` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID', `code` varchar(10) DEFAULT NULL COMMENT '版块编码(唯一标识)', `name` varchar(30) DEFAULT NULL COMMENT '版块名称', `type` int DEFAULT '1' COMMENT '类型 1=板块 2=概念 3=地域(关联BKType枚举)', `hot_num` int DEFAULT '0' COMMENT '热度(用于排序展示)', `create_date` datetime DEFAULT NULL COMMENT '创建时间', `ths_code` varchar(10) DEFAULT NULL COMMENT '同花顺版块Code(多源数据兼容)', `ths_name` varchar(30) DEFAULT NULL COMMENT '同花顺版块Name(多源数据兼容)', -- 其他扩展字段(五行、成绩标记等,用于个性化分析) PRIMARY KEY (`id`), KEY `idx_stock_bk_1` (`code`) -- 编码索引,提升查询效率 ) ENGINE=InnoDB COMMENT='股票版块信息表';2.2 设计思路拆解
- 多源兼容:预留
ths_code、ths_name字段,支持东方财富、同花顺等多平台板块数据接入,避免单一数据源依赖; - 分类明确:
type字段直接关联BKType枚举,确保分类统一,后续查询 “某类板块” 时直接通过type过滤,高效便捷; - 可扩展性:
hot_num(热度)、five_xing(五行)等字段为扩展字段,可根据业务需求灵活使用(如按热度排序展示热门板块); - 实体映射:对应
StockBkDo实体类,通过 MyBatis-Plus 实现 ORM 映射,无需手动编写 SQL,提升开发效率。
三、重点实战:东方财富板块列表批量爬取(全程代码解析)
这是本文核心内容 —— 通过东方财富公开接口,实现板块列表的批量爬取,支持分页获取,自动解析数据并适配后续入库逻辑。
实体类: StockBkDo 和 BKInfo 一致
@Data@EqualsAndHashCode(callSuper=false)@TableName("stock_bk")publicclassStockBkDoimplementsSerializable{privatestaticfinallongserialVersionUID=1L;/** * id */@TableId(value="id",type=IdType.AUTO)privateIntegerid;/** * 板块编码 */@TableField("code")privateStringcode;/** * 板块名称 */@TableField("name")privateStringname;/** * 类型 1是板块 2是 概念 3是地域 */@TableField("type")privateIntegertype;/** * 板块顺序 */@TableField("hot_num")privateStringhotNum;/** * 板块五行 */@TableField("five_xing")privateStringfiveXing;/** * 五行原因 */@TableField("five_reason")privateStringfiveReason;/** * 成绩标记 */@TableField("score_type")privateIntegerscoreType;/** * 成绩标记原因 */@TableField("score_message")privateStringscoreMessage;@TableField("create_date")privateDatecreateDate;@TableField("ths_code")privateStringthsCode;@TableField("ths_name")privateStringthsName;@TableField(exist=false)privateStringwebUrl;}3.1 先分析:爬取接口核心信息
目标接口:
https://push2.eastmoney.com/api/qt/clist/get?cb=jinhaiyuejiang&pn=1&pz=50&po=1&np=1&fields=f12,f13,f14,f62&fid=f62&fs=m:90+t:2&ut=b2884a393a59ad64002292a3e90d46a5&_=提取关键词:
`https://push2.eastmoney.com/api/qt/clist/get?cb={0}&pn={1}&pz=50&po=1&np=1&fields=f12,f13,f14,f62&fid=f62&fs=m:90+t:2&ut=b2884a393a59ad64002292a3e90d46a5&_=`接口参数说明(关键!)
cb:回调函数名(用于 JSONP 跨域,代码中通过CB_BK常量定义);pn:页码(分页核心参数,从 1 开始递增);pz:每页条数(固定 50 条,接口限制);fields:需要返回的字段(f12=板块编码、f14=板块名称,对应后续解析字段);fs:筛选条件(m:90+t:2表示查询行业板块列表,若查询概念 / 地域需调整该参数)。
3.2 核心代码:分页爬取实现
/** * 批量获取全量板块列表(分页爬取东方财富接口) * @return 全量板块信息列表 */@OverridepublicList<BKInfo>findAllBkList(){try{booleanstopSearch=false;// 终止爬取标记intpage=1;// 起始页码List<BKInfo>allResultList=newArrayList<>();// 分页循环爬取:直到获取不到数据为止do{// 1. 拼接完整URL(替换回调函数名和页码)Stringurl=MessageFormat.format("https://push2.eastmoney.com/api/qt/clist/get?cb={0}&pn={1}&pz=50&po=1&np=1&fields=f12,f13,f14,f62&fid=f62&fs=m:90+t:2&ut=b2884a393a59ad64002292a3e90d46a5&_={2}",CB_BK,page,MyDateUtil.getTimezone()// 时间戳参数防缓存);try{// 2. 调用接口并解析数据List<BKInfo>currentPageList=parseBKInfoList(url,BKType.BK);// 3. 处理当前页数据if(!CollUtil.isEmpty(currentPageList)){allResultList.addAll(currentPageList);page++;// 页码递增,继续爬取下一页}else{stopSearch=true;// 无数据,终止爬取}// 4. 延迟1秒:避免高频请求被限制IP(反爬关键)ThreadUtil.safeSleep(1000);}catch(Exceptione){log.error("第{}页板块数据爬取出错",page,e);throwe;// 可根据需求调整:是否终止整体爬取}}while(!stopSearch);log.info("板块列表爬取完成,共获取{}条数据",allResultList.size());returnallResultList;}catch(Exceptione){log.error("全量板块列表爬取异常",e);returnCollections.emptyList();}}3.3 关键步骤:数据解析实现
爬取接口返回的是 JSONP 格式数据,需要先处理格式,再解析核心字段(板块编码、名称):
/** * 解析板块列表数据(JSONP格式转JSON,提取核心字段) * @param url 爬取接口URL * @param bkType 板块类型(对应BKType枚举) * @return 解析后的板块信息列表 */privateList<BKInfo>parseBKInfoList(Stringurl,BKTypebkType){try{// 1. 发送GET请求获取数据(携带请求头,模拟浏览器请求)Stringcontent=HttpUtil.sendGet(HttpClientConfig.proxyNoUseCloseableHttpClient(),url,buildDfHeaderMap()// 构建请求头(含Cookie,提升爬取稳定性));// 2. 处理JSONP格式:去掉回调函数包裹(如 cbName(...) → ... )Stringcb=getCb(bkType);content=content.substring(cb.length()+1);// 去掉"cbName("content=content.substring(0,content.length()-2);// 去掉");"// 3. JSON解析:提取data.diff中的板块列表JSONObjectjsonObject=JSONObject.parseObject(content);JSONObjectdata=jsonObject.getJSONObject("data");if(ObjectUtils.isEmpty(data)){returnCollections.emptyList();}JSONArrayjsonArray=data.getJSONArray("diff");if(jsonArray.size()<=0){returnCollections.emptyList();}// 4. 映射为BKInfo对象(后续可直接转StockBkDo入库)List<BKInfo>result=newArrayList<>(jsonArray.size());jsonArray.forEach(n->{JSONObjecttempObject=JSONObject.parseObject(n.toString());BKInfobkInfo=newBKInfo();bkInfo.setCode(tempObject.getString("f12"));// 板块编码bkInfo.setName(tempObject.getString("f14"));// 板块名称bkInfo.setType(bkType.getCode());// 板块类型编码result.add(bkInfo);});returnresult;}catch(Exceptione){log.error("板块数据解析异常",e);globalWebExceptionHandlerAspect.addException();returnCollections.emptyList();}}/** * 构建请求头(关键:模拟浏览器,提升爬取成功率) */private@NotNullMap<String,String>buildDfHeaderMap(){Map<String,String>headerMap=newHashMap<>();headerMap.put("Host","20.push2.eastmoney.com");headerMap.put("Cookie","东方财富的Cookie");returnheaderMap;}3.4 最后一步:数据入库(衔接 StockBkDo)
将解析后的BKInfo列表转换为StockBkDo,批量插入数据库:
/** * 板块数据批量入库 * @param bkInfoList 解析后的板块信息列表 * @return 入库成功数量 */publicintbatchInsertBkData(List<BKInfo>bkInfoList){if(CollUtil.isEmpty(bkInfoList)){return0;}// 转换为StockBkDo对象List<StockBkDo>bkDoList=bkInfoList.stream().map(bkInfo->{StockBkDobkDo=newStockBkDo();bkDo.setCode(bkInfo.getCode());bkDo.setName(bkInfo.getName());bkDo.setType(bkInfo.getType());bkDo.setCreateDate(newDate());// 创建时间// 其他扩展字段可根据需求补充returnbkDo;}).collect(Collectors.toList());// 批量插入(MyBatis-Plus方法)returnstockBkMapper.insertBatch(bkDoList);}四、技术要点:爬取稳定性与扩展性优化
4.1 稳定性优化(反爬 + 容错)
- 请求延迟:爬取每页后延迟 1 秒(
ThreadUtil.safeSleep(1000)),避免高频请求被封 IP; - 请求头伪装:携带
Host、Cookie等请求头,模拟浏览器行为,提升爬取成功率; - 异常容错:单页爬取失败时抛出异常(可根据需求调整为 “跳过当前页”),避免影响整体爬取;
- 时间戳防缓存:URL 拼接时间戳参数(
MyDateUtil.getTimezone()),避免获取缓存的旧数据。
4.2 扩展性优化(支持多类型板块爬取)
当前代码爬取的是 “行业板块”(fs=m:90+t:2),若要爬取 “概念板块”“地域板块”,只需调整fs参数:
// 爬取概念板块:fs参数改为 m:90+t:3StringgnUrl=MessageFormat.format("https://push2.eastmoney.com/api/qt/clist/get?cb={0}&pn={1}&pz=50&po=1&np=1&fields=f12,f13,f14,f62&fid=f62&fs=m:90+t:3&ut=b2884a393a59ad64002292a3e90d46a5&_={2}",CB_GN,page,MyDateUtil.getTimezone());// 爬取地域板块:fs参数改为 m:90+t:1StringdyUrl=MessageFormat.format("https://push2.eastmoney.com/api/qt/clist/get?cb={0}&pn=1&pz=50&po=1&np=1&fields=f12,f14&fid=f62&fs=m:90+t:1&ut=b2884a393a59ad64002292a3e90d46a5&_={2}",CB_DY,page,MyDateUtil.getTimezone());五. 避坑指南: 板块爬取的 5 个典型问题 ⚠️
- JSONP 格式处理错误:忘记去掉回调函数包裹(如
cbName(...)),导致 JSON 解析失败 → 严格按substring方法处理格式; - 高频请求被封 IP:未加延迟或请求头伪装 → 必须添加 1-2 秒延迟,携带真实 Cookie 和 Host;
- 分页终止条件错误:页码递增无限制 → 只有当 “当前页无数据” 时才终止爬取;
- 字段映射错误:把
f13当成板块编码(实际f12是编码) → 先通过浏览器调试接口,确认字段含义; - 数据库插入重复:未做去重处理 → 入库前先查询数据库,存在则更新,不存在则插入。
六、下期预告
下期主要讲解一下 板块,概念,地区 这些的区别,用于读者更好的理解。
私信回复: “板块获取” 获取到目前最新的 板块数据,直接导入到数据库中即可使用。
最后留个小问题:你在做量化系统时,还需要爬取哪些类型的分类数据?是否需要支持 “板块 - 数据” 关联关系的自动维护?欢迎在评论区留言讨论,后续将持续优化完善~
请大家关注一下地球号,这个可以比较快的接收到最新的文章: Java量化系列(三十一) 别再手动整理板块数据了!Java 批量爬取某券商板块列表,3 种分类自动入库