【java爬虫】获取个股详细数据并用echarts展示

前言

前面一篇文章介绍了获取个股数据的方法,本文将会对获取的接口进行一些优化,并且添加查询数据的接口,并且基于后端返回数据编写一个前端页面对数据进行展示。

具体的获取个股数据的接口可以看上一篇文章

【java爬虫】基于springboot+jdbcTemplate+sqlite+OkHttp获取个股的详细数据-CSDN博客

下面是操作演示,首先是爬虫获取股票数据

接着是进行获取个股详细数据并且进行数据展示

数据图表还可以下载下来,下面是下载下来的图片,不过下载下来的图片就不能查看每个点的详细数据了

后端接口

相对于前文,后端接口进行了一定优化,每年的数据分3次获取,时间段分别是0101-0501,0501-0901和0901-1231,并且每次请求都是从2023年开始逐年往前获取,一旦发现没有数据了就停止获取。

服务类的详细代码如下

@Slf4j
@Service
public class StockService {

    // 没有数据对应的返回
    private final String NO_DATA_RESPONSE1 = "historySearchHandler({})";
    private final String NO_DATA_RESPONSE2 = "history({})";

    @Autowired
    private SQLiteStockDao sqLiteStockDao;

    // 获取一个OKHttp实例
    private OkHttpClient client = new OkHttpClient()
            .newBuilder()
            .connectTimeout(1000, TimeUnit.SECONDS)
            .build();

    public void clearAll() {
        sqLiteStockDao.clearAll();
    }

    public void createTbaleIfNotExist() {
        sqLiteStockDao.createTbaleIfNotExist();
    }

    // 查询所有的数据
    public List<StockEntity> queryAllByCode(String code) {
        return sqLiteStockDao.queryAllByCode(code);
    }


    // 获取数据并且存入数据库
    // 三个参数分别是:股票代码,开始时间和结束时间
    // 开始时间和结束时间都填年份,代码中会自动补全具体时间
    public int getDataByYear(String code, String start, String end) {
        String url = "https://q.stock.sohu.com/hisHq?";
        Request request = null;
        Response response = null;
        int num = 0;
        // 一年的数据分三次请求
        String[] startTime = {"0101", "0501", "0901"};
        String[] endTime = {"0501", "0901", "1231"};
        try {
            for (int i = Integer.parseInt(end); i >= Integer.parseInt(start); i--) {
                for (int j = startTime.length-1; j >=0; j--) {
                    HttpUrl.Builder httpBuiler = HttpUrl.parse(url).newBuilder();
                    String starttime = i + startTime[j];
                    String endtime = i + endTime[j];
                    log.info("开始计算时间段[" + starttime + "," + endtime + "]内数据");
                    httpBuiler.addQueryParameter("code", "cn_" + code);
                    httpBuiler.addQueryParameter("start", starttime);
                    httpBuiler.addQueryParameter("end", endtime);
                    httpBuiler.addQueryParameter("stat", "1");
                    httpBuiler.addQueryParameter("order", "D");
                    httpBuiler.addQueryParameter("period", "d");
                    httpBuiler.addQueryParameter("callback", "history");
                    httpBuiler.addQueryParameter("rt", "jsonp");
                    request = new Request.Builder()
                            .url(httpBuiler.build())
                            .get()   //默认就是GET请求,可以不写
                            .addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36")
                            .build();

                    response = client.newCall(request).execute();
                    String res = response.body().string();
                    log.info("请求得到的数据:" + res);
                    if (res.contains(NO_DATA_RESPONSE1) || res.contains(NO_DATA_RESPONSE2)) {
                        // 如果返回为空就认为后面没有数据了
                        log.info("时间段[" + starttime + "," + endtime + "]没有数据");
                        return num;
                    } else {
                        List<StockEntity> entities = parseStrToArr(res, code);
                        sqLiteStockDao.insertItems(entities);
                        log.info("时间段[" + starttime + "," + endtime + "]内有" + entities.size() + "条数据");
                        num += entities.size();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return num;
    }

    // 将string数据解析成List列表
    private List<StockEntity> parseStrToArr(String res, String code) {
        if (res.contains(NO_DATA_RESPONSE1) || res.contains(NO_DATA_RESPONSE2)) {
            return new ArrayList<>();
        }
        List<StockEntity> entities = new ArrayList<>();
        res = res.split("\\(\\[")[1].split("]\\)")[0];
        JSONObject jsonObject = JSON.parseObject(res);
        // 获取 hq 字段的值
        Object hq = jsonObject.get("hq");
        // 判断 hq 的值是否为数组
        if (hq instanceof JSONArray) {
            // 遍历数组
            for (Object arr : (JSONArray) hq) {
                JSONArray jsonArray = (JSONArray) arr;
                StockEntity entity = new StockEntity();
                entity.setRecord_date((String) jsonArray.get(0));
                Double open_price = Double.parseDouble((String) jsonArray.get(1));
                Double close_price = Double.parseDouble((String) jsonArray.get(2));
                Double change_amend = Double.parseDouble((String) jsonArray.get(3));
                Double change_range = Double.parseDouble(((String) jsonArray.get(4)).split("%")[0]);
                Double max_price = Double.parseDouble((String) jsonArray.get(5));
                Double min_price = Double.parseDouble((String) jsonArray.get(6));
                Double volume = Double.parseDouble((String) jsonArray.get(7));
                Double turnover = Double.parseDouble((String) jsonArray.get(8));
                Double turnover_rate = Double.parseDouble(((String) jsonArray.get(9)).split("%")[0]);
                entity.setOpen_price(open_price);
                entity.setClose_price(close_price);
                entity.setChange_amend(change_amend);
                entity.setChange_range(change_range);
                entity.setMax_price(max_price);
                entity.setMin_price(min_price);
                entity.setVolume(volume);
                entity.setTurnover(turnover);
                entity.setTurnover_rate(turnover_rate);
                entity.setCode(code);
                entity.setId(entity.getCode() + "_" + (String) jsonArray.get(0));
                entities.add(entity);
            }
        }
        return entities;
    }

}

Dao层新增了查询某一只股票详细数据的方法,详细代码如下

@Slf4j
@Repository
public class SQLiteStockDao implements StockDao {

    private final String TABLE_NAME = "stock_table";

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public void clearAll() {
        String sql = "DELETE FROM " + TABLE_NAME;
        jdbcTemplate.batchUpdate(sql);
        log.info("成功清空数据表" + TABLE_NAME);
    }

    @Override
    public void createTbaleIfNotExist() {
        Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name = ?", Integer.class, TABLE_NAME);
        if (count == 0) {
            String sql = "CREATE TABLE " + TABLE_NAME + "(" +
                    "id VARCHAR(50) PRIMARY KEY," +
                    "code VARCHAR(20)," +           // 股票代码
                    "record_date VARCHAR(20)," +    // 记录的时间
                    "open_price float," +           // 开盘价
                    "close_price float," +           // 收盘价
                    "change_ament float," +          // 涨跌额
                    "change_range float," +          // 涨跌幅
                    "max_price float," +             // 最高价格
                    "min_price float," +             // 最低价格
                    "volume float," +                // 成交量(手)
                    "turnover float," +              // 成交额(万)
                    "turnover_rate float)";               // 换手率
            jdbcTemplate.execute(sql);
            log.info(TABLE_NAME + "建表成功");
        } else {
            log.info("建表失败,表格已存在");
        }
    }

    @Override
    public void insertItems(List<StockEntity> entityList) {
        String sql = "INSERT OR IGNORE INTO " + TABLE_NAME + " (id, code, record_date," +
                "open_price, close_price, change_ament," +
                "change_range, max_price, min_price," +
                "volume, turnover, turnover_rate) values (?,?,?,?,?,?,?,?,?,?,?,?)";
        // 将列表转为Object数组
        List<Object[]> arr = new ArrayList<>();
        for(int i=0; i<entityList.size(); i++) {
            arr.add(entityList.get(i).changeToArray());
        }
        jdbcTemplate.batchUpdate(sql, arr);
    }

    @Override
    public List<StockEntity> queryAllByCode(String code) {
        String sql = "SELECT open_price, close_price, record_date, volume FROM " + TABLE_NAME +" WHERE code=? ORDER BY record_date DESC";
        log.info("执行sql:" + sql);
        List<StockEntity> stockEntities = jdbcTemplate.query(sql, new Object[]{code}, new BeanPropertyRowMapper<>(StockEntity.class));
        return stockEntities;
    }


}

Dao层对应的实体类如下

@Data
@NoArgsConstructor
@AllArgsConstructor
public class StockEntity {
    private String id;
    private String code;
    private String record_date;
    private Double open_price;
    private Double close_price;
    private Double change_amend;
    private Double change_range;
    private Double max_price;
    private Double min_price;
    private Double volume;
    private Double turnover;
    private Double turnover_rate;

    // 将数据转换为Object数组
    public Object[] changeToArray() {
        Object[] arr = new Object[]{
                id,
                code,
                record_date,
                open_price.toString(),
                close_price.toString(),
                change_amend.toString(),
                change_range.toString(),
                max_price.toString(),
                min_price.toString(),
                volume.toString(),
                turnover.toString(),
                turnover_rate.toString()
        };
        return arr;
    }

}

最后就是提供给前端调用的接口了,主要是获取某一只股票的数据,只需要传入股票代码就能开始获取数据,还有查询的接口,同样是输入股票代码进行查询,控制类的详细代码如下

@Controller
@CrossOrigin
@RequestMapping("/stock")
public class StockController {

    private final String START_YEAR = "1985";
    private final String END_YEAR = "2023";

    @Autowired
    private StockService stockService;

    @RequestMapping("/clear")
    @ResponseBody
    public String clear() {
        stockService.clearAll();
        return "success";
    }

    @RequestMapping("/createTable")
    @ResponseBody
    public String getData() {
        stockService.createTbaleIfNotExist();
        return "success";
    }

    @RequestMapping("/getDataByYear/{code}/{start}/{end}")
    @ResponseBody
    public String getDataByYear(@PathVariable("code") String code,
                                @PathVariable("start") String start,
                                @PathVariable("end") String end) {
        Integer num = stockService.getDataByYear(code, start, end);
        return num.toString();
    }

    @RequestMapping("/getData/{code}")
    @ResponseBody
    public String getData(@PathVariable("code") String code) {
        Integer num = stockService.getDataByYear(code, START_YEAR, END_YEAR);
        List<StockEntity> stockEntityList = stockService.queryAllByCode(code);
        return JSON.toJSONString(stockEntityList);
    }

    @RequestMapping("/queryData/{code}")
    @ResponseBody
    public String queryData(@PathVariable("code") String code) {
        List<StockEntity> stockEntityList = stockService.queryAllByCode(code);
        return JSON.toJSONString(stockEntityList);
    }
}

前端页面

下面来说一下前端页面的编写,前端页面一共分为三个大块,

  • 沪深300成分股数据和操作按钮,通过按钮可以进行数据获取或者数据展示
  • 个股详细数据,这一个表格的内容会在你选定具体的股票后变更
  • 数据展示,选定个股后会动态生成展示的数据

前端主要用了vue+element-plus+axios+echarts进行编写,echarts表格参数参考了官方示例,由于数据量比较大,所以选用了大数据量的图表,参考的地址如下

Examples - Apache ECharts

下面展示页面的详细代码

<template>
  <div>
    <el-row class="container">
      <div class="left-grid">
        <el-card class="box-card">
          <template #header>
            <div class="card-header">
              <span>沪深300成分股</span>
            </div>
          </template>
          <el-table
            :data="table_data"
            :show-header="true"
            :max-height="250"
            stripe
          >
            <el-table-column
              type="index"
              label="序号"
              width="65%"
            ></el-table-column>
            <el-table-column
              prop="code"
              label="股票代码"
              width="85%"
            ></el-table-column>
            <el-table-column
              prop="name"
              label="公司简称"
              width="85%"
            ></el-table-column>
            <el-table-column prop="industry" label="操作">
              <template #default="scope">
                <el-button
                  type="primary"
                  size="small"
                  @click="queryData(scope.row)"
                  >查询</el-button
                >
                <el-button
                  type="primary"
                  size="small"
                  @click="getData(scope.row)"
                  >获取</el-button
                >
              </template>
            </el-table-column>
          </el-table>
        </el-card>
        <el-card>
          <template #header>
            <div class="card-header">
              <span>{{ table_title }}</span>
            </div>
          </template>
          <el-table
            v-loading="loading"
            :data="stock_data"
            :show-header="true"
            :max-height="220"
            stripe
          >
            <el-table-column prop="record_date" label="时间"></el-table-column>
            <el-table-column prop="open_price" label="开盘价"></el-table-column>
            <el-table-column
              prop="close_price"
              label="收盘价"
            ></el-table-column>
            <el-table-column
              prop="volume"
              label="成交量(手)"
            ></el-table-column>
          </el-table>
        </el-card>
      </div>

      <div class="right-grid" ref="myChart"></div>
    </el-row>
  </div>
</template>

<script>
import axios from "axios";
import { ElMessage } from "element-plus";
import { getCurrentInstance } from "vue";
export default {
  data() {
    return {
      update_status: "未开始",
      loading: true,
      table_title: "个股数据",
      // 沪深300成分股数据
      table_data: [],
      // 个股详细数据
      stock_data: [],
      echarts: getCurrentInstance().appContext.config.globalProperties.$echarts,
    };
  },
  mounted() {
    this.init();
  },
  methods: {
    init() {
      var url = "http://localhost:9001/queryAll";
      axios
        .get(url)
        .then((response) => {
          this.table_data = response.data;
          console.log(response);
          this.loading = false;
        })
        .catch((error) => {
          console.log(error);
          this.loading = false;
        });
    },
    // 绘制折线图
    create_axis() {
      //3.初始化实例对象 echarts.init(dom容器)
      var data_xAxis = [];
      var data_yAxis = [];
      for (var i = this.stock_data.length - 1; i >= 0; i--) {
        data_xAxis.push(this.stock_data[i].record_date);
        data_yAxis.push(this.stock_data[i].close_price);
      }
      console.log(data_xAxis);
      console.log(data_yAxis);
      var dom = this.$refs["myChart"]; // 获取dom节点
      var myChart = this.echarts.init(dom);
      //4.指定配置项和数据
      var option = {
        tooltip: {
          trigger: "axis",
          position: function (pt) {
            return [pt[0], "10%"];
          },
        },
        title: {
          left: "center",
          text: this.table_title,
        },
        toolbox: {
          feature: {
            dataZoom: {
              yAxisIndex: "none",
            },
            restore: {},
            saveAsImage: {},
          },
        },
        xAxis: {
          type: "category",
          boundaryGap: false,
          data: data_xAxis,
        },
        yAxis: {
          type: "value",
          boundaryGap: [0, "100%"],
        },
        dataZoom: [
          {
            type: "inside",
            start: 0,
            end: 10,
          },
          {
            start: 0,
            end: 10,
          },
        ],
        series: [
          {
            name: this.table_title,
            type: "line",
            symbol: "none",
            sampling: "lttb",
            itemStyle: {
              color: "rgb(135,206,235)",
            },
            areaStyle: {
              color: new this.echarts.graphic.LinearGradient(0, 0, 0, 1, [
                {
                  offset: 0,
                  color: "rgb(135,206,250)",
                },
                {
                  offset: 1,
                  color: "rgb(135,206,235)",
                },
              ]),
            },
            data: data_yAxis,
          },
        ],
      };
      //5.将配置项设置给echarts实例对象,使用刚指定的配置项和数据显示图表。
      myChart.setOption(option);
    },
    // 查询数据
    queryData(row) {
      var url = "http://localhost:9001/stock/queryData/" + row.code;
      this.loading = true;
      this.table_title = row.code + " " + row.name;
      ElMessage("开始查询 " + this.table_title + " 的数据");
      axios
        .get(url)
        .then((response) => {
          this.stock_data = response.data;
          console.log(response);
          this.loading = false;
          ElMessage({
            message: "查询 " + this.table_title + " 的数据成功",
            type: "success",
          });
          // 绘制数据
          this.create_axis();
        })
        .catch((error) => {
          console.log(error);
          this.loading = false;
          ElMessage.error("查询 " + this.table_title + " 的数据失败");
        });
    },
    // 获取数据
    getData(row) {
      var url = "http://localhost:9001/stock/getData/" + row.code;
      this.loading = true;
      this.table_title = row.code + " " + row.name;
      ElMessage("开始获取 " + this.table_title + " 的数据");
      axios
        .get(url)
        .then((response) => {
          this.stock_data = response.data;
          console.log(response);
          this.loading = false;
          ElMessage({
            message: "获取 " + this.table_title + " 的数据成功",
            type: "success",
          });
          // 绘制数据
          this.create_axis();
        })
        .catch((error) => {
          console.log(error);
          this.loading = false;
          ElMessage.error("获取 " + this.table_title + " 的数据失败");
        });
    },
  },
};
</script>

<style scoped>
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.container {
  display: grid;
  grid-template-columns: 35% 65%;
  width: 100%;
  height: 80vh;
}
.left-grid {
  background-color: #f0f0f0;
  border-radius: 2%;
  padding: 10px;
  height: 95%;
}
.right-grid {
  background-color: #f9ecc3;
  border-radius: 2%;
  padding: 10px;
  height: 95%;
}
</style>

前端页面有一个问题,就是数据量非常非常大,页面会很卡,这个问题的其中一个解决办法就是在获取数据的时候颗粒度可以小一点,比如一个星期获取一个数据之类的,因为一张图表也不可能展示出所有的数据,大家可能也只是想看一个总体的走势图,不过本文没有进行相关的优化,因为个人自用的话这点卡顿是可以接受的。 

结语

本文展示了通过网络爬虫获取个股详细数据,并且进行数据展示的方法,通过这个方法可以查询个股数据,并且用图表的方式将股票价格展示出来,这样可以非常直观地观察某一只股票的价格走势,由于获取到的数据量非常大,后期还可以进行一定的数据分析,如果你有什么想法欢迎和我交流,下面展示一下获取到的股票走势图。

  • 21
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
如果您下载了本程序,但是该程序无法运行,或者您不会部署,那么您可以选择退款或者寻求我们的帮助(如果找我们帮助的话,是需要追加额外费用的) 爬虫(Web Crawler)是一种自动化程序,用于从互联网上收集信息。其主要功能是访问网页、提取数据并存储,以便后续分析或展示爬虫通常由搜索引擎、数据挖掘工具、监测系统等应用于网络数据抓取的场景。 爬虫的工作流程包括以下几个关键步骤: URL收集: 爬虫从一个或多个初始URL开始,递归或迭代地发现新的URL,构建一个URL队列。这些URL可以通过链接分析、站点地图、搜索引擎等方式获取。 请求网页: 爬虫使用HTTP或其他协议向目标URL发起请求,获取网页的HTML内容。这通常通过HTTP请求库实现,如Python中的Requests库。 解析内容: 爬虫获取的HTML进行解析,提取有用的信息。常用的解析工具有正则表达式、XPath、Beautiful Soup等。这些工具帮助爬虫定位和提取目标数据,如文本、图片、链接等。 数据存储: 爬虫将提取的数据存储到数据库、文件或其他存储介质中,以备后续分析或展示。常用的存储形式包括关系型数据库、NoSQL数据库、JSON文件等。 遵守规则: 为避免对网站造成过大负担或触发反爬虫机制,爬虫需要遵守网站的robots.txt协议,限制访问频率和深度,并模拟人类访问行为,如设置User-Agent。 反爬虫应对: 由于爬虫的存在,一些网站采取了反爬虫措施,如验证码、IP封锁等。爬虫工程师需要设计相应的策略来应对这些挑战。 爬虫在各个领域都有广泛的应用,包括搜索引擎索引、数据挖掘、价格监测、新闻聚合等。然而,使用爬虫需要遵守法律和伦理规范,尊重网站的使用政策,并确保对被访问网站的服务器负责。
要将Java爬虫爬取到的数据展示echarts折线图,需要经过以下步骤: 1. 将爬取到的数据存储到数据库中,比如MySQL或者MongoDB。 2. 在Java Web应用中使用JDBC或者ORM框架(比如Hibernate、Mybatis等)连接数据库,读取数据并转换为JSON格式。 3. 在HTML页面中引入echarts的JS库和CSS文件,通过echarts的API渲染折线图。 4. 将Java中读取到的数据转换成echarts所需的格式,并通过Ajax异步请求将数据传递给前端页面。可以使用JSON格式来传递数据。 5. 使用echarts的API将数据绘制成折线图。 以下是一个简单的示例代码,假设我们已经将爬取到的数据存储到MySQL数据库中: Java代码: ``` import java.sql.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class DataProvider { private static final String URL = "jdbc:mysql://localhost:3306/test"; private static final String USER = "root"; private static final String PASSWORD = "password"; public static List<Map<String, Object>> getData() { List<Map<String, Object>> list = new ArrayList<>(); Connection conn = null; Statement stmt = null; ResultSet rs = null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection(URL, USER, PASSWORD); stmt = conn.createStatement(); String sql = "SELECT * FROM data"; rs = stmt.executeQuery(sql); while (rs.next()) { Map<String, Object> map = new HashMap<>(); map.put("date", rs.getString("date")); map.put("value", rs.getInt("value")); list.add(map); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (rs != null) rs.close(); if (stmt != null) stmt.close(); if (conn != null) conn.close(); } catch (Exception e) { e.printStackTrace(); } } return list; } } ``` 上述代码中,我们通过JDBC连接MySQL数据库,并查询名为"data"的表中的数据,并将其转换为List<Map<String, Object>>类型的数据。 HTML代码: ``` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>折线图示例</title> <script src="https://cdn.staticfile.org/echarts/4.8.0/echarts.min.js"></script> </head> <body> <div id="main" style="width: 600px;height:400px;"></div> <script type="text/javascript"> var myChart = echarts.init(document.getElementById('main')); var option = { title: { text: '折线图示例' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: [] }, yAxis: { type: 'value' }, series: [{ data: [], type: 'line' }] }; myChart.setOption(option); // 异步请求数据 var xhr = new XMLHttpRequest(); xhr.open('GET', '/data', true); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { var data = JSON.parse(xhr.responseText); var xAxisData = []; var seriesData = []; for (var i = 0; i < data.length; i++) { xAxisData.push(data[i].date); seriesData.push(data[i].value); } myChart.setOption({ xAxis: { data: xAxisData }, series: [{ data: seriesData }] }); } }; xhr.send(); </script> </body> </html> ``` 上述代码中,我们引入了echarts的JS库,并在页面上创建了一个div容器,用于展示折线图。然后我们通过异步请求从Java后端读取数据,并将其转换为折线图所需的格式。最后通过echarts的API将数据绘制成折线图。 注意:上述示例代码仅供参考,实际应用中需要根据具体的需求进行修改和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值