From f6d217e4fdbf23f9d4f1340728b1b5eb3f74fe6f Mon Sep 17 00:00:00 2001 From: ArvinLovegood Date: Fri, 20 Jun 2025 11:33:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(analyze):=20=E6=B7=BB=E5=8A=A0=E6=83=85?= =?UTF-8?q?=E6=84=9F=E5=88=86=E6=9E=90=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=96=B0=E9=97=BB=E6=8E=A8=E9=80=81=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 App.vue 中添加情感分析相关的导入和使用 - 在 app_common.go 中实现 AnalyzeSentiment 方法- 在 market_news_api.go 和 models.go 中集成情感分析结果 - 更新前端通知显示,根据情感分析结果调整通知类型和样式 - 在 go.mod 中添加 gojieba 依赖用于情感分析 --- app_common.go | 4 + backend/data/market_news_api.go | 2 + backend/data/stock_sentiment_analysis.go | 282 ++++++++++++++++++ backend/data/stock_sentiment_analysis_test.go | 31 ++ backend/models/models.go | 17 +- frontend/src/App.vue | 29 +- frontend/wailsjs/go/main/App.d.ts | 2 + frontend/wailsjs/go/main/App.js | 4 + frontend/wailsjs/go/models.ts | 20 ++ go.mod | 1 + go.sum | 2 + 11 files changed, 381 insertions(+), 13 deletions(-) create mode 100644 backend/data/stock_sentiment_analysis.go create mode 100644 backend/data/stock_sentiment_analysis_test.go diff --git a/app_common.go b/app_common.go index d6d7e58..a73a611 100644 --- a/app_common.go +++ b/app_common.go @@ -27,3 +27,7 @@ func (a *App) IndustryResearchReport(industryCode string) []any { func (a App) EMDictCode(code string) []any { return data.NewMarketNewsApi().EMDictCode(code, a.cache) } + +func (a App) AnalyzeSentiment(text string) data.SentimentResult { + return data.AnalyzeSentiment(text) +} diff --git a/backend/data/market_news_api.go b/backend/data/market_news_api.go index b8f144a..628dd85 100644 --- a/backend/data/market_news_api.go +++ b/backend/data/market_news_api.go @@ -73,6 +73,7 @@ func (m MarketNewsApi) GetNewTelegraph(crawlTimeOut int64) *[]models.Telegraph { //telegraph = append(telegraph, ReplaceSensitiveWords(selection.Text())) if telegraph.Content != "" { + telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description cnt := int64(0) db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt) if cnt == 0 { @@ -192,6 +193,7 @@ func (m MarketNewsApi) GetSinaNews(crawlTimeOut uint) *[]models.Telegraph { logger.SugaredLogger.Infof("telegraph.SubjectTags:%v %s", telegraph.SubjectTags, telegraph.Content) if telegraph.Content != "" { + telegraph.SentimentResult = AnalyzeSentiment(telegraph.Content).Description cnt := int64(0) db.Dao.Model(telegraph).Where("time=? and source=?", telegraph.Time, telegraph.Source).Count(&cnt) if cnt == 0 { diff --git a/backend/data/stock_sentiment_analysis.go b/backend/data/stock_sentiment_analysis.go new file mode 100644 index 0000000..a74d01b --- /dev/null +++ b/backend/data/stock_sentiment_analysis.go @@ -0,0 +1,282 @@ +package data + +import ( + "bufio" + "fmt" + "github.com/yanyiwu/gojieba" + "os" + "strings" +) + +// 金融情感词典,包含股票市场相关的专业词汇 +var ( + // 正面金融词汇及其权重 + positiveFinanceWords = map[string]float64{ + "上涨": 2.0, "涨停": 3.0, "牛市": 3.0, "反弹": 2.0, "新高": 2.5, + "利好": 2.5, "增持": 2.0, "买入": 2.0, "推荐": 1.5, "看多": 2.0, + "盈利": 2.0, "增长": 2.0, "超预期": 2.5, "强劲": 1.5, "回升": 1.5, + "复苏": 2.0, "突破": 2.0, "创新高": 3.0, "回暖": 1.5, "上扬": 1.5, + "利好消息": 3.0, "收益增长": 2.5, "利润增长": 2.5, "业绩优异": 2.5, + "潜力股": 2.0, "绩优股": 2.0, "强势": 1.5, "走高": 1.5, "攀升": 1.5, + "大涨": 2.5, "飙升": 3.0, "井喷": 3.0, "爆发": 2.5, "暴涨": 3.0, + } + + // 负面金融词汇及其权重 + negativeFinanceWords = map[string]float64{ + "下跌": 2.0, "跌停": 3.0, "熊市": 3.0, "回调": 1.5, "新低": 2.5, + "利空": 2.5, "减持": 2.0, "卖出": 2.0, "看空": 2.0, "亏损": 2.5, + "下滑": 2.0, "萎缩": 2.0, "不及预期": 2.5, "疲软": 1.5, "恶化": 2.0, + "衰退": 2.0, "跌破": 2.0, "创新低": 3.0, "走弱": 1.5, "下挫": 1.5, + "利空消息": 3.0, "收益下降": 2.5, "利润下滑": 2.5, "业绩不佳": 2.5, + "垃圾股": 2.0, "风险股": 2.0, "弱势": 1.5, "走低": 1.5, "缩量": 2.5, + "大跌": 2.5, "暴跌": 3.0, "崩盘": 3.0, "跳水": 3.0, "重挫": 3.0, + } + + // 否定词,用于反转情感极性 + negationWords = map[string]struct{}{ + "不": {}, "没": {}, "无": {}, "非": {}, "未": {}, "别": {}, "勿": {}, + } + + // 程度副词,用于调整情感强度 + degreeWords = map[string]float64{ + "非常": 1.8, "极其": 2.2, "太": 1.8, "很": 1.5, + "比较": 0.8, "稍微": 0.6, "有点": 0.7, "显著": 1.5, + "大幅": 1.8, "急剧": 2.0, "轻微": 0.6, "小幅": 0.7, + } + + // 转折词,用于识别情感转折 + transitionWords = map[string]struct{}{ + "但是": {}, "然而": {}, "不过": {}, "却": {}, "可是": {}, + } +) + +// SentimentResult 情感分析结果类型 +type SentimentResult struct { + Score float64 // 情感得分 + Category SentimentType // 情感类别 + PositiveCount int // 正面词数量 + NegativeCount int // 负面词数量 + Description string // 情感描述 +} + +// SentimentType 情感类型枚举 +type SentimentType int + +const ( + Positive SentimentType = iota + Negative + Neutral +) + +// AnalyzeSentiment 判断文本的情感 +func AnalyzeSentiment(text string) SentimentResult { + // 初始化得分 + score := 0.0 + positiveCount := 0 + negativeCount := 0 + + // 分词(简单按单个字符分割) + words := splitWords(text) + + // 检查文本是否包含转折词,并分割成两部分 + var transitionIndex int + var hasTransition bool + for i, word := range words { + if _, ok := transitionWords[word]; ok { + transitionIndex = i + hasTransition = true + break + } + } + + // 处理有转折的文本 + if hasTransition { + // 转折前的部分 + preTransitionWords := words[:transitionIndex] + preScore, prePos, preNeg := calculateScore(preTransitionWords) + + // 转折后的部分,权重加倍 + postTransitionWords := words[transitionIndex+1:] + postScore, postPos, postNeg := calculateScore(postTransitionWords) + postScore *= 1.5 // 转折后的情感更重要 + + score = preScore + postScore + positiveCount = prePos + postPos + negativeCount = preNeg + postNeg + } else { + // 没有转折的文本 + score, positiveCount, negativeCount = calculateScore(words) + } + + // 确定情感类别 + var category SentimentType + switch { + case score > 1.0: + category = Positive + case score < -1.0: + category = Negative + default: + category = Neutral + } + + return SentimentResult{ + Score: score, + Category: category, + PositiveCount: positiveCount, + NegativeCount: negativeCount, + Description: GetSentimentDescription(category), + } +} + +// 计算情感得分 +func calculateScore(words []string) (float64, int, int) { + score := 0.0 + positiveCount := 0 + negativeCount := 0 + + // 遍历每个词,计算情感得分 + for i, word := range words { + // 首先检查是否为程度副词 + degree, isDegree := degreeWords[word] + + // 检查是否为否定词 + _, isNegation := negationWords[word] + + // 检查是否为金融正面词 + if posScore, isPositive := positiveFinanceWords[word]; isPositive { + // 检查前一个词是否为否定词或程度副词 + if i > 0 { + prevWord := words[i-1] + if _, isNeg := negationWords[prevWord]; isNeg { + score -= posScore + negativeCount++ + continue + } + + if deg, isDeg := degreeWords[prevWord]; isDeg { + score += posScore * deg + positiveCount++ + continue + } + } + + score += posScore + positiveCount++ + continue + } + + // 检查是否为金融负面词 + if negScore, isNegative := negativeFinanceWords[word]; isNegative { + // 检查前一个词是否为否定词或程度副词 + if i > 0 { + prevWord := words[i-1] + if _, isNeg := negationWords[prevWord]; isNeg { + score += negScore + positiveCount++ + continue + } + + if deg, isDeg := degreeWords[prevWord]; isDeg { + score -= negScore * deg + negativeCount++ + continue + } + } + + score -= negScore + negativeCount++ + continue + } + + // 处理程度副词(如果后面跟着情感词) + if isDegree && i+1 < len(words) { + nextWord := words[i+1] + + if posScore, isPositive := positiveFinanceWords[nextWord]; isPositive { + score += posScore * degree + positiveCount++ + continue + } + + if negScore, isNegative := negativeFinanceWords[nextWord]; isNegative { + score -= negScore * degree + negativeCount++ + continue + } + } + + // 处理否定词(如果后面跟着情感词) + if isNegation && i+1 < len(words) { + nextWord := words[i+1] + + if posScore, isPositive := positiveFinanceWords[nextWord]; isPositive { + score -= posScore + negativeCount++ + continue + } + + if negScore, isNegative := negativeFinanceWords[nextWord]; isNegative { + score += negScore + positiveCount++ + continue + } + } + } + + return score, positiveCount, negativeCount +} + +// 简单的分词函数,考虑了中文和英文 +func splitWords(text string) []string { + x := gojieba.NewJieba() + defer x.Free() + + return x.Cut(text, true) +} + +// GetSentimentDescription 获取情感类别的文本描述 +func GetSentimentDescription(category SentimentType) string { + switch category { + case Positive: + return "看涨" + case Negative: + return "看跌" + case Neutral: + return "中性" + default: + return "未知" + } +} + +func main() { + // 从命令行读取输入 + reader := bufio.NewReader(os.Stdin) + fmt.Println("请输入要分析的股市相关文本(输入exit退出):") + + for { + fmt.Print("> ") + text, err := reader.ReadString('\n') + if err != nil { + fmt.Println("读取输入时出错:", err) + continue + } + + // 去除换行符 + text = strings.TrimSpace(text) + + // 检查是否退出 + if text == "exit" { + break + } + + // 分析情感 + result := AnalyzeSentiment(text) + + // 输出结果 + fmt.Printf("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n", + GetSentimentDescription(result.Category), + result.Score, + result.PositiveCount, + result.NegativeCount) + } +} diff --git a/backend/data/stock_sentiment_analysis_test.go b/backend/data/stock_sentiment_analysis_test.go new file mode 100644 index 0000000..3779b5e --- /dev/null +++ b/backend/data/stock_sentiment_analysis_test.go @@ -0,0 +1,31 @@ +package data + +import ( + "fmt" + "testing" +) + +// @Author spark +// @Date 2025/6/19 13:05 +// @Desc +//----------------------------------------------------------------------------------- + +func TestAnalyzeSentiment(t *testing.T) { + // 分析情感 + text := " 【调查:韩国近两成中小学生过度使用智能手机或互联网】财联社6月19日电,韩国女性家族部18日公布的一项年度调查结果显示,接受调查的韩国中小学生中,共计约17.3%、即超过21万人使用智能手机或互联网的程度达到了“危险水平”,这意味着他们因过度依赖智能手机或互联网而需要关注或干预,这一比例引人担忧。 (新华社)\n" + text = "消息人士称,联合利华(Unilever)正在为Graze零食品牌寻找买家。\n" + text = "【韩国未来5年将投入51万亿韩元发展文化产业】 据韩联社,韩国文化体育观光部(文体部)今后5年将投入51万亿韩元(约合人民币2667亿元)预算,落实总统李在明在竞选时期提出的“将韩国打造成全球五大文化强国之一”的承诺。\n" + //text = "【油气股持续拉升 国际实业午后涨停】财联社6月19日电,油气股午后持续拉升,国际实业、宝莫股份午后涨停,准油股份、山东墨龙。茂化实华此前涨停,通源石油、海默科技、贝肯能源、中曼石油、科力股份等多股涨超5%。\n" + //text = " 【三大指数均跌逾1% 下跌个股近4800只】财联社6月19日电,指数持续走弱,沪指下挫跌逾1.00%,深成指跌1.25%,创业板指跌1.39%。核聚变、风电、军工、食品消费等板块指数跌幅居前,沪深京三市下跌个股近4800只。\n" + text = "【银行理财首单网下打新落地】财联社6月20日电,记者从多渠道获悉,光大理财以申报价格17元参与信通电子网下打新,并成功入围有效报价,成为行业内首家参与网下打新的银行理财公司。光大理财工作人员向证券时报记者表示,本次光大理财是以其管理的混合类产品“阳光橙增盈绝对收益策略”参与了此次网下打新,该产品为光大理财“固收+”银行理财产品。资料显示,信通电子成立于1996年,核心产品包括输电线路智能巡检系统、变电站智能辅控系统、移动智能终端及其他产品。根据其招股说明书,信通电子2023、2024年营业收入分别较上年增长19.08%和7.97%,净利润分别较上年增长5.6%和15.11%。 (证券时报)" + text = " 【以军称拦截数枚伊朗导弹】财联社6月20日电,据央视新闻报道,以军在贝尔谢巴及周边区域拦截了数枚伊朗导弹,但仍有导弹或拦截残骸落地。以色列国防军发文表示,搜救队伍正在一处“空中物体落地”的所在区域开展工作,公众目前可以离开避难场所。伊朗方面对上述说法暂无回应。" + result := AnalyzeSentiment(text) + + // 输出结果 + fmt.Printf("情感分析结果: %s (得分: %.2f, 正面词:%d, 负面词:%d)\n", + result.Description, + result.Score, + result.PositiveCount, + result.NegativeCount) + +} diff --git a/backend/models/models.go b/backend/models/models.go index b30312b..4c2fe64 100644 --- a/backend/models/models.go +++ b/backend/models/models.go @@ -218,14 +218,15 @@ type Prompt struct { type Telegraph struct { gorm.Model - Time string `json:"time"` - Content string `json:"content"` - SubjectTags []string `json:"subjects" gorm:"-:all"` - StocksTags []string `json:"stocks" gorm:"-:all"` - IsRed bool `json:"isRed"` - Url string `json:"url"` - Source string `json:"source"` - TelegraphTags []TelegraphTags `json:"tags" gorm:"-:migration;foreignKey:TelegraphId"` + Time string `json:"time"` + Content string `json:"content"` + SubjectTags []string `json:"subjects" gorm:"-:all"` + StocksTags []string `json:"stocks" gorm:"-:all"` + IsRed bool `json:"isRed"` + Url string `json:"url"` + Source string `json:"source"` + TelegraphTags []TelegraphTags `json:"tags" gorm:"-:migration;foreignKey:TelegraphId"` + SentimentResult string `json:"sentimentResult" gorm:"-:all"` } type TelegraphTags struct { gorm.Model diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 56ba11e..930966c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -16,18 +16,18 @@ import { AnalyticsOutline, BarChartSharp, EaselSharp, ExpandOutline, Flag, - Flame, FlameSharp, + Flame, FlameSharp, InformationOutline, LogoGithub, NewspaperOutline, - NewspaperSharp, + NewspaperSharp, Notifications, PowerOutline, Pulse, ReorderTwoOutline, SettingsOutline, Skull, SkullOutline, SkullSharp, SparklesOutline, StarOutline, - Wallet, + Wallet, WarningOutline, } from '@vicons/ionicons5' -import {GetConfig, GetGroupList} from "../wailsjs/go/main/App"; +import {AnalyzeSentiment, GetConfig, GetGroupList} from "../wailsjs/go/main/App"; @@ -547,7 +547,26 @@ onMounted(() => { }, }) EventsOn("newsPush", (data) => { - notification.create({ title: data.time, content: data.content,duration:1000*60 }) + //console.log(data) + if(data.isRed){ + notification.create({ + //type:"error", + // avatar: () => h(NIcon,{component:Notifications,color:"red"}), + title: data.time, + content: () => h(NText,{type:"error"}, { default: () => data.content }), + meta: () => h(NText,{type:"warning"}, { default: () => data.source}), + duration:1000*40, + }) + }else{ + notification.create({ + //type:"info", + //avatar: () => h(NIcon,{component:Notifications}), + title: data.time, + content: () => h(NText,{type:"info"}, { default: () => data.content }), + meta: () => h(NText,{type:"warning"}, { default: () => data.source}), + duration:1000*30 , + }) + } }) }) }) diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index b2c0c97..4e2757e 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -11,6 +11,8 @@ export function AddPrompt(arg1:models.Prompt):Promise; export function AddStockGroup(arg1:number,arg2:string):Promise; +export function AnalyzeSentiment(arg1:string):Promise; + export function CheckUpdate():Promise; export function DelPrompt(arg1:number):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index cb19f3f..27d7441 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -18,6 +18,10 @@ export function AddStockGroup(arg1, arg2) { return window['go']['main']['App']['AddStockGroup'](arg1, arg2); } +export function AnalyzeSentiment(arg1) { + return window['go']['main']['App']['AnalyzeSentiment'](arg1); +} + export function CheckUpdate() { return window['go']['main']['App']['CheckUpdate'](); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 39717de..972cfa8 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -290,6 +290,26 @@ export namespace data { + export class SentimentResult { + Score: number; + Category: number; + PositiveCount: number; + NegativeCount: number; + Description: string; + + static createFrom(source: any = {}) { + return new SentimentResult(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Score = source["Score"]; + this.Category = source["Category"]; + this.PositiveCount = source["PositiveCount"]; + this.NegativeCount = source["NegativeCount"]; + this.Description = source["Description"]; + } + } export class Settings { ID: number; // Go type: time diff --git a/go.mod b/go.mod index 04404d5..4af734e 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/stretchr/testify v1.10.0 github.com/tidwall/gjson v1.14.2 github.com/wailsapp/wails/v2 v2.10.1 + github.com/yanyiwu/gojieba v1.4.6 go.uber.org/zap v1.27.0 golang.org/x/sys v0.31.0 golang.org/x/text v0.23.0 diff --git a/go.sum b/go.sum index 32f0849..9e60394 100644 --- a/go.sum +++ b/go.sum @@ -135,6 +135,8 @@ github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhw github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns= github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY= +github.com/yanyiwu/gojieba v1.4.6 h1:9oKbZijSHBdoTabXK34romSWj4aQLvs+j1ctIQjSxPk= +github.com/yanyiwu/gojieba v1.4.6/go.mod h1:JUq4DddFVGdHXJHxxepxRmhrKlDpaBxR8O28v6fKYLY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=