专栏首页java之旅​Java爬取同花顺股票数据(附源码)
原创

​Java爬取同花顺股票数据(附源码)

Java爬取同花顺股票数据(附源码)

最近有小伙伴问我能不能抓取同花顺的数据,最近股票行情还不错,想把数据抓下来自己分析分析。我大A股,大家都知道的,一个概念火了,相应的股票就都大涨。

如果能及时获取股票涨跌信息,那就能在刚开始火起来的时候杀进去,小赚一笔。但是股票那么多,小伙伴也盯不过来,于是就微信问我,能不能抓取同花顺的板块下的股票信息存到数据库里?他就能根据数据库里的数据,制定一些策略。

image.png

俗话说:哪里有痛点,哪里就有编程!不就是个同花顺嘛,办他!

调研背景

于是我点开了同花顺的板块页面:http://q.10jqka.com.cn/gn/

发现有好268个概念:

板块网页

分析概念板块的网页HTML发现,268个概念的URL就在HTML中:

板块-网页源码(全部268个概念)

打开其中的“阿里巴巴概念”,发现网页又有分页:

分页信息

分页的数据,是根据接口实时获取的,接口中注入了一些Cooki信息和其他标识,同花顺的反爬虫策略一直比较强,使用模拟接口的方式可能难度会比较大,所以使用selenium模拟浏览器操作这种方式比较完美。

设计方案

技术方向有了,再简单整理一下思路:

  • 根据http://q.10jqka.com.cn/gn/,获取板块网页的源码HTML,用Jsoup解析HTML获取每个概念的url信息放到List中
  • 遍历List,根据概念的url获取概念网页源码HTML,解析股票信息
    • 再递归点击执行“下一页”操作,获取每一页的股票数据,直至尾页

  • 把股票信息存储到数据库

配置环境

先介绍下工程所需要的环境:

编码工具:idea 语言:java 依赖:jdk1.8、maven、chrome、ChromeDriver

我们使用的方案是模拟浏览器的操作,所以我们需要在电脑安装chrome浏览器和chromedriver驱动。chrome的安装这里就不说了,百度下载个浏览器就行。

关键是安装 ChromeDriver ,需要安装和当前chrome版本一致的驱动才写。

查看chrome版本:chrome浏览器输入:Chrome://version

我的chrome的版本

在根据版本下载对于的驱动,版本最好要一致,比如我的是:79.0.3945.117 (正式版本) (64 位),我下载的就是 79.0.3945.36。

ChromeDriver各版本的下载地址:

淘宝镜像:https://npm.taobao.org/mirrors/chromedriver

谷歌下载(需要访问外国网站,不推荐):https://sites.google.com/a/chromium.org/chromedriver/downloads

下面这一步可做可不做,不做也能启动工程,只是需要修改代码中的一个配置即可。

配置方式: 将下载好的ChromeDriver文件放到/usr/local/bin/目录下: cp chromedriver /usr/local/bin/ 检测是否安装成功 chromedriver --version

如果不配置,只需要记得修改ChromeDriver在代码中配置的路径,你只需要将路径改为你自己的ChromeDriver路径即可,比如我的是:

System.setProperty(
  "webdriver.chrome.driver",
  "/Users/admin/Documents/selenium/chrome/79.0.3945.36/chromedriver"
);

记得修改代码里ChromeDriver的路径。

记得修改代码里ChromeDriver的路径。

记得修改代码里ChromeDriver的路径。

验证方案

首先完成设计方案中的三步

package com.ths.controller;

import com.ths.service.ThsGnCrawlService;
import com.ths.service.ThsGnDetailCrawlService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.List;

@Controller
public class CrawlController {

    @Autowired
    private ThsGnCrawlService thsGnCrawlService;

    @Autowired
    private ThsGnDetailCrawlService thsGnDetailCrawlService;

    @RequestMapping("/test")
    @ResponseBody
    public void test() {
        // 抓取所有概念板块的url
        List<HashMap<String, String>> list = thsGnCrawlService.ThsGnCrawlListUrl();
        // 放入阻塞队列
        thsGnDetailCrawlService.putAllArrayBlockingQueue(list);
        // 根据url多线程抓取
        thsGnDetailCrawlService.ConsumeCrawlerGnDetailData(1);
    }

}

先看看thsGnCrawlService.ThsGnCrawlListUrl();方法,如何抓取所有概念板块的url?

package com.ths.service.impl;

import com.ths.parse.service.ThsParseHtmlService;
import com.ths.service.ThsGnCrawlService;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
public class ThsGnCrawlServiceImpl implements ThsGnCrawlService {
    private final static Logger LOGGER = LoggerFactory.getLogger(ThsGnCrawlServiceImpl.class);

    /**
     * 同花顺全部概念板块url
     */
    private final static String GN_URL = "http://q.10jqka.com.cn/gn/";

    @Autowired
    private ThsParseHtmlService thsParseHtmlService;

    @Override
    public List<HashMap<String, String>> ThsGnCrawlListUrl() {
        System.setProperty("webdriver.chrome.driver", "/Users/admin/Documents/selenium/chrome/79.0.3945.36/chromedriver");
        ChromeOptions options = new ChromeOptions();
        //是否启用浏览器界面的参数
        //无界面参数
//        options.addArguments("headless");
        //禁用沙盒 就是被这个参数搞了一天
//        options.addArguments("no-sandbox");
        WebDriver webDriver = new ChromeDriver(options);
        try {
            // 根据网速设置,网速慢可以调低点
            webDriver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);
            webDriver.get(GN_URL);
            Thread.sleep(1000L);
            String gnWindow = webDriver.getWindowHandle();
            // 获取同花顺概念页面的HTML
            String thsGnHtml = webDriver.getPageSource();
            LOGGER.info("获取同花顺url:[{}]的html为:/n{}", GN_URL, thsGnHtml);
            return thsParseHtmlService.parseGnHtmlReturnGnUrlList(thsGnHtml);
        } catch (Exception e) {
            LOGGER.error("获取同花顺概念页面的HTML,出现异常:", e);
        } finally {
            webDriver.close();
            webDriver.quit();
        }
        return null;
    }
}

这里使用了上文说的ChromeDriver,我们需要根据自己的配置,修改对应的地址(重复第四遍!)。

根据代码可以看到String thsGnHtml = webDriver.getPageSource();方法获取页面的HTML,再解析HTML就能获取各大概念板块的url。

解析HTML我使用的是Jsoup,简单易上手,api也很简单,解析HTML获取各大板块的url的代码如下:

package com.ths.parse.service.impl;

import com.ths.parse.service.ThsParseHtmlService;
import org.jsoup.Jsoup;
import org.jsoup.helper.StringUtil;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

@Service
public class ThsParseHtmlServiceImpl implements ThsParseHtmlService {

    /**
     * 解析同花顺概念板块的Html页面:http://q.10jqka.com.cn/gn/
     * 返回所有概念板块的url地址
     */
    public List<HashMap<String, String>> parseGnHtmlReturnGnUrlList(String html) {
        if (StringUtil.isBlank(html)) {
            return null;
        }
        List<HashMap<String, String>> list = new ArrayList<>();
        Document document = Jsoup.parse(html);
        Elements cateItemsFromClass = document.getElementsByClass("cate_items");
        for (Element element : cateItemsFromClass) {
            Elements as = element.getElementsByTag("a");
            for (Element a : as) {
                String gnUrl = a.attr("href");
                String name = a.text();
                HashMap<String, String> map = new HashMap<>();
                map.put("url", gnUrl);
                map.put("gnName", name);
                list.add(map);
            }
        }
        return list;
    }
}

可以看到,只要在html中有的数据,定位到标签就能获取对应的数据。

然后放到阻塞队列:

		/**
     * 阻塞队列
     */
    private ArrayBlockingQueue<HashMap<String, String>> arrayBlockingQueue = new ArrayBlockingQueue<>(1000);

    @Override
    public void putAllArrayBlockingQueue(List<HashMap<String, String>> list) {
        if (!CollectionUtils.isEmpty(list)) {
            arrayBlockingQueue.addAll(list);
        }
    }

再开启多个线程,从阻塞队列里获取url,分别抓取概念板块的股票数据,如果页面有分页,就循环点击下一页,再获取数据,直到尾页,代码如下:

package com.ths.service.impl;

import com.ths.dao.StockThsGnInfoDao;
import com.ths.domain.StockThsGnInfo;
import com.ths.service.ThsGnDetailCrawlService;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;

@Service
public class ThsGnDetailCrawlServiceImpl implements ThsGnDetailCrawlService {
    private final static Logger LOGGER = LoggerFactory.getLogger(ThsGnDetailCrawlServiceImpl.class);

    /**
     * 阻塞队列
     */
    private ArrayBlockingQueue<HashMap<String, String>> arrayBlockingQueue = new ArrayBlockingQueue<>(1000);

    @Autowired
    private StockThsGnInfoDao stockThsGnInfoDao;

    @Override
    public void putAllArrayBlockingQueue(List<HashMap<String, String>> list) {
        if (!CollectionUtils.isEmpty(list)) {
            arrayBlockingQueue.addAll(list);
        }
    }

    @Override
    public void ConsumeCrawlerGnDetailData(int threadNumber) {
        for (int i = 0; i < threadNumber; ++i) {
            LOGGER.info("开启线程第[{}]个消费", i);
            new Thread(new crawlerGnDataThread()).start();
        }
        LOGGER.info("一共开启线程[{}]个消费", threadNumber);
    }

    class crawlerGnDataThread implements Runnable {

        @Override
        public void run() {
            try {
                while (true) {
                    Map<String, String> map = arrayBlockingQueue.take();
                    String url = map.get("url");
                    String gnName = map.get("gnName");
                    String crawlerDateStr = new SimpleDateFormat("yyyy-MM-dd HH:00:00").format(new Date());
                    //chromederiver存放位置
                    System.setProperty("webdriver.chrome.driver", "/Users/admin/Documents/selenium/chrome/79.0.3945.36/chromedriver");
                    ChromeOptions options = new ChromeOptions();
                    //无界面参数
                    //        options.addArguments("headless");
                    //禁用沙盒 就是被这个参数搞了一天
                    //        options.addArguments("no-sandbox");
                    WebDriver webDriver = new ChromeDriver(options);
                    try {
                        webDriver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);
                        webDriver.get(url);
                        Thread.sleep(1000L);
                        String oneGnHtml = webDriver.getPageSource();
                        LOGGER.info("当前概念:[{}],html数据为[{}]", gnName, oneGnHtml);
                        LOGGER.info(oneGnHtml);
                        // TODO 解析并存储数据
                        parseHtmlAndInsertData(oneGnHtml, gnName, crawlerDateStr);
                        clicktoOneGnNextPage(webDriver, oneGnHtml, gnName, crawlerDateStr);
                    } catch (Exception e) {
                        LOGGER.error("用chromerDriver抓取数据,出现异常,url为[{}],异常为[{}]", url, e);
                    } finally {
                        webDriver.close();
                        webDriver.quit();
                    }
                }
            } catch (Exception e) {
                LOGGER.error("阻塞队列出现循环出现异常:", e);
            }
        }
    }

    public void parseHtmlAndInsertData(String html, String gnName, String crawlerDateStr) {
        Document document = Jsoup.parse(html);
//        Element boardElement = document.getElementsByClass("board-hq").get(0);
//        String gnCode = boardElement.getElementsByTag("h3").get(0).getElementsByTag("span").get(0).text();

        Element table = document.getElementsByClass("m-pager-table").get(0);
        Element tBody = table.getElementsByTag("tbody").get(0);
        Elements trs = tBody.getElementsByTag("tr");
        for (Element tr : trs) {
            try {
                Elements tds = tr.getElementsByTag("td");
                String stockCode = tds.get(1).text();
                String stockName = tds.get(2).text();
                BigDecimal stockPrice = parseValueToBigDecimal(tds.get(3).text());
                BigDecimal stockChange = parseValueToBigDecimal(tds.get(4).text());
                BigDecimal stockChangePrice = parseValueToBigDecimal(tds.get(5).text());
                BigDecimal stockChangeSpeed = parseValueToBigDecimal(tds.get(6).text());
                BigDecimal stockHandoverScale = parseValueToBigDecimal(tds.get(7).text());
                BigDecimal stockLiangBi = parseValueToBigDecimal(tds.get(8).text());
                BigDecimal stockAmplitude = parseValueToBigDecimal(tds.get(9).text());
                BigDecimal stockDealAmount = parseValueToBigDecimal(tds.get(10).text());
                BigDecimal stockFlowStockNumber = parseValueToBigDecimal(tds.get(11).text());
                BigDecimal stockFlowMakertValue = parseValueToBigDecimal(tds.get(12).text());
                BigDecimal stockMarketTtm = parseValueToBigDecimal(tds.get(13).text());
                // 存储数据
                StockThsGnInfo stockThsGnInfo = new StockThsGnInfo();
                stockThsGnInfo.setGnName(gnName);
                stockThsGnInfo.setGnCode(null);
                stockThsGnInfo.setStockCode(stockCode);
                stockThsGnInfo.setStockName(stockName);
                stockThsGnInfo.setStockPrice(stockPrice);
                stockThsGnInfo.setStockChange(stockChange);
                stockThsGnInfo.setStockChangePrice(stockChangePrice);
                stockThsGnInfo.setStockChangeSpeed(stockChangeSpeed);
                stockThsGnInfo.setStockHandoverScale(stockHandoverScale);
                stockThsGnInfo.setStockLiangBi(stockLiangBi);
                stockThsGnInfo.setStockAmplitude(stockAmplitude);
                stockThsGnInfo.setStockDealAmount(stockDealAmount);
                stockThsGnInfo.setStockFlowStockNumber(stockFlowStockNumber);
                stockThsGnInfo.setStockFlowMakertValue(stockFlowMakertValue);
                stockThsGnInfo.setStockMarketTtm(stockMarketTtm);
                stockThsGnInfo.setCrawlerTime(crawlerDateStr);
                stockThsGnInfo.setCrawlerVersion("同花顺概念板块#" + crawlerDateStr);
                stockThsGnInfo.setCreateTime(new Date());
                stockThsGnInfo.setUpdateTime(new Date());
                stockThsGnInfoDao.insert(stockThsGnInfo);
            } catch (Exception e) {
                LOGGER.error("插入同花顺概念板块数据出现异常:", e);
            }

        }
    }

    public BigDecimal parseValueToBigDecimal(String value) {
        if (StringUtils.isEmpty(value)) {
            return BigDecimal.ZERO;
        } else if ("--".equals(value)) {
            return BigDecimal.ZERO;
        } else if (value.endsWith("亿")) {
            return new BigDecimal(value.substring(0, value.length() - 1)).multiply(BigDecimal.ONE);
        }
        return new BigDecimal(value);
    }

    public boolean clicktoOneGnNextPage(WebDriver webDriver, String oneGnHtml, String key, String crawlerDateStr) throws InterruptedException {
        // 是否包含下一页
        String pageNumber = includeNextPage(oneGnHtml);
        if (!StringUtils.isEmpty(pageNumber)) {
            WebElement nextPageElement = webDriver.findElement(By.linkText("下一页"));
            webDriver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);
            nextPageElement.click();
            Thread.sleep(700);
            String nextPageHtml = webDriver.getPageSource();
            LOGGER.info("下一页:");
            LOGGER.info(nextPageHtml);
            // TODO 解析并存储数据
            parseHtmlAndInsertData(nextPageHtml, key, crawlerDateStr);
            clicktoOneGnNextPage(webDriver, nextPageHtml, key, crawlerDateStr);
        }
        return true;
    }

    public String includeNextPage(String html) {
        Document document = Jsoup.parse(html);
        List<Element> list = document.getElementsByTag("a");
        for (Element element : list) {
            String a = element.text();
            if ("下一页".equals(a)) {
                String pageNumber = element.attr("page");
                return pageNumber;
            }
        }
        return null;
    }
}

最后对,概念板块的页面数据进行解析入库。

数据展示

验证

抓取数据入库,验证成功! 源码地址:Java爬取同花顺股票数据,源码地址

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • SpringBoot整合Swagger

    上一篇《简单搭建SpringBoot项目》讲了简单的搭建SpringBoot 项目,而 SpringBoot 和 Swagger-ui 搭配在持续交付的前后端开...

    java之旅
  • ​SpringBoot连接多RabbitMQ源

    在实际开发中,很多场景需要异步处理,这时就需要用到RabbitMQ,而且随着场景的增多程序可能需要连接多个RabbitMQ。SpringBoot本身提供了默认的...

    java之旅
  • SpringBoot教程之RabbitMQ示例

    SpringBoot框架已经提供了RabbitMQ的使用jar包,开发人员在使用RabbitMQ的时候只需要引用jar包简单的配置一下就可以使用RabbitMQ...

    java之旅
  • Geotools中实现NC转等值面

    前面的文章有实现IDW插值并生成等值面的,本文在前文基础上实现气象NC数据生成等值面。

    lzugis
  • Scala 强大的精简语法(示例)

    Scala 是面向对象与函数编程语言,最终编译成 java 字节码,运行在 jvm 上。

    田祥
  • Spark SQL DataFrame与RDD交互

    Spark SQL 支持自动将 JavaBeans 的 RDD 转换为 DataFrame。使用反射获取的 BeanInfo 定义了表的 schema。目前为止...

    smartsi
  • geotools等值线生成

    前文中,提到了等值面的生成,后面有人经常会问等值线的生成,本文在前文的基础上做了一点修改,完成了等值线的geotools生成。

    lzugis
  • 读写hdfs文件(工作笔记)

    用户3003813
  • 微信开发之token认证 原

    用户2603479
  • 论文 | 百度「一次包会」模型:「一次性」教会Agent认新事物

    百度 Research 在近日发表了一篇博文,介绍了通过交互式对话来教 AI 智能体学会语言和一次性实现主动概念学习的方法。

    AI科技评论

扫码关注云+社区

领取腾讯云代金券