stockgo数据爬取-业绩预告

本文介绍了使用Go语言构建的股票业绩预告爬虫系统,该系统能够每日自动爬取和更新同花顺网站的股票业绩预告数据,包括年报、季报,并按行业和概念进行查询。系统克服了同花顺的反爬策略,实现了请求频率控制,并提供了数据条件查询和列表展示。此外,还展示了如何解析网页和存储数据到数据库中。
摘要由CSDN通过智能技术生成

一、爬虫目标

本次爬虫程序的主要目标有:

1.每日自动爬取股票业绩预告(包括年报、一季报、中报、三季报);

2.爬取历史数据;

3.每日自动增量更新,免人工维护;

4.业绩预告分类分级,按照行业,概念可以查询;

5.数据条件查询,列表展示,颜色提示;

二、难点

众所周知,同花顺反爬技术还是非常厉害的,算法加密,请求频率控制等已经阻拦了95%的爬虫,本框架采用go语言编写的其中一个原因就是使用go语言更方便进行请求频率控制,更加简洁,当然更重要的原因是go语言编译生成程序的加密安全性更好,还有部署方便等优点。在本框架中,已经解决了同花顺加密hexinv,和请求频率问题。千万注意控制请求频率,否则会被封IP,我有被腾讯、新浪、东财、同花顺封IP的经历,后续编写stockgo框架后,基本没有发生过。下文会有相关代码片段。

三、效果图

效果图

效果图

叠加到K线图

选一个业绩预增的新能源牛股,效果就更加明显,2020-09-02我们的选股系统发出了买入信号(选股系统后续再讲,有兴趣的朋友可以到wx)
在这里插入图片描述
天赐材料

同花顺业绩预告
在这里插入图片描述

四、上代码

1.代码结构

代码结构非常简单:

config.go 配置文件解析器

ths_yjyg_parse.go 网页爬取及解析器

ths_yjyg_task.go 业绩预告任务

run目录下面是程序主入口(yjyg_main)

aes.min.js是同花顺hexinv生成程序

使用go调用js生成hexinv

conf下面的config.yaml 配置文件

另外还有model目录中的数据库对象文件

2.model代码

package model

import "time"

// 同花顺业绩预告

type ThsYjyg struct {
   ID        int       `gorm:"primary_key" orm:"column(id);auto"`
   Code      string    `gorm:"index:code_date_jidu,unique;column:code;type:varchar(20);not null"` // 股票代码
   StockName string    `gorm:"column:stock_name;type:varchar(100);not null"`
   Date      time.Time `gorm:"index:code_date_jidu,unique;column:date;type:date;not null"` // 日期
   Jidu      int       `gorm:"index:code_date_jidu,unique;column:jidu;type:int(4)"`        //季度 1.一季报;2中报;3三季报;4年报
   Year      int       `gorm:"column:year;type:int(8)"`                                    //年
   //YgType    int       `gorm:"index:code_date_type,unique;column:yg_type;type:int(4);not null"` //
   YjLevel      int     `gorm:"column:yj_level;type:int(8);comment:业绩预告级别"`
   YjLevelLabel string  `gorm:"column:yj_level_label;type:varchar(100);not null;comment:业绩预估类型"`
   Sumary       string  `gorm:"column:sumary;type:varchar(300);not null;comment:业绩预告摘要"`
   JlrChange    float64 `gorm:"column:jlr_change;type:decimal(19,2);comment:净利润变动幅度%"`
   LastJlr      float64 `gorm:"column:last_jlr;type:decimal(19,2);comment:上年同期净利润"`
   Created      time.Time
}

3.网页解析器

网页爬取和解析很简单,我们通过分析同花顺原网页调用,发现每次是通过异步请求获取一段html,页面再动态渲染,参数只有两个,一个是季报日期,一个是分页参数,代码如下,网页解析使用了goquery,非常优秀的开源网页解析工具。

package yjyg

import (
   "fmt"
   "github.com/PuerkitoBio/goquery"
  "stockgo/core/global"
	"stockgo/core/util"
	"stockgo/model"
   "go.uber.org/zap"
   "strconv"
   "strings"
   "time"
)

const URL = "https://data.10jqka.com.cn/ajax/yjyg/date/%s/board/ALL/field/enddate/order/desc/page/%d/ajax/1/free/1/"

var YJTYPES = map[string]int{
   "预计增亏":   11,
   "业绩大幅下降": 12,
   "预计续亏":   14,
   "业绩预亏":   16,
   "业绩预降":   18, // 小于20,利空消息

   "不确定": 20,
   "预计减亏": 21,
   "业绩预盈": 25, //这个是中性的

   "预计扭亏":   31, //30 以上是利好消息
   "业绩预增":   34,
   "业绩大幅上升": 35,
}

func GetAndParse(date string, pageNo int, year int ,jidu int, mt *global.HttpLimit) *YjygInfo {
   url := fmt.Sprintf(URL, date, pageNo)
   global.LOG.Info("业绩预告爬取地址: ", zap.String("URL", url))
   doc := util.GetHtmlWithHead(url, util.GetThsHeader(), mt) //获取同花顺hexinv


   if doc == nil {
      return nil
   }
   return ParseData(doc, pageNo, year, jidu)
}

func ParseData(doc *goquery.Document, pageNo int, year int ,jidu int) *YjygInfo {

   pageInfo := getText(doc.Find("body > div.m-page.J-ajax-page > span"))
   // 存在没有页码的情况,第一页不满的情况

   total:=1

   if pageInfo != ""{
      arr := strings.Split(pageInfo, "/")
      total, _ = strconv.Atoi(arr[1])
   }

   yjygInfo := YjygInfo{
      PageNo: pageNo,
      Total:  total,
      List:   make([]model.ThsYjyg, 0),
   }


   doc.Find("body > table > tbody > tr").Each(func(i int, selection *goquery.Selection) {
      yjgg := model.ThsYjyg{
         Code:         util.FullCode(getText(selection.Find(" td:nth-child(2) > a"))),
         StockName:    getText(selection.Find(" td:nth-child(3) > a")),
         YjLevelLabel: getText(selection.Find(" td:nth-child(4) > span")),
         Sumary:       getText(selection.Find("td:nth-child(5) > a")),
         JlrChange:    util.ParseFloatTwo(getText(selection.Find("td:nth-child(6)"))),
         Created:      time.Now(),
         Year:         year,
         Jidu:         jidu,
      }
      yjgg.Date, _ = time.Parse(util.Date_Format, selection.Find("td.tc.cur").Text())
      yjgg.YjLevel = YJTYPES[yjgg.YjLevelLabel]

      lastJlrStr := getText(selection.Find("td:nth-child(7)"))
      yjgg.LastJlr = getLastJlr(lastJlrStr)
      yjygInfo.List = append(yjygInfo.List, yjgg)
   })

   return &yjygInfo
}

func getText(selection *goquery.Selection) string {
   temp := util.DecodeToGBK(selection.Text()) //处理数据编码
   temp = strings.TrimSpace(temp)             //去除多余空格
   //temp = strings.ReplaceAll(temp,"聽"," ") //替换转码导致的乱码,空格变成了聽
   return temp
}

func getLastJlr(jlr string) float64 {
   if jlr == "-" {
      return 0.0
   }
   if strings.Contains(jlr, "亿") {
      jlr = strings.ReplaceAll(jlr, "亿", "")
      return util.ParseFloatTwo(jlr) * 10000
   } else if strings.Contains(jlr, "万") {
      jlr = strings.ReplaceAll(jlr, "万", "")
      return util.ParseFloatTwo(jlr)
   }
   return 0.0

}

type YjygInfo struct {
   PageNo int
   Total  int
   List   []model.ThsYjyg
}

获取同花顺hexinv请求头部参数代码:

解决了绝大多数用户爬取同花顺的难题,使用基于chorme Driver 方式爬取太不方便了。

package util

import (
   "github.com/robertkrimen/otto"
   "io/ioutil"
)

//同花顺util

// 使用js生成核心V参数
func getHexinV() string {
   //伪造hexinv参数

   filePath := "aes.min.js"
   //先读入文件内容
   bytes, err := ioutil.ReadFile(filePath)
   if err != nil {
      panic(err)
   }

   vm := otto.New()
   _, err = vm.Run(string(bytes))
   if err != nil {
      panic(err)
   }
   value, err := vm.Call("v", nil, "")
   if err != nil {
      panic(err)
   }
   return value.String()

}

// 获取一个同花顺头部,每次自动生成一个hexin-v
func GetThsHeader() map[string]string {
   header := make(map[string]string)
   hexinv := getHexinV()
   header["hexin-v"] = hexinv
   header["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36"
   return header
}

4.爬取任务

任务接口

type StockTaskHandler interface {
   GetName() string
   DoTask()
}

业绩预告爬取任务

package yjyg

import (
   "stockgo/core/global"
   "stockgo/core/util"
   "stockgo/model"
   "go.uber.org/zap"
   "strconv"
   "time"
)

var dateSuffixMap = map[int]string{
   1: "-03-31",
   2: "-06-30",
   3: "-09-30",
   4: "-12-31",
}

//业绩预告
// 一季报4月15号之前发布 ,实际有4-30
//半年度业绩预告:报告期在当年7月15日前;  实际8-27
//第三季度业绩预告:报告期为当年10月15日之前 实际11-25 ,大部分在10-30
//年度业绩预告:报告期在次年1月31日前; 4-30 ;
type ThsYjygTask struct {
   Jidu  int    // 一季报1、中报2、三季报3、年报4
   Year  int    // 年,默认使用当年逻辑
   Mode  string // 模式[ALL 全量,UPDATE 增量],爬历史数据用ALL模式
   Limit *global.HttpLimit
}

func (t *ThsYjygTask) GetName() string {
   return "同花顺业绩预告爬取任务"+t.getDate()
}

func (t ThsYjygTask) DoTask() {
   if t.Year ==0 {
      t.Year=time.Now().Year()
   }
   //部分预报不需要一直查询
   if t.Mode != "ALL" && !needRun(t.Jidu, t.Year) {
      return
   }

   dateStr := t.getDate()
   pageNo := 1
   total := 1 //总页数,默认为1,先启动起来,然后爬虫修正总页数
   // 业务逻辑
   // 1.从第一页开始查询,提取出总页数,如果不是下一页,继续PageNO+1 查询
   // 2.和数据库历史数据对比,如果数据库有,则不继续下一页查询(全量模式下除外)

   for pageNo <= total {
      yjygInfo := GetAndParse(dateStr, pageNo, t.Year, t.Jidu, t.Limit)
      total = yjygInfo.Total // 每页返回的total都是一样的
      pageNo++

      // 保存 数据,并判断已存在数目
      repeatCount := saveYjygList(yjygInfo.List)
      if repeatCount > 0 && t.Mode != "ALL" { //增量模式下,如果当前页是否已经存在重复数据,就不需要再查询下一页了
         return
      }
   }

}

// 如果存在重复数据,返回count+1
func saveYjygList(list []model.ThsYjyg) int {

   count := 0
   for _, item := range list {
      if !saveYjyg(item) {
         count++
      }
   }
   return count
}

func saveYjyg(info model.ThsYjyg) bool {
   oldYjyg := model.ThsYjyg{}
   err := global.Table(global.KLineDb, &info).Where("code=? and date=? and jidu=?", info.Code, info.Date.Format(util.Date_Format), info.Jidu).First(&oldYjyg).Error

   if oldYjyg.ID > 0 {
      return false
   }

   err = global.Table(global.KLineDb, &info).Save(&info).Error
   if err != nil {
      global.LOG.Error("save yjyg error : ", zap.Error(err))
   }
   return true
}

//根据时间来判断是否需要爬取
func needRun(typ int, year int) bool {
   now := time.Now()
   month := now.Month()
   if typ == 1 {
      return month < 5 //一季报最多查询到4月份
   } else if typ == 2 {
      return month > 3 && month < 9 //中报3-9月
   } else if typ == 3 {
      return month >= 7 && month < 11 // 三季报7-11月
   } else { // 年报 全年都有可能 ,主要集中在4-30前
      return true //month <=4 || month >=7 //主要集中在下一年4-30前,有些票是当年就发布了
   }

}

func (t ThsYjygTask) getDate() string {
   if t.Year == 0 {
      t.Year = time.Now().Year()
      if time.Now().Month() < 5 && t.Jidu == 4 { //默认情况下,4月份以前的查询的是上一年的年报
         t.Year -= 1
      }
   }
   return strconv.Itoa(t.Year) + dateSuffixMap[t.Jidu]
}

func InitDb() {
   // 初始化数据库,数据库同步
   global.LOG.Debug("初始化数据库表 ............")
   global.AutoMigrate(global.KLineDb, &model.ThsYjyg{})
}

爬取任务有两种模式,一种是爬取历史数据,一种是日常每日进行增量爬取,代码非常简单,100行左右

5.主程序

package main

import (
   "stockgo/core/engine"
   "stockgo/core/global"
   "stockgo/core/task"
   "stockgo/craw/ths/yjyg"
)

func main() {

   var conf yjyg.Conf
   engine.Start("", &conf)
   defer engine.Close()

   yjyg.InitDb() //自动初始化数据库
   tasks := task.NewStockTasks()
   for _, jidu := range conf.Config.Types {
      yjygTask := &yjyg.ThsYjygTask{ //同花顺业绩预告任务
         Mode:  conf.Config.Mode,
         Year:  conf.Config.Year,
         Jidu:  jidu,
         Limit: global.Limit,
      }

      tasks = append(tasks, task.CreateStockTask("同花顺数据爬取--", yjygTask))
   }

   task.RunStockTask(tasks) //并发运行

}

主程序非常简洁,主要分为几步:

(1) engine引擎加载配置文件并自动连接数据库,并再主程序结束时自动关闭数据库等

(2) 自动初始化数据库,主要是自动生成数据库表

(3)根据配置文件循环创建爬虫任务,并将任务放入task运行器中并行运行

再看看配置文件

# StockGo Global Configuration

# system configuration
system:
  name: 同花顺业绩预告数据爬取任务
  taskNum: 10  #并发任务数目
  limitSped: 3000  #网络请求限制,3000ms允许发出一个请求

# 数据库配置,至少配置一个default,默认数据库
mysql:
  - name: default
    showSql: false
    dataSource: 'stockdev:stock123456@tcp(db.stock.com:3306)/stock_data?charset=utf8&parseTime=True&loc=Local'
  - name: kline
    showSql: true
    dataSource: 'stockdev:stock123456@tcp(db.stock.com:3306)/stock_data?charset=utf8&parseTime=True&loc=Local'

#业绩预告爬取数据,当补全历史数据时候,配置对应年份,mode 设置为ALL,
#其他自动爬取时候,去掉配置,使用空值
yjyg:
  year: 0  # 默认0使用系统时间year
  mode: UPDATE   #ALL 查询所有,全量查询,其他值[update] 增量更新
  types: 1,2,3,4   # 1,2,3,4 季报类型


# zap logger configuration(zap 日志配置)
zap:
  # 可使用 "debug", "info", "warn", "error", "dpanic", "panic", "fatal",
  level: 'info'
  # console: 控制台, json: json格式输出
  format: 'console'
  prefix: '[StockGo]'
  director: 'log'
  link-name: 'latest.log'
  show-line: true
  # LowercaseLevelEncoder:小写, LowercaseColorLevelEncoder:小写带颜色,CapitalLevelEncoder: 大写, CapitalColorLevelEncoder: 大写带颜色,
  encode-level: 'LowercaseColorLevelEncoder'
  stacktrace-key: 'stacktrace'
  log-in-console: true

config.go

package yjyg

import "stockgo/core/config"

type Config struct {
   Year  int    `mapstructure:"year" json:"year" yaml:"year"`
   Mode  string `mapstructure:"mode" json:"mode" yaml:"mode"`
   Types []int  `mapstructure:"types" json:"types" yaml:"types"`
}

type Conf struct {
   Zap       config.Zap     `mapstructure:"zap" json:"zap" yaml:"zap"`
   System    config.System  `mapstructure:"system" json:"system" yaml:"system"`
   MysqlList []config.Mysql `mapstructure:"mysql" json:"mysql" yaml:"mysql"`
   Config    Config         `mapstructure:"yjyg" json:"yjyg" yaml:"yjyg"`
}

func (bc *Conf) GetZap() *config.Zap {
   return &bc.Zap
}
func (bc *Conf) GetSystem() *config.System {
   return &bc.System
}

func (bc *Conf) GetMysql() []config.Mysql {
   return bc.MysqlList
}

6.运行日志

[StockGo]2021/08/27 - 15:57:55.604      info    服务启动        {"name": "同花顺业绩预告数据爬取任务"}
[StockGo]2021/08/27 - 15:57:55.605      info    数据库注册      {"Name": "default", "url": "stockdev:stock123456@tcp(db.stock.com:3306)/stock_data?charset=utf8&parseTime=True&loc=Local", "isLog": false}
[StockGo]2021/08/27 - 15:57:55.606      info    数据库注册      {"Name": "kline", "url": "stockdev:stock123456@tcp(db.stock.com:3306)/stock_data?charset=utf8&parseTime=True&loc=Local", "isLog": true}
[StockGo]2021/08/27 - 15:57:55.607      info     9 begin Task 同花顺数据爬取--  同花顺业绩预告爬取任务2021-03-31 0 
[StockGo]2021/08/27 - 15:57:55.607      info     9 end Task 同花顺数据爬取--  同花顺业绩预告爬取任务2021-03-31 0 
[StockGo]2021/08/27 - 15:57:55.607      info     6 begin Task 同花顺数据爬取--  同花顺业绩预告爬取任务2021-09-30 2 
[StockGo]2021/08/27 - 15:57:55.607      info     4 begin Task 同花顺数据爬取--  同花顺业绩预告爬取任务2021-12-31 3 
[StockGo]2021/08/27 - 15:57:55.607      info    业绩预告爬取地址:      {"URL": "https://data.10jqka.com.cn/ajax/yjyg/date/2021-09-30/board/ALL/field/enddate/order/desc/page/1/ajax/1/free/1/"}
[StockGo]2021/08/27 - 15:57:55.607      info    业绩预告爬取地址:      {"URL": "https://data.10jqka.com.cn/ajax/yjyg/date/2021-12-31/board/ALL/field/enddate/order/desc/page/1/ajax/1/free/1/"}
[StockGo]2021/08/27 - 15:57:55.607      info     3 begin Task 同花顺数据爬取--  同花顺业绩预告爬取任务2021-06-30 1 
[StockGo]2021/08/27 - 15:57:55.607      info    业绩预告爬取地址:      {"URL": "https://data.10jqka.com.cn/ajax/yjyg/date/2021-06-30/board/ALL/field/enddate/order/desc/page/1/ajax/1/free/1/"}
[StockGo]2021/08/27 - 15:57:55.950      info     4 end Task 同花顺数据爬取--  同花顺业绩预告爬取任务2021-12-31 3 
[StockGo]2021/08/27 - 15:57:59.605      info     6 end Task 同花顺数据爬取--  同花顺业绩预告爬取任务2021-09-30 2 
[StockGo]2021/08/27 - 15:58:01.818      info     3 end Task 同花顺数据爬取--  同花顺业绩预告爬取任务2021-06-30 1 
[StockGo]2021/08/27 - 15:58:01.818      info    运行总时长:46 秒

7.数据爬取之后,需要做页面进行数据展示

数据查询和展示框架基于gin-vue-admin二次开发,添加了avue,采用自动配置,自动生成的方式,基本5分钟内搞定,就是页面不够精细,不过也足够使用了,这里不进行多讲,如果有朋友有兴趣,后续我专门写一篇文章介绍。

五、最后

同花顺业绩预报数据是数据爬取中比较简单的,本人框架专注研究欧奈尔和落升,感兴趣的朋友可以一起交流,后续可能也会将当前策略筛选出来的股票分享给大家。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值