在第四篇文章中,我们完成了自选股票维护功能,用户已能便捷管理核心关注标的。但对量化交易者而言,仅维护自选列表远远不够------还需实时掌握单只股票的关键信息,比如当前价格、涨跌幅度、成交量、市盈率等,这些数据是判断标的短期走势、执行策略信号的核心依据。
本文作为Java量化系列的第五篇,将聚焦"股票详细信息获取"模块,基于SpringBoot技术栈,实现两大核心目标:一是定义统一的股票详情数据模型(StockShowInfoDto),兼容不同数据源的字段映射;二是对接财联社、腾讯两大公开数据源,实现股票详情信息的实时爬取、解析与封装。同时会复用系列第三篇中的历史数据爬取基础代码,保证系统代码的复用性与一致性,适合有一定Java爬虫与量化开发基础的工程师参考。
一、核心需求与数据模型设计
1.1 核心需求边界
股票详细信息获取的核心诉求是"实时性""完整性""兼容性",结合量化交易场景,明确需求边界如下:
- 数据完整性:覆盖股票基础信息(代码、名称、全代码、交易所类型)、行情数据(开盘价、收盘价、最高价、最低价、当前价)、交易数据(成交量、成交金额、换手率、量比)、估值数据(市盈率)、辅助判断数据(涨跌幅度、是否涨停);
- 多数据源兼容:对接财联社、腾讯两大数据源,支持数据源切换(避免单一数据源失效导致功能不可用);
- 实时性保障:通过HTTP请求实时爬取数据源接口,确保获取的是最新行情数据(非历史缓存);
- 自动计算与封装:部分字段(如涨停状态、涨跌幅度百分比)需基于原始数据自动计算,简化上层调用;
- 异常兼容:爬取失败时返回null并记录日志,支持后续降级处理(如切换备用数据源)。
1.2 核心数据模型:StockShowInfoDto
为统一不同数据源的返回格式,定义StockShowInfoDto作为股票详细信息的标准输出模型,包含基础信息、行情数据、交易数据等全量字段,并通过自定义getter方法实现部分字段的自动计算(如涨停状态、涨跌幅度百分比转换)。
核心代码实现:
@Data
@Schema(description = "股票展示信息")
public class StockShowInfoDto implements Serializable {
/** 股票的代码(如001318) */
@Schema(description = "股票的代码")
private String code;
/** 股票的名称(如三峡能源) */
@Schema(description = "股票的名称")
private String name;
/** 当前查询日期 */
@Schema(description = "日期")
private String currDate;
/** 股票的全代码(如sz001318、sh600000) */
@Schema(description = "股票的全代码")
private String fullCode;
/** 交易所类型(0-未知/1-深交所/2-上交所,可自定义) */
@Schema(description = "交易所类型")
private Integer exchange;
/** 行情日期(当日) */
@Schema(description = "当前天")
private String date;
/** 开盘价 */
@Schema(description = "开盘价")
private BigDecimal openingPrice;
/** 昨天的收盘价 */
@Schema(description = "昨天的收盘价")
private BigDecimal yesClosingPrice;
/** 最高价格 */
@Schema(description = "最高价格")
private BigDecimal highestPrice;
/** 最低价格 */
@Schema(description = "最低价格")
private BigDecimal lowestPrice;
/** 收盘价(盘后有效,盘中取当前价) */
@Schema(description = "收盘价")
private BigDecimal closingPrice;
/** 当前的价格(盘中实时价格) */
@Schema(description = "当前的价格")
private BigDecimal nowPrice;
/** 成交量(股) */
@Schema(description = "成交量(股)")
private long tradingVolume;
/** 成交量金额(元) */
@Schema(description = "成交量金额")
private BigDecimal tradingValue;
/** 涨跌幅度(当前价-昨日收盘价) */
@Schema(description = "涨跌幅度")
private BigDecimal amplitude;
/** 涨跌幅度百分比(带%符号,如+2.35%) */
@Schema(description = "涨跌幅度百分比")
private String amplitudeProportion;
/** 换手率(%) */
@Schema(description = "换手率")
private BigDecimal changingProportion;
/** 量比 */
@Schema(description = "量比")
private BigDecimal than;
/** 市盈率(TTM) */
@Schema(description = "市盈率")
private BigDecimal peRatio;
/** 是否涨停(1-涨停/0-不涨停,自动计算) */
@Schema(description = "是否涨停 1为涨停 0为不涨停")
private Integer zt = 0;
/** 涨跌幅度百分比(double形式,用于计算,如2.35) */
@Schema(description = "涨幅 double形式")
private Double amplitudeProportionDouble;
/** 数据源网页地址(辅助信息) */
@Schema(description = "webUrl地址信息")
private String webUrl;
/**
* 自动计算是否涨停:调用StockUtil工具类判断
* 核心逻辑:根据股票代码(区分主板/创业板/科创板)、当前涨幅判断
*/
public Integer getZt() {
return StockUtil.isZt(this.code, this.name, this.amplitudeProportion) ? 1 : 0;
}
/**
* 自动转换涨跌幅度百分比为double:去除%符号,转为数值型
* 示例:"2.35%" → 2.35,空值返回0.0
*/
public Double getAmplitudeProportionDouble() {
if (StrUtil.isBlank(amplitudeProportion)) {
return 0.0d;
}
String substring = amplitudeProportion.replace("%", "");
return Double.parseDouble(substring);
}
}
关键设计亮点:
- 自动计算字段:通过自定义getter方法实现
zt(涨停状态)和amplitudeProportionDouble(涨跌幅度double值)的自动计算,无需上层代码处理,简化调用; - 字段兼容性:兼顾不同数据源的字段差异(如财联社返回"last_px"表示当前价,腾讯返回"nowPrice"),通过DTO统一封装,屏蔽底层差异;
- 辅助信息字段:
fullCode(全代码)用于数据源接口调用,webUrl用于记录数据源地址,便于问题排查。
二、核心设计:多数据源架构与流程
2.1 多数据源架构设计
为避免单一数据源失效导致功能不可用,设计"主备数据源"架构:以财联社作为主数据源(数据字段更完整,包含市盈率、量比等关键估值数据),腾讯作为备用数据源(接口稳定性高,适合降级使用)。核心架构如下:
上层调用 → 数据源选择器 → 主数据源(财联社)爬取 → 解析封装 → 结果返回;主数据源失败 → 自动切换备用数据源(腾讯) → 解析封装 → 结果返回;均失败 → 返回null+日志记录
2.2 核心流程拆解
无论主备数据源,核心流程均遵循"参数校验 → 构建请求(URL+请求头) → 发送HTTP请求 → 解析响应数据 → DTO封装 → 自动计算字段 → 结果返回",差异仅在于请求URL、请求头、响应数据格式的解析逻辑。
三、核心实现:多数据源爬取与封装
下面分别拆解财联社(主数据源)和腾讯(备用数据源)的完整实现逻辑,包含请求构建、数据爬取、解析封装等关键步骤,同时说明代码复用要点(如复用第三篇的腾讯历史数据爬取基础方法)。
3.1 主数据源:财联社股票详情爬取实现
3.1.1 核心依赖与工具类
需依赖HTTP工具类(HttpUtil)发送GET请求、JSON工具类(JSONUtil)解析响应数据、 BigDecimal工具类(BigDecimalUtil)处理数值计算,同时需配置财联社接口地址(通过配置文件注入,便于维护)。
3.1.2 完整代码实现
@Service
public class StockDetailServiceImpl implements StockDetailService {
@Override
public StockShowInfoDto getByCode(String fullCode) {
// 1. 参数校验:全代码不能为空(如sz001318、sh600000)
if (StrUtil.isBlank(fullCode)) {
log.error("财联社爬取股票详情失败:全代码为空");
return null;
}
try {
// 2. 构建请求URL:通过MessageFormat格式化占位符
String url = MessageFormat.format("https://x-quote.cls.cn/quote/stock/basic?secu_code={0}&fields=open_px,av_px,high_px,low_px,change,change_px,down_price,change_3,change_5,qrr,entrust_rate,tr,amp,TotalShares,mc,NetAssetPS,NonRestrictedShares,cmc,business_amount,business_balance,pe,ttm_pe,pb,secu_name,secu_code,trade_status,secu_type,preclose_px,up_price,last_px&app=CailianpressWeb&os=web&sv=8.4.6&sign=9f8797a1f4de66c2370f7a03990d2737", fullCode);
// 3. 构建请求头:模拟浏览器请求,避免被反爬(关键:需设置User-Agent等请求头)
Map<String, String> headerMap = buildDjrHeaderMap();
// 4. 发送HTTP GET请求:使用无代理的CloseableHttpClient,避免代理失效问题
String content = HttpUtil.sendGet(HttpClientConfig.proxyNoUseCloseableHttpClient(), url, headerMap);
// 5. 解析响应数据:JSON格式转为JSONObject
JSONObject jsonObject = JSONUtil.parseObj(content);
// 6. 提取核心数据节点:响应数据中"data"字段为股票详情核心数据
JSONObject data = jsonObject.getJSONObject("data");
if (data == null) {
log.error("财联社爬取股票详情失败:响应数据中data节点为空,fullCode={}", fullCode);
return null;
}
// 7. DTO封装:将JSON数据映射到StockShowInfoDto
StockShowInfoDto stockShowInfoDto = new StockShowInfoDto();
// 基础信息封装
stockShowInfoDto.setCode(StrUtil.subSufByLength(fullCode, 6)); // 从全代码中截取后6位作为股票代码(如sz001318→001318)
stockShowInfoDto.setName(data.getStr("secu_name")); // 股票名称
stockShowInfoDto.setCurrDate(DateUtil.now()); // 当前查询时间
stockShowInfoDto.setFullCode(fullCode); // 全代码(保留原始参数)
stockShowInfoDto.setDate(DateUtil.now()); // 行情日期(当日)
stockShowInfoDto.setWebUrl("金亥跃江聊量化"); // 辅助信息:数据源标识
// 行情数据封装(映射财联社响应字段)
stockShowInfoDto.setOpeningPrice(data.getBigDecimal("open_px")); // 开盘价
stockShowInfoDto.setYesClosingPrice(data.getBigDecimal("preclose_px")); // 昨日收盘价
stockShowInfoDto.setHighestPrice(data.getBigDecimal("high_px")); // 最高价
stockShowInfoDto.setLowestPrice(data.getBigDecimal("low_px")); // 最低价
stockShowInfoDto.setClosingPrice(data.getBigDecimal("last_px")); // 收盘价(盘中取当前价)
stockShowInfoDto.setNowPrice(data.getBigDecimal("last_px")); // 当前价
// 交易数据封装
stockShowInfoDto.setTradingVolume(data.getLong("business_amount")); // 成交量(股)
stockShowInfoDto.setTradingValue(data.getBigDecimal("business_balance")); // 成交金额(元)
stockShowInfoDto.setAmplitude(data.getBigDecimal("change_px")); // 涨跌幅度(当前价-昨日收盘价)
// 数值计算与格式化:涨跌幅度百分比(乘以100,保留2位小数,加%符号)
stockShowInfoDto.setAmplitudeProportion(BigDecimalUtil.mul100(data.getBigDecimal("change")));
// 估值与辅助数据封装
stockShowInfoDto.setChangingProportion(BigDecimalUtil.mul100B(data.getBigDecimal("tr"))); // 换手率(乘以100,保留2位小数)
stockShowInfoDto.setThan(BigDecimalUtil.mul100B(data.getBigDecimal("qrr"))); // 量比(乘以100,保留2位小数)
stockShowInfoDto.setPeRatio(data.getBigDecimal("ttm_pe")); // 市盈率(TTM)
// 自动计算涨停状态(调用DTO自定义getter方法,无需手动赋值)
stockShowInfoDto.setZt(stockShowInfoDto.getZt());
return stockShowInfoDto;
} catch (Exception e) {
log.error("财联社爬取股票详情失败,fullCode={}", fullCode, e);
// 主数据源失败,可在此处自动切换备用数据源(腾讯),此处简化处理,返回null
return null;
}
}
/**
* 构建财联社请求头:模拟浏览器请求,避免被反爬
*/
private Map<String, String> buildDjrHeaderMap() {
Map<String, String> headerMap = new HashMap<>();
headerMap.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
headerMap.put("Referer", "https://www.cls.cn/");
headerMap.put("Accept", "application/json, text/plain, */*");
return headerMap;
}
}
3.1.3 关键注意点
- 请求头构建:必须模拟浏览器请求头(尤其是User-Agent、Referer),否则财联社接口会拒绝返回数据(反爬机制);
- 全代码处理:财联社接口需要传入全代码(如sz001318),需从全代码中截取后6位作为股票纯代码(如001318),便于后续逻辑处理;
- 数值计算:涨跌幅度百分比、换手率、量比等字段需乘以100转为百分比格式,使用BigDecimalUtil工具类避免浮点数精度丢失;
- 异常处理:捕获所有异常(如HTTP请求失败、JSON解析失败、字段缺失等),记录详细日志(包含全代码),便于问题排查,同时为切换备用数据源预留扩展点。
3.2 备用数据源:腾讯股票详情爬取实现
3.2.1 代码复用要点
复用第三篇中的parseTxMoneyYesHistory方法(用于爬取腾讯股票历史数据),该方法已实现"构建腾讯接口URL、设置请求头、发送HTTP请求、获取响应数据"的核心逻辑,此处仅需新增"响应数据解析为TxStockHistoryInfo列表"和"TxStockHistoryInfo转StockShowInfoDto"的逻辑。
3.2.2 完整代码实现
@Service
public class StockDetailServiceImpl implements StockDetailService {
@Resource
private DefaultProperties defaultProperties; // 配置文件封装类,包含腾讯接口地址
@Resource
private DailyTradingInfoParse dailyTradingInfoParse; // 腾讯数据解析工具类
@Override
public StockShowInfoDto getNowInfo(String fullCode) {
// 1. 参数校验:全代码不能为空
if (StrUtil.isBlank(fullCode)) {
log.error("腾讯爬取股票详情失败:全代码为空");
return null;
}
try {
// 2. 复用第三篇的基础方法:爬取腾讯股票当前行情数据(返回TxStockHistoryInfo列表)
// 注:DateTime.now()表示获取当日数据,parseTxMoneyYesHistory方法已兼容当日实时数据爬取
List<TxStockHistoryInfo> txStockHistoryInfos = parseTxMoneyYesHistory(Collections.singletonList(fullCode), DateTime.now());
// 3. 校验解析结果:列表为空则返回null
if (CollUtil.isEmpty(txStockHistoryInfos)) {
log.error("腾讯爬取股票详情失败:解析后数据列表为空,fullCode={}", fullCode);
return null;
}
// 4. 获取单只股票数据(列表仅含一条记录)
TxStockHistoryInfo txStockHistoryInfo = txStockHistoryInfos.get(0);
// 5. TxStockHistoryInfo转StockShowInfoDto:字段映射+格式转换
StockShowInfoDto stockShowInfoDto = new StockShowInfoDto();
// 基础信息封装
stockShowInfoDto.setCode(txStockHistoryInfo.getCode()); // 股票代码
stockShowInfoDto.setName(txStockHistoryInfo.getName()); // 股票名称
stockShowInfoDto.setFullCode(fullCode); // 全代码
stockShowInfoDto.setExchange(0); // 交易所类型:腾讯接口未返回,暂设为0(可后续扩展解析逻辑)
stockShowInfoDto.setDate(DateUtil.now()); // 行情日期(当日)
// 行情数据封装(映射TxStockHistoryInfo字段)
stockShowInfoDto.setOpeningPrice(txStockHistoryInfo.getOpeningPrice()); // 开盘价
stockShowInfoDto.setYesClosingPrice(txStockHistoryInfo.getYesClosingPrice()); // 昨日收盘价
stockShowInfoDto.setHighestPrice(txStockHistoryInfo.getHighestPrice()); // 最高价
stockShowInfoDto.setLowestPrice(txStockHistoryInfo.getLowestPrice()); // 最低价
stockShowInfoDto.setClosingPrice(txStockHistoryInfo.getClosingPrice()); // 收盘价
stockShowInfoDto.setNowPrice(txStockHistoryInfo.getNowPrice()); // 当前价
// 交易数据封装
stockShowInfoDto.setTradingVolume(txStockHistoryInfo.getTradingVolume()); // 成交量(股)
stockShowInfoDto.setTradingValue(txStockHistoryInfo.getTradingValue()); // 成交金额(元)
stockShowInfoDto.setAmplitude(txStockHistoryInfo.getAmplitude()); // 涨跌幅度
// 数值格式化:涨跌幅度百分比转为带%的字符串(如2.35→"2.35%")
stockShowInfoDto.setAmplitudeProportion(BigDecimalUtil.toShowString2(txStockHistoryInfo.getAmplitudeProportion()));
stockShowInfoDto.setChangingProportion(txStockHistoryInfo.getChangingProportion()); // 换手率(已为百分比格式)
stockShowInfoDto.setThan(txStockHistoryInfo.getThan()); // 量比
stockShowInfoDto.setPeRatio(txStockHistoryInfo.getDynamicPriceRatio()); // 动态市盈率
// 涨停状态:腾讯数据未直接提供,暂设为0(可复用StockUtil工具类计算,此处简化处理)
stockShowInfoDto.setZt(0);
return stockShowInfoDto;
} catch (Exception e) {
log.error("腾讯爬取股票详情失败,fullCode={}", fullCode, e);
return null;
}
}
/**
* 复用第三篇的腾讯历史数据爬取基础方法:构建URL、发送请求、获取响应数据
* 核心逻辑:拼接股票代码列表→构建腾讯接口URL→设置Referer请求头→发送GET请求→返回响应数据解析后的列表
*/
@Override
public List<TxStockHistoryInfo> parseTxMoneyYesHistory(List<String> codeList, DateTime beforeLastWorking) {
// 1. 拼接股票代码参数(多个代码用逗号分隔,如"sz001318,sh600000")
String qParam = StrUtil.join(",", codeList);
// 2. 构建请求URL(配置文件中接口地址格式:http://qt.gtimg.cn/q={0})
String url = MessageFormat.format(defaultProperties.getTxMoneyHistoryUrl(), qParam);
try {
// 3. 构建请求头:仅需设置Referer为腾讯股票页面,避免反爬
Map<String, String> header = new HashMap<>();
header.put("Referer", "http://qt.gtimg.cn");
// 4. 发送HTTP GET请求:使用无代理客户端,编码设为GBK(腾讯接口响应编码为GBK)
String content = HttpUtil.sendGet(HttpClientConfig.proxyNoUseCloseableHttpClient(), url, header, "gbk");
// 5. 解析响应数据:调用专用解析工具类,将特殊格式字符串解析为TxStockHistoryInfo列表
return dailyTradingInfoParse.parseTxMoneyHistory(content, codeList, beforeLastWorking);
} catch (Exception e) {
log.error("parseTxMoneyYesHistory获取股票{}当前信息失败", qParam, e);
return null;
}
}
}
3.2.3 关键注意点
- 代码复用:复用
parseTxMoneyYesHistory方法,减少重复编码,同时保证系统代码风格一致; - 响应编码:腾讯接口响应编码为GBK,发送请求时需指定编码为GBK,否则会出现中文乱码问题;
- 数据格式解析:腾讯接口返回特殊格式字符串(如"v_sz001318="51~三峡能源001318 ...""),需通过
dailyTradingInfoParse.parseTxMoneyHistory工具类自定义解析逻辑(按"~"分割字符串,提取对应字段); - 字段兼容:腾讯数据部分字段缺失(如涨停状态需手动计算),需在封装时补充处理,确保DTO输出格式统一。
3.3 数据源切换策略实现
为实现"主备数据源自动切换",新增数据源选择器方法,核心逻辑:优先调用财联社接口,若返回null(爬取失败),则自动调用腾讯接口;若均失败,返回null并记录日志。
@Service
public class StockDetailServiceImpl implements StockDetailService {
@Override
public StockShowInfoDto getStockDetail(String fullCode) {
// 1. 优先调用主数据源(财联社)
StockShowInfoDto mainSourceResult = getByCode(fullCode);
if (mainSourceResult != null) {
return mainSourceResult;
}
// 2. 主数据源失败,切换备用数据源(腾讯)
log.warn("财联社爬取失败,切换腾讯数据源,fullCode={}", fullCode);
StockShowInfoDto backupSourceResult = getNowInfo(fullCode);
if (backupSourceResult != null) {
return backupSourceResult;
}
// 3. 均失败,返回null+记录错误日志
log.error("主备数据源爬取均失败,fullCode={}", fullCode);
return null;
}
}
四、核心优化点与生产环境适配
上述实现已满足股票详情获取的基础需求,但在生产环境中需补充以下优化点,确保系统稳定性、性能与可维护性:
-
缓存优化:对股票详情数据添加Redis缓存(缓存key为
STOCK_DETAIL + fullCode),缓存过期时间设为5分钟(行情数据实时性要求较高,不宜过长);爬取新数据后同步更新缓存,减少重复爬取,提升响应速度; -
反爬机制适配:
-
请求频率控制:通过线程池+计数器限制爬取频率(如单IP每分钟最多爬取60次),避免被数据源封禁IP;
-
请求头动态化:维护User-Agent池,每次请求随机选择一个User-Agent,模拟多浏览器请求,降低反爬风险;
-
数据源健康监控:记录主备数据源的爬取成功率(成功次数/总次数),通过监控工具(如Prometheus + Grafana)展示,当主数据源成功率低于90%时,自动发送告警(如钉钉/邮件),便于及时处理;
-
字段校验增强:对DTO封装后的字段进行非空校验(如核心行情字段openingPrice、nowPrice等),若为空则填充默认值(如0.0),避免上层调用出现空指针异常;
-
代码解耦:将HTTP请求、JSON解析、数值计算等通用逻辑抽取为独立工具类(如HttpUtil、JsonUtil、BigDecimalUtil),提升代码复用性与可维护性;
-
降级策略:当主备数据源均失败时,返回最近一次缓存的股票详情数据(若存在),而非直接返回null,提升用户体验。
五、系列文章预告
本文完成了量化系统"数据展示层"的核心模块------股票详细信息实时获取,实现了多数据源爬取、自动切换、数据封装的完整逻辑。下一篇文章将聚焦"构建自己的股票实时查看系统",结合本文获取的实时详情数据与第四篇的自选股票。
最后,留一个思考问题:在高并发场景下(如大量用户同时查询不同股票详情),如何设计缓存策略(如缓存穿透、缓存击穿、缓存雪崩的解决方案),才能兼顾性能与数据实时性?欢迎在评论区交流~