兄弟们,复盘的时候有没有过这种烦恼?想翻几只股票的日K线、分钟K线对比走势,结果打开行情软件翻来翻去,要么加载慢,要么广告弹窗打断思路,好不容易找到还没法批量保存,下次复盘又得重新找......
别愁了!这篇系列第十二篇,咱们直接搞个全自动K线图抓取神器------收盘后自动把指定股票的日K线、分钟K线图抓下来,存成图片文件还同步记到数据库,后续不管是复盘对比,还是对接量化策略做图形分析,直接拿现成的就行,省心到飞起!
一、先搞懂:这功能到底能解决啥痛点?
可能有兄弟觉得"不就是看个K线吗?软件里看就行",但真正天天复盘、做量化的都懂,手动找K线的痛点太明显了:
- 复盘效率低:想对比10只股票的走势,得一个个在软件里搜,翻页翻到手软;
- 没法批量保存:行情软件里的K线图没法批量下载,下次想翻历史走势还得重新加载;
- 量化策略缺数据:做技术面分析的量化策略,需要K线图的可视化数据支撑,直接抓下来才能对接后续分析;
- 怕数据丢失:行情软件偶尔会出问题,自己存一份本地+数据库,多一层保障,心里踏实。
而咱们做的这个工具,刚好把这些痛点全解决:收盘后自动跑,指定股票列表的日K、分钟K一键抓完存好,后续用的时候直接调,不用再跟软件"斗智斗勇"。
二、核心逻辑:简单3步,自动抓完存好
其实整个流程超简单,不用搞复杂的逻辑,核心就3步,新手也能看明白:
- 选时间:固定在收盘后执行(15点之后),避免盘中数据不完整;
- 抓数据:根据股票代码,调用新浪财经的K线图接口(日K、分钟K有现成的公开接口,不用自己找),拿到Base64格式的图片数据;
- 存数据:把Base64转成图片文件存到本地文件夹,同时把股票代码、K线类型、保存路径这些信息记到数据库,方便后续查询。
小科普:为啥用新浪财经的接口?因为它的K线图接口是公开的,不用登录、不用破解,直接拼接股票代码就能用,稳定性还强,对咱们开发者太友好了!
三、核心代码拆解:复制粘贴就能用,重点都标好了
下面把核心代码拆解开讲,每个部分的作用、关键参数都标清楚了,大家直接复制过去,改改股票列表和保存路径就能跑通。
3.1 入口方法:选股票、选K线类型,一键触发
先有个入口方法saveImageSelected,传入要抓的股票代码列表和K线类型(1=分钟K,2=日K),后续定时任务直接调用这个方法就行:
/**
* 入口方法:指定股票列表和K线类型,触发抓取保存
* @param codeList 要抓取的股票代码列表(比如["600000","000001"])
* @param type K线类型:1=分钟K,2=日K
*/
private void saveImageSelected(List<String> codeList, Integer type) {
List<String> resultCodeList = codeList;
// 关键:只在15点之后执行(收盘后),保证数据完整
List<String> fullCodeListByCodeList = stockService.findFullCodeListByCodeList(resultCodeList);
if (type == 1) {
// 抓取分钟K线
getAndSaveImage(fullCodeListByCodeList, KType.MIN);
}
if (type == 2) {
// 抓取日K线
getAndSaveImage(fullCodeListByCodeList, KType.DAY);
}
}
3.2 核心方法:抓取+保存,一步到位
真正负责"抓数据+存数据"的是getAndSaveImage方法,里面包含了创建文件夹、判断是否已抓取、调用接口、转Base64存图片、记数据库这一系列操作,逻辑很顺:
/**
* 核心方法:抓取K线图并保存(本地文件+数据库)
* @param listFullCode 完整股票代码列表(带市场前缀,比如["sh600000","sz000001"])
* @param kType K线类型(MIN=分钟,DAY=日)
*/
private void getAndSaveImage(List<String> listFullCode, KType kType) {
// 1. 确定保存日期:取上一个交易日(避免非交易日无数据)
String date = DateUtil.format(dateHelper.getBeforeLastWorking(new Date()), Const.STOCK_DATE_FORMAT);
// 2. 确定保存路径,没有文件夹就自动创建
String directory = getStockImageDirectory(kType);
File imageDirection = new File(uploadFilePath + File.separator + directory + File.separator + date);
if (!imageDirection.exists()) {
imageDirection.mkdirs(); // 自动创建多级文件夹,不用手动建
}
// 3. 循环遍历每只股票,逐个抓取
for (String fullCode : listFullCode) {
String realCode = fullCode.substring(2); // 去掉市场前缀(sh/sz),拿纯股票代码
// 4. 过滤非交易股票,避免白跑一趟
if (!StockCodeType.isTradeType(realCode)) {
log.info("股票 {} 不是可交易股票,跳过", realCode);
continue;
}
// 5. 检查Redis,避免重复抓取(已经抓过的就跳过)
if (KType.MIN.equals(kType)) {
if (!redisUtil.isMembers(DataKey.IMAGE_MINUTE, realCode)) {
log.info("股票 {} 分钟K线已抓取过,跳过", realCode);
continue;
}
} else {
if (!redisUtil.isMembers(DataKey.IMAGE_DAY, realCode)) {
log.info("股票 {} 日K线已抓取过,跳过", realCode);
continue;
}
}
// 6. 关键:调用接口获取K线图的Base64数据
Object data = crawlerStockService.getCrawlerLineByFullCode(fullCode, kType.getCode()).getData();
if (ObjectUtils.isEmpty(data)) {
log.warn("获取股票 {} {}K线失败", fullCode, kType.getName());
// 抓取失败:从Redis移除,方便后续重新抓取
removeFromRedis(realCode, kType);
continue;
}
// 7. Base64转图片字节数组,准备保存
byte[] decode = Base64.getDecoder().decode(data.toString());
String prefix = RandomUtil.randomString(8) + "_"; // 加随机前缀,避免文件名重复
// 8. 把保存信息记到数据库(方便后续查询)
saveRecordToDb(kType, fullCode, prefix);
// 9. 保存图片文件到本地文件夹
File newFile = new File(imageDirection, prefix + fullCode + ".png");
if (!newFile.exists()) { // 避免重复保存
FileUtil.writeBytes(decode, newFile);
}
// 10. 休眠2秒:避免请求太频繁被封IP,温柔一点
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
}
// 11. 抓取成功:从Redis移除,标记为已完成
removeFromRedis(realCode, kType);
}
}
// 辅助方法:从Redis移除标记
private void removeFromRedis(String realCode, KType kType) {
if (KType.MIN.equals(kType)) {
redisUtil.sRem(DataKey.IMAGE_MINUTE, realCode);
} else {
redisUtil.sRem(DataKey.IMAGE_DAY, realCode);
}
}
private String getStockImageDirectory(KType kType) {
switch(kType) {
case DAY:
return "dayImages";
case MIN:
default:
return "images";
}
}
3.3 接口调用方法:对接新浪接口,拿到Base64数据
上面步骤里"获取K线数据"的核心,是getCrawlerLineByFullCode方法,它会根据K线类型拼接对应的新浪接口地址,拿到图片的Base64数据:
/**
* 对接新浪接口,获取K线图的Base64数据
* @param fullCode 完整股票代码(带sh/sz前缀)
* @param type K线类型编码
* @return 包含Base64数据的结果
*/
public OutputResult getCrawlerLineByFullCode(String fullCode, Integer type) {
// 确定K线类型(默认分钟K)
KType kType = Optional.ofNullable(KType.getKType(type)).orElse(KType.MIN);
String result = "";
// 根据K线类型调用不同的接口方法
switch (kType) {
case MIN: {
result = crawlerService.getMinUrl(fullCode); // 分钟K接口
break;
}
case DAY: {
result = crawlerService.getDayUrl(fullCode); // 日K接口
break;
}
case WEEK:{
result = crawlerService.getWeekUrl(fullCode); // 周K(预留扩展)
break;
}
case MONTH:{
result = crawlerService.getMonthUrl(fullCode); // 月K(预留扩展)
break;
}
default: {
break;
}
}
return OutputResult.buildSucc(result);
}
3.4 关键:新浪K线接口地址,直接用!
最后是最核心的接口拼接方法,新浪的分钟K和日K接口格式固定,直接把股票代码拼进去就行,不用额外处理:
/**
* 拼接分钟K线接口地址(新浪)
* 接口格式:https://image.sinajs.cn/newchart/min/n/{0}.gif
* {0} 填完整股票代码(比如sh600000)
*/
public String getMinUrl(String code) {
String url = MessageFormat.format(defaultProperties.getMinUrl(), code);
try {
// 从接口获取图片字节数组
byte[] btImg1 = ImageUtil.getImageFromNetByUrl(url);
// 转成Base64字符串返回
return Base64.encode(btImg1);
} catch (Exception e) {
log.error("获取股票{}分钟K线出错", code, e);
return null;
}
}
/**
* 拼接日K线接口地址(新浪)
* 接口格式:https://image.sinajs.cn/newchart/daily/n/{0}.gif
*/
public String getDayUrl(String code) {
// 逻辑和分钟K一样,只是接口地址不同
String url = MessageFormat.format(defaultProperties.getDayUrl(), code);
try {
byte[] btImg1 = ImageUtil.getImageFromNetByUrl(url);
return Base64.encode(btImg1);
} catch (Exception e) {
log.error("获取股票{}日K线出错", code, e);
return null;
}
}
重点记一下接口地址:1. 分钟K:https://image.sinajs.cn/newchart/min/n/{0}.gif
- 日K:https://image.sinajs.cn/newchart/daily/n/{0}.gif把{0}换成完整股票代码(比如sh600000、sz000001),
直接在浏览器里打开就能看到K线图!
四、怎么用?2种方式,覆盖不同需求
写好代码后,用起来超灵活,两种方式按需选:
4.1 收盘后定时自动跑(推荐)
用Spring Scheduler加个定时任务,设置在每天15:05执行(确保收盘完成),指定要跟踪的股票列表,每天自动抓完存好,完全不用管:
// 定时任务:每天15:05执行,抓取指定股票的日K和分钟K
@Scheduled(cron = "0 5 15 ? * 1-5") // 周一到周五,15点05分触发
public void autoSaveKLineAfterClose() {
// 要跟踪的股票列表(可以从数据库查,更灵活)
List<String> stockCodeList = Arrays.asList("600000", "000001", "300001");
saveImageSelected(stockCodeList, 1); // 1=分钟K
saveImageSelected(stockCodeList, 2); // 2=日K
log.info("收盘后K线抓取完成!");
}
4.2 实时手动触发查询
如果临时想查某只股票的K线图,直接调用getCrawlerLineByFullCode方法,传入股票代码和K线类型,就能拿到Base64数据,转成图片就能看,比软件里搜还快。
五、小优化:让工具更稳、更好用
为了避免出问题,给大家提2个小优化点,加上之后工具更稳:
- Redis去重:用Redis的集合记录已抓取的股票,避免重复抓取浪费资源;
- 异常处理:抓取失败的股票,从Redis移除标记,方便后续重新抓取;
- 休眠控制:每抓一只股票休眠2秒,避免请求太频繁被新浪封IP;
- 多级文件夹:按"K线类型/日期"创建文件夹,比如"day/20251222",图片分类存放,后续找的时候一目了然。
金亥跃江为了表示对各位粉丝们的感谢,将之前的数据开放出来,大家可以访问:
https://www.yueshushu.top/adminStockPage/adminMinuteImage.html?sign=stock
进行查看分时图信息
六、系列预告:下一篇搞K线图形态!
这篇咱们搞定了K线图的"抓取+保存",把可视化数据存了下来。有了这些K线图数据,
下一篇咱们就搞点更实用的------ 股票的K线形态
最后留个小问题:如果想抓取周K线和月K线,只需要改哪里?懂的兄弟评论区说下,新手可以跟着思路练手~