From 797a35eaa58a02cf05a4675f4be00f6b5291db5b Mon Sep 17 00:00:00 2001 From: ArvinLovegood Date: Tue, 25 Feb 2025 22:07:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(stock-data):=E6=B7=BB=E5=8A=A0=E5=B1=8F?= =?UTF-8?q?=E5=B9=95=E5=88=86=E8=BE=A8=E7=8E=87=E9=80=82=E9=85=8D=EF=BC=8C?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E8=B0=83=E6=95=B4=E5=BA=94=E7=94=A8=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E5=A4=A7=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 GetRealTimeStockPriceInfo 函数,用于获取指定股票的实时价格和时间 - 优化爬虫配置,提高数据抓取效率 - 添加屏幕分辨率适配,动态调整应用窗口大小 - 修复部分股票代码格式问题,确保数据准确性 --- README.md | 11 ++--- app.go | 10 +++++ backend/data/crawler_api.go | 69 +++++++++++++++++++++++++++++ backend/data/stock_data_api.go | 39 ++++++++++++++++ backend/data/stock_data_api_test.go | 36 +++++++++++++-- main.go | 21 ++++++--- 6 files changed, 173 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6e19829..60a9fc2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,12 @@ - 本项目仅供娱乐,不喜勿喷,AI分析股票结果仅供学习研究,投资有风险,请谨慎使用。 - 开发环境主要基于Windows10+,其他平台未测试或功能受限。 -### 💬 大模型 +### 📦 立即体验 +- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases) +- 绿色版:[go-stock-windows-amd64.exe](https://github.com/ArvinLovegood/go-stock/releases) + + +### 💬 支持大模型/平台 | 模型 | 状态 | 备注 | | --- | --- |-------------------------------------------| | [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 | @@ -47,10 +52,6 @@ ### 2025.02.12 可配置的提问模板 - [v2025.2.12.7-alpha](https://github.com/ArvinLovegood/go-stock/releases/tag/v2025.2.12.7-alpha) -## 📦 立即体验 -- 安装版:[go-stock-amd64-installer.exe](https://github.com/ArvinLovegood/go-stock/releases) -- 绿色版:[go-stock-windows-amd64.exe](https://github.com/ArvinLovegood/go-stock/releases) - ## 🦄 重大更新 ### BIG NEWS !!! 重大更新!!! diff --git a/app.go b/app.go index d2fc8f0..7bad630 100644 --- a/app.go +++ b/app.go @@ -21,6 +21,7 @@ import ( "golang.org/x/sys/windows/registry" "os" "strings" + "syscall" "time" ) @@ -638,3 +639,12 @@ func (a *App) ExportConfig() string { } return "导出成功:" + file } +func getScreenResolution() (int, int, error) { + user32 := syscall.NewLazyDLL("user32.dll") + getSystemMetrics := user32.NewProc("GetSystemMetrics") + + width, _, _ := getSystemMetrics.Call(0) + height, _, _ := getSystemMetrics.Call(1) + + return int(width), int(height), nil +} diff --git a/backend/data/crawler_api.go b/backend/data/crawler_api.go index a516505..7a5c353 100644 --- a/backend/data/crawler_api.go +++ b/backend/data/crawler_api.go @@ -38,6 +38,7 @@ func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bo c.crawlerCtx, chromedp.ExecPath(path), chromedp.Flag("headless", headless), + chromedp.Flag("blink-settings", "imagesEnabled=false"), chromedp.Flag("disable-javascript", false), chromedp.Flag("disable-gpu", true), chromedp.UserAgent(c.crawlerBaseInfo.Headers["User-Agent"]), @@ -88,6 +89,73 @@ func (c *CrawlerApi) GetHtml(url, waitVisible string, headless bool) (string, bo return htmlContent, true } + +func (c *CrawlerApi) GetHtmlWithNoCancel(url, waitVisible string, headless bool) (html string, ok bool, parent context.CancelFunc, child context.CancelFunc) { + htmlContent := "" + path, e := checkBrowserOnWindows() + logger.SugaredLogger.Infof("GetHtml path:%s", path) + var parentCancel context.CancelFunc + var childCancel context.CancelFunc + var pctx context.Context + var cctx context.Context + + if e { + pctx, parentCancel = chromedp.NewExecAllocator( + c.crawlerCtx, + chromedp.ExecPath(path), + chromedp.Flag("headless", headless), + chromedp.Flag("blink-settings", "imagesEnabled=false"), + chromedp.Flag("disable-javascript", false), + chromedp.Flag("disable-gpu", true), + chromedp.UserAgent(c.crawlerBaseInfo.Headers["User-Agent"]), + chromedp.Flag("disable-background-networking", true), + chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"), + chromedp.Flag("disable-background-timer-throttling", true), + chromedp.Flag("disable-backgrounding-occluded-windows", true), + chromedp.Flag("disable-breakpad", true), + chromedp.Flag("disable-client-side-phishing-detection", true), + chromedp.Flag("disable-default-apps", true), + chromedp.Flag("disable-dev-shm-usage", true), + chromedp.Flag("disable-extensions", true), + chromedp.Flag("disable-features", "site-per-process,Translate,BlinkGenPropertyTrees"), + chromedp.Flag("disable-hang-monitor", true), + chromedp.Flag("disable-ipc-flooding-protection", true), + chromedp.Flag("disable-popup-blocking", true), + chromedp.Flag("disable-prompt-on-repost", true), + chromedp.Flag("disable-renderer-backgrounding", true), + chromedp.Flag("disable-sync", true), + chromedp.Flag("force-color-profile", "srgb"), + chromedp.Flag("metrics-recording-only", true), + chromedp.Flag("safebrowsing-disable-auto-update", true), + chromedp.Flag("enable-automation", true), + chromedp.Flag("password-store", "basic"), + chromedp.Flag("use-mock-keychain", true), + ) + //defer pcancel() + cctx, childCancel = chromedp.NewContext(pctx, chromedp.WithLogf(logger.SugaredLogger.Infof)) + //defer cancel() + err := chromedp.Run(cctx, chromedp.Navigate(url), + chromedp.WaitVisible(waitVisible, chromedp.ByQuery), // 确保 元素可见 + chromedp.WaitReady(waitVisible, chromedp.ByQuery), // 确保 元素准备好 + chromedp.InnerHTML("body", &htmlContent), + ) + if err != nil { + logger.SugaredLogger.Error(err.Error()) + return "", false, parentCancel, childCancel + } + } else { + cctx, childCancel = chromedp.NewContext(c.crawlerCtx, chromedp.WithLogf(logger.SugaredLogger.Infof)) + //defer cancel() + err := chromedp.Run(cctx, chromedp.Navigate(url), chromedp.WaitVisible("body"), chromedp.InnerHTML("body", &htmlContent)) + if err != nil { + logger.SugaredLogger.Error(err.Error()) + return "", false, parentCancel, childCancel + } + } + return htmlContent, true, parentCancel, childCancel + +} + func (c *CrawlerApi) GetHtmlWithActions(actions *[]chromedp.Action, headless bool) (string, bool) { htmlContent := "" *actions = append(*actions, chromedp.InnerHTML("body", &htmlContent)) @@ -99,6 +167,7 @@ func (c *CrawlerApi) GetHtmlWithActions(actions *[]chromedp.Action, headless boo c.crawlerCtx, chromedp.ExecPath(path), chromedp.Flag("headless", headless), + chromedp.Flag("blink-settings", "imagesEnabled=false"), chromedp.Flag("disable-javascript", false), chromedp.Flag("disable-gpu", true), chromedp.UserAgent(c.crawlerBaseInfo.Headers["User-Agent"]), diff --git a/backend/data/stock_data_api.go b/backend/data/stock_data_api.go index 20ecd39..d88e21f 100644 --- a/backend/data/stock_data_api.go +++ b/backend/data/stock_data_api.go @@ -585,6 +585,45 @@ func (IndexBasic) TableName() string { return "tushare_index_basic" } +type RealTimeStockPriceInfo struct { + StockCode string + Price string `json:"当前价格"` + Time time.Time +} + +func GetRealTimeStockPriceInfo(ctx context.Context, stockCode string) (price, priceTime string) { + if strutil.HasPrefixAny(stockCode, []string{"SZ", "SH", "sh", "sz"}) { + crawlerAPI := CrawlerApi{} + crawlerBaseInfo := CrawlerBaseInfo{ + Name: "EastmoneyCrawler", + Description: "EastmoneyCrawler Description", + BaseUrl: "https://quote.eastmoney.com/", + Headers: map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"}, + } + crawlerAPI = crawlerAPI.NewCrawler(ctx, crawlerBaseInfo) + htmlContent, ok := crawlerAPI.GetHtml(fmt.Sprintf("https://quote.eastmoney.com/%s.html", stockCode), "div.zxj", true) + if ok { + price := "" + priceTime := "" + document, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent)) + if err != nil { + logger.SugaredLogger.Errorf("GetRealTimeStockPriceInfo error: %v", err) + } + document.Find("div.zxj").Each(func(i int, selection *goquery.Selection) { + price = selection.Text() + logger.SugaredLogger.Infof("股票代码: %s, 当前价格: %s", stockCode, price) + }) + + document.Find("span.quote_title_time").Each(func(i int, selection *goquery.Selection) { + priceTime = selection.Text() + logger.SugaredLogger.Infof("股票代码: %s, 当前价格时间: %s", stockCode, priceTime) + }) + return price, priceTime + } + } + return price, priceTime +} + func SearchStockPriceInfo(stockCode string, crawlTimeOut int64) *[]string { if strutil.HasPrefixAny(stockCode, []string{"HK", "hk"}) { diff --git a/backend/data/stock_data_api_test.go b/backend/data/stock_data_api_test.go index 28c793e..b2a6a24 100644 --- a/backend/data/stock_data_api_test.go +++ b/backend/data/stock_data_api_test.go @@ -1,6 +1,7 @@ package data import ( + "context" "encoding/json" "fmt" "github.com/duke-git/lancet/v2/convertor" @@ -9,6 +10,7 @@ import ( "go-stock/backend/db" "go-stock/backend/logger" "io/ioutil" + "regexp" "strings" "testing" "time" @@ -43,8 +45,36 @@ func TestSearchStockInfoByCode(t *testing.T) { } func TestSearchStockPriceInfo(t *testing.T) { - SearchStockPriceInfo("hk06030", 30) - //SearchStockPriceInfo("sh600859", 30) + //SearchStockPriceInfo("hk06030", 30) + SearchStockPriceInfo("sh600171", 30) +} + +func TestGetRealTimeStockPriceInfo(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + text, texttime := GetRealTimeStockPriceInfo(ctx, "sh600171") + logger.SugaredLogger.Infof("res:%s,%s", text, texttime) + + text, texttime = GetRealTimeStockPriceInfo(ctx, "sh600438") + logger.SugaredLogger.Infof("res:%s,%s", text, texttime) + + texttime = strings.ReplaceAll(texttime, ")", "") + texttime = strings.ReplaceAll(texttime, "(", "") + parts := strings.Split(texttime, " ") + logger.SugaredLogger.Infof("parts:%+v", parts) + + //去除中文字符 + // 正则表达式匹配中文字符 + re := regexp.MustCompile(`\p{Han}+`) + texttime = re.ReplaceAllString(texttime, "") + + logger.SugaredLogger.Infof("texttime:%s", texttime) + location, err := time.ParseInLocation("2006-01-02 15:04:05", texttime, time.Local) + if err != nil { + return + } + logger.SugaredLogger.Infof("location:%s", location.Format("2006-01-02 15:04:05")) } func TestParseFullSingleStockData(t *testing.T) { @@ -52,7 +82,7 @@ func TestParseFullSingleStockData(t *testing.T) { SetHeader("Host", "hq.sinajs.cn"). SetHeader("Referer", "https://finance.sina.com.cn/"). SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0"). - Get(fmt.Sprintf(sinaStockUrl, time.Now().Unix(), "sh600859,sz000034,hk01810,hk00856")) + Get(fmt.Sprintf(sinaStockUrl, time.Now().Unix(), "sh600584,sz000938,hk01810,hk00856")) if err != nil { logger.SugaredLogger.Error(err.Error()) } diff --git a/main.go b/main.go index 5c5c8cd..be0bdb9 100644 --- a/main.go +++ b/main.go @@ -107,14 +107,25 @@ func main() { logger.NewDefaultLogger().Info("version: " + Version) logger.NewDefaultLogger().Info("commit: " + VersionCommit) // Create application with options - err := wails.Run(&options.App{ + var width, height int + var err error + + width, height, err = getScreenResolution() + if err != nil { + logger.NewDefaultLogger().Error("get screen resolution error") + width = 1366 + height = 768 + } + + // Create application with options + err = wails.Run(&options.App{ Title: "go-stock", - Width: 1366, - Height: 920, + Width: width * 2 / 3, + Height: height * 2 / 3, MinWidth: 1024, MinHeight: 768, - MaxWidth: 1920, - MaxHeight: 960, + MaxWidth: width, + MaxHeight: height, DisableResize: false, Fullscreen: false, Frameless: true,