HarmonyOS开发:情感分析与舆情监控
HarmonyOS开发:情感分析与舆情监控
核心要点:本文系统讲解HarmonyOS平台上的情感分析技术实现与舆情监控系统搭建,涵盖基于词典的情感分析、基于深度学习的细粒度情感识别、舆情趋势追踪与预警机制,以及在ArkTS中的完整工程实践。
一、背景与动机
你有没有过这样的经历——花了三个月打磨的新功能上线后,应用商店的评分突然从4.6掉到了3.8,但你完全不知道为什么?等你一条一条翻完评论,才发现是某个不起眼的交互改动惹了祸。
这就是典型的"舆情后知后觉"问题。在移动互联网时代,用户情绪的传播速度远超我们的想象。一条负面评论可能在几个小时内被转发上千次,等你反应过来,舆论的洪水已经淹没了你的品牌形象。
情感分析,就是帮我们"读懂"用户情绪的技术。它不仅能判断一条评论是正面还是负面,还能识别出具体的情绪类型——愤怒、失望、惊喜、感动——甚至能追踪情绪随时间的变化趋势。
舆情监控,则是情感分析的"升级版"。它把零散的情感数据汇聚成趋势图表,设置预警阈值,在负面情绪爆发之前就发出警报。这就像给你的APP装了一个"情绪雷达",让你能够提前感知风险、及时应对。
HarmonyOS的NLP能力包提供了端侧情感分析的基础能力,配合自定义情感词典和轻量模型,我们可以构建一套完整的端侧舆情监控系统。接下来,我们就一步步实现它。
二、核心原理
2.1 情感分析的三种技术路线
情感分析从粗到细可以分为三个层次:
- 篇章级情感分析:判断整段文本的情感极性(正面/负面/中性)
- 句子级情感分析:逐句判断情感,适合长文本的细粒度分析
- 方面级情感分析(ABSA):判断文本中每个"方面"的情感,如"手机屏幕很好,但电池太差"——屏幕正面,电池负面
在HarmonyOS端侧,我们推荐"词典法 + 轻量模型"的混合方案:
flowchart TD
A["📝 文本输入"] --> B["📖 分词与预处理"]
B --> C["📚 词典匹配"]
C --> D{"词典覆盖率≥80%?"}
D -->|"是"| E["✅ 词典法输出结果"]
D -->|"否"| F["🧠 模型推理"]
F --> G["🎯 融合决策"]
E --> G
G --> H["📊 情感分数与标签"]
H --> I["📈 舆情趋势追踪"]
I --> J{"负面情绪突增?"}
J -->|"是"| K["🚨 预警通知"]
J -->|"否"| L["✅ 正常记录"]
classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
classDef error fill:#EF5350,stroke:#C62828,color:#fff
classDef success fill:#66BB6A,stroke:#2E7D32,color:#fff
classDef info fill:#CE93D8,stroke:#7B1FA2,color:#fff
class A primary
class B primary
class C info
class D warning
class E success
class F info
class G primary
class H success
class I primary
class J warning
class K error
class L success
2.2 情感词典的构建
情感词典是词典法的核心。一个好的情感词典需要包含:
- 基础情感词:如"好"、“差”、“喜欢”、“讨厌”
- 程度副词:如"非常"、“稍微”、“极其”——它们会放大或缩小情感强度
- 否定词:如"不"、“没”、“非”——它们会翻转情感极性
- 领域特定词:不同领域的情感词不同,比如"卡顿"在APP领域是负面词
2.3 舆情趋势的数学模型
舆情监控的核心是检测"异常波动"。我们使用滑动窗口 + Z-Score的方法:
- 滑动窗口均值:计算过去N个时间点的情感均值
- Z-Score异常检测:当当前情感分数偏离均值超过2个标准差时,触发预警
- 趋势斜率:用线性回归计算情感变化趋势,斜率为负且绝对值大时预警
三、代码实战
3.1 基于词典的情感分析引擎
/**
* 基于词典的情感分析引擎
* 支持基础情感词、程度副词、否定词的复合计算
*/
// 情感极性枚举
enum SentimentPolarity {
POSITIVE = 'positive',
NEGATIVE = 'negative',
NEUTRAL = 'neutral',
}
// 情感分析结果
interface SentimentResult {
polarity: SentimentPolarity; // 情感极性
score: number; // 情感分数 -1.0 ~ 1.0
confidence: number; // 置信度 0 ~ 1.0
positiveWords: string[]; // 命中的正面词
negativeWords: string[]; // 命中的负面词
negatedWords: string[]; // 被否定词修饰的词
intensifiedWords: string[]; // 被程度副词修饰的词
}
// 情感词典条目
interface SentimentEntry {
word: string;
score: number; // -1.0 ~ 1.0,正值代表正面,负值代表负面
category: string; // 情感类别
}
export class LexiconSentimentAnalyzer {
// 基础情感词典
private sentimentDict: Map<string, SentimentEntry> = new Map();
// 程度副词词典(倍率)
private degreeDict: Map<string, number> = new Map();
// 否定词列表
private negationWords: Set<string> = new Set();
constructor() {
this.initDictionaries();
}
// 初始化词典
private initDictionaries(): void {
// ===== 正面情感词 =====
const positiveEntries: SentimentEntry[] = [
{ word: '好', score: 0.7, category: '满意' },
{ word: '棒', score: 0.8, category: '满意' },
{ word: '优秀', score: 0.9, category: '满意' },
{ word: '喜欢', score: 0.7, category: '喜爱' },
{ word: '满意', score: 0.7, category: '满意' },
{ word: '方便', score: 0.6, category: '便利' },
{ word: '快速', score: 0.6, category: '效率' },
{ word: '流畅', score: 0.7, category: '体验' },
{ word: '漂亮', score: 0.7, category: '外观' },
{ word: '实用', score: 0.6, category: '功能' },
{ word: '推荐', score: 0.7, category: '认可' },
{ word: '惊喜', score: 0.8, category: '惊喜' },
{ word: '感动', score: 0.8, category: '感动' },
{ word: '贴心', score: 0.7, category: '满意' },
{ word: '完美', score: 0.9, category: '满意' },
];
// ===== 负面情感词 =====
const negativeEntries: SentimentEntry[] = [
{ word: '差', score: -0.7, category: '不满' },
{ word: '烂', score: -0.8, category: '不满' },
{ word: '垃圾', score: -0.9, category: '愤怒' },
{ word: '讨厌', score: -0.7, category: '厌恶' },
{ word: '失望', score: -0.7, category: '失望' },
{ word: '卡顿', score: -0.7, category: '体验' },
{ word: '崩溃', score: -0.8, category: '愤怒' },
{ word: '慢', score: -0.5, category: '效率' },
{ word: '难用', score: -0.7, category: '体验' },
{ word: '闪退', score: -0.8, category: '问题' },
{ word: '坑人', score: -0.8, category: '愤怒' },
{ word: '骗人', score: -0.9, category: '愤怒' },
{ word: '恶心', score: -0.8, category: '厌恶' },
{ word: '无语', score: -0.6, category: '不满' },
{ word: '糟糕', score: -0.7, category: '不满' },
];
// 构建情感词典
[...positiveEntries, ...negativeEntries].forEach(entry => {
this.sentimentDict.set(entry.word, entry);
});
// ===== 程度副词 =====
const degreeWords: [string, number][] = [
['非常', 1.5], ['极其', 2.0], ['特别', 1.5], ['相当', 1.3],
['很', 1.3], ['太', 1.5], ['超', 1.5], ['格外', 1.5],
['稍微', 0.5], ['有点', 0.6], ['略微', 0.5], ['些许', 0.5],
];
degreeWords.forEach(([word, multiplier]) => {
this.degreeDict.set(word, multiplier);
});
// ===== 否定词 =====
const negations = ['不', '没', '没有', '非', '未', '别', '莫', '勿', '无', '不是'];
negations.forEach(word => {
this.negationWords.add(word);
});
}
// 添加自定义情感词
addSentimentWord(entry: SentimentEntry): void {
this.sentimentDict.set(entry.word, entry);
}
// 分析文本情感
analyze(text: string): SentimentResult {
const positiveWords: string[] = [];
const negativeWords: string[] = [];
const negatedWords: string[] = [];
const intensifiedWords: string[] = [];
let totalScore = 0;
let matchedCount = 0;
// 简单分词(按字符扫描,实际项目应使用NLP分词API)
const chars = [...text];
for (let i = 0; i < chars.length; i++) {
// 尝试匹配2-4字词
for (let len = 4; len >= 2; len--) {
if (i + len > chars.length) continue;
const word = chars.slice(i, i + len).join('');
const entry = this.sentimentDict.get(word);
if (entry === undefined) continue;
let wordScore = entry.score;
matchedCount++;
// 检查前面的程度副词(向前看2个词)
let degreeMultiplier = 1.0;
for (let j = Math.max(0, i - 4); j < i; j++) {
for (let dLen = 2; dLen >= 1; dLen--) {
if (j + dLen > i) continue;
const degreeWord = chars.slice(j, j + dLen).join('');
const degree = this.degreeDict.get(degreeWord);
if (degree !== undefined) {
degreeMultiplier = degree;
intensifiedWords.push(degreeWord + word);
break;
}
}
}
// 检查前面的否定词(向前看2个词)
let isNegated = false;
for (let j = Math.max(0, i - 3); j < i; j++) {
for (let nLen = 2; nLen >= 1; nLen--) {
if (j + nLen > i) continue;
const negWord = chars.slice(j, j + nLen).join('');
if (this.negationWords.has(negWord)) {
isNegated = true;
negatedWords.push(negWord + word);
break;
}
}
}
// 计算最终分数:基础分 × 程度倍率 × 否定翻转
if (isNegated) {
wordScore = -wordScore * degreeMultiplier * 0.8; // 否定后强度略降
} else {
wordScore = wordScore * degreeMultiplier;
}
totalScore += wordScore;
// 记录命中的情感词
if (entry.score > 0) {
positiveWords.push(word);
} else {
negativeWords.push(word);
}
break; // 匹配到最长词后跳出
}
}
// 归一化分数到 -1.0 ~ 1.0
const normalizedScore = matchedCount > 0
? Math.max(-1, Math.min(1, totalScore / matchedCount))
: 0;
// 判断极性
let polarity: SentimentPolarity;
if (normalizedScore > 0.1) {
polarity = SentimentPolarity.POSITIVE;
} else if (normalizedScore < -0.1) {
polarity = SentimentPolarity.NEGATIVE;
} else {
polarity = SentimentPolarity.NEUTRAL;
}
// 计算置信度
const confidence = matchedCount > 0
? Math.min(1, matchedCount * 0.2 + Math.abs(normalizedScore) * 0.5)
: 0.1;
return {
polarity,
score: Math.round(normalizedScore * 1000) / 1000,
confidence: Math.round(confidence * 100) / 100,
positiveWords,
negativeWords,
negatedWords,
intensifiedWords,
};
}
// 批量分析
analyzeBatch(texts: string[]): SentimentResult[] {
return texts.map(text => this.analyze(text));
}
}
3.2 舆情趋势追踪与预警系统
/**
* 舆情趋势追踪与预警系统
* 基于滑动窗口 + Z-Score的异常检测
*/
// 时间点数据
interface SentimentDataPoint {
timestamp: number; // 时间戳
score: number; // 情感分数
source: string; // 数据来源
text: string; // 原始文本
polarity: SentimentPolarity;
}
// 预警信息
interface AlertInfo {
id: string;
timestamp: number;
type: 'sudden_drop' | 'trend_decline' | 'negative_spike';
severity: 'low' | 'medium' | 'high' | 'critical';
message: string;
currentScore: number;
averageScore: number;
affectedTexts: string[];
}
// 舆情统计
interface SentimentStats {
total: number;
positive: number;
negative: number;
neutral: number;
averageScore: number;
positiveRate: number;
negativeRate: number;
}
export class SentimentMonitor {
private dataPoints: SentimentDataPoint[] = [];
private alerts: AlertInfo[] = [];
private analyzer: LexiconSentimentAnalyzer;
private windowSize: number = 100; // 滑动窗口大小
private alertThreshold: number = 2.0; // Z-Score预警阈值
private trendThreshold: number = -0.05; // 趋势斜率阈值
private alertCallback: ((alert: AlertInfo) => void) | null = null;
constructor() {
this.analyzer = new LexiconSentimentAnalyzer();
}
// 设置预警回调
onAlert(callback: (alert: AlertInfo) => void): void {
this.alertCallback = callback;
}
// 添加数据点
addDataPoint(text: string, source: string = 'user_review'): SentimentDataPoint {
const result = this.analyzer.analyze(text);
const dataPoint: SentimentDataPoint = {
timestamp: Date.now(),
score: result.score,
source,
text,
polarity: result.polarity,
};
this.dataPoints.push(dataPoint);
// 保持数据量在合理范围
if (this.dataPoints.length > 10000) {
this.dataPoints = this.dataPoints.slice(-5000);
}
// 检查是否需要预警
this.checkAlert(dataPoint);
return dataPoint;
}
// 批量添加数据点
addDataPoints(items: Array<{ text: string; source: string }>): SentimentDataPoint[] {
return items.map(item => this.addDataPoint(item.text, item.source));
}
// 异常检测
private checkAlert(currentPoint: SentimentDataPoint): void {
if (this.dataPoints.length < 10) return; // 数据不足,不检测
const window = this.getRecentWindow();
const scores = window.map(p => p.score);
const mean = this.calculateMean(scores);
const stdDev = this.calculateStdDev(scores, mean);
// Z-Score异常检测
if (stdDev > 0) {
const zScore = (currentPoint.score - mean) / stdDev;
if (zScore < -this.alertThreshold) {
this.triggerAlert({
id: `alert_${Date.now()}`,
timestamp: Date.now(),
type: 'sudden_drop',
severity: this.getSeverity(zScore),
message: `情感分数骤降:当前${currentPoint.score.toFixed(2)},均值${mean.toFixed(2)},Z-Score=${zScore.toFixed(2)}`,
currentScore: currentPoint.score,
averageScore: mean,
affectedTexts: [currentPoint.text],
});
}
}
// 趋势检测(最近20个点的线性回归斜率)
if (this.dataPoints.length >= 20) {
const recent20 = this.dataPoints.slice(-20);
const slope = this.calculateTrendSlope(recent20);
if (slope < this.trendThreshold) {
this.triggerAlert({
id: `trend_${Date.now()}`,
timestamp: Date.now(),
type: 'trend_decline',
severity: slope < -0.1 ? 'high' : 'medium',
message: `情感趋势持续下降:斜率=${slope.toFixed(4)}`,
currentScore: currentPoint.score,
averageScore: mean,
affectedTexts: recent20.slice(-5).map(p => p.text),
});
}
}
// 负面情绪突增检测
const recentNegative = window.filter(p => p.polarity === SentimentPolarity.NEGATIVE).length;
const negativeRate = recentNegative / window.length;
if (negativeRate > 0.6 && window.length >= 20) {
this.triggerAlert({
id: `spike_${Date.now()}`,
timestamp: Date.now(),
type: 'negative_spike',
severity: negativeRate > 0.8 ? 'critical' : 'high',
message: `负面情绪突增:负面占比${(negativeRate * 100).toFixed(1)}%`,
currentScore: currentPoint.score,
averageScore: mean,
affectedTexts: window.filter(p => p.polarity === SentimentPolarity.NEGATIVE)
.slice(-5).map(p => p.text),
});
}
}
// 触发预警
private triggerAlert(alert: AlertInfo): void {
// 避免短时间内重复预警
const recentAlerts = this.alerts.filter(
a => a.type === alert.type && Date.now() - a.timestamp < 300000 // 5分钟内
);
if (recentAlerts.length > 0) return;
this.alerts.push(alert);
this.alertCallback?.(alert);
}
// 获取最近窗口数据
private getRecentWindow(): SentimentDataPoint[] {
return this.dataPoints.slice(-this.windowSize);
}
// 计算均值
private calculateMean(values: number[]): number {
if (values.length === 0) return 0;
return values.reduce((sum, v) => sum + v, 0) / values.length;
}
// 计算标准差
private calculateStdDev(values: number[], mean: number): number {
if (values.length < 2) return 0;
const variance = values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / (values.length - 1);
return Math.sqrt(variance);
}
// 计算趋势斜率(最小二乘法线性回归)
private calculateTrendSlope(points: SentimentDataPoint[]): number {
const n = points.length;
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
for (let i = 0; i < n; i++) {
const x = i;
const y = points[i].score;
sumX += x;
sumY += y;
sumXY += x * y;
sumX2 += x * x;
}
const denominator = n * sumX2 - sumX * sumX;
if (denominator === 0) return 0;
return (n * sumXY - sumX * sumY) / denominator;
}
// 获取严重程度
private getSeverity(zScore: number): 'low' | 'medium' | 'high' | 'critical' {
const absZ = Math.abs(zScore);
if (absZ >= 4) return 'critical';
if (absZ >= 3) return 'high';
if (absZ >= 2.5) return 'medium';
return 'low';
}
// 获取统计数据
getStatistics(): SentimentStats {
const window = this.getRecentWindow();
const positive = window.filter(p => p.polarity === SentimentPolarity.POSITIVE).length;
const negative = window.filter(p => p.polarity === SentimentPolarity.NEGATIVE).length;
const neutral = window.filter(p => p.polarity === SentimentPolarity.NEUTRAL).length;
const total = window.length;
const avgScore = total > 0
? window.reduce((sum, p) => sum + p.score, 0) / total
: 0;
return {
total,
positive,
negative,
neutral,
averageScore: Math.round(avgScore * 1000) / 1000,
positiveRate: total > 0 ? Math.round(positive / total * 1000) / 1000 : 0,
negativeRate: total > 0 ? Math.round(negative / total * 1000) / 1000 : 0,
};
}
// 获取趋势数据(用于图表展示)
getTrendData(pointCount: number = 50): Array<{ index: number; score: number; timestamp: number }> {
const step = Math.max(1, Math.floor(this.dataPoints.length / pointCount));
const result: Array<{ index: number; score: number; timestamp: number }> = [];
for (let i = 0; i < this.dataPoints.length; i += step) {
const point = this.dataPoints[i];
result.push({
index: result.length,
score: point.score,
timestamp: point.timestamp,
});
}
return result;
}
// 获取预警列表
getAlerts(): AlertInfo[] {
return [...this.alerts].reverse(); // 最新的在前
}
}
3.3 舆情监控仪表盘UI组件
/**
* 舆情监控仪表盘UI组件
* 实时展示情感分析结果、趋势图表、预警信息
*/
import { LexiconSentimentAnalyzer, SentimentResult, SentimentPolarity } from './LexiconSentimentAnalyzer';
import { SentimentMonitor, SentimentStats, AlertInfo } from './SentimentMonitor';
@ObservedV2
class SentimentDashboardViewModel {
@Trace inputText: string = '';
@Trace sentimentResult: SentimentResult | null = null;
@Trace stats: SentimentStats | null = null;
@Trace alerts: AlertInfo[] = [];
@Trace isAnalyzing: boolean = false;
@Trace recentScores: number[] = [];
private analyzer: LexiconSentimentAnalyzer = new LexiconSentimentAnalyzer();
private monitor: SentimentMonitor = new SentimentMonitor();
constructor() {
// 注册预警回调
this.monitor.onAlert((alert: AlertInfo) => {
this.alerts = this.monitor.getAlerts();
});
}
// 分析文本
analyzeText(): void {
if (!this.inputText.trim()) return;
this.isAnalyzing = true;
this.sentimentResult = this.analyzer.analyze(this.inputText);
// 添加到监控系统
this.monitor.addDataPoint(this.inputText);
this.stats = this.monitor.getStatistics();
this.alerts = this.monitor.getAlerts();
// 更新趋势数据
this.recentScores = this.monitor.getTrendData(20).map(d => d.score);
this.isAnalyzing = false;
}
// 获取极性颜色
getPolarityColor(polarity: SentimentPolarity): string {
switch (polarity) {
case SentimentPolarity.POSITIVE:
return '#66BB6A';
case SentimentPolarity.NEGATIVE:
return '#EF5350';
case SentimentPolarity.NEUTRAL:
return '#FFB74D';
default:
return '#9E9E9E';
}
}
// 获取极性文本
getPolarityText(polarity: SentimentPolarity): string {
switch (polarity) {
case SentimentPolarity.POSITIVE:
return '😊 正面';
case SentimentPolarity.NEGATIVE:
return '😞 负面';
case SentimentPolarity.NEUTRAL:
return '😐 中性';
default:
return '未知';
}
}
// 获取预警严重程度颜色
getSeverityColor(severity: string): string {
switch (severity) {
case 'critical':
return '#EF5350';
case 'high':
return '#FF7043';
case 'medium':
return '#FFB74D';
case 'low':
return '#66BB6A';
default:
return '#9E9E9E';
}
}
}
@Entry
@Component
struct SentimentDashboardPage {
private viewModel: SentimentDashboardViewModel = new SentimentDashboardViewModel();
build() {
Scroll() {
Column() {
// 标题栏
this.TitleBar()
// 输入与即时分析
this.InputSection()
// 分析结果卡片
this.ResultCard()
// 舆情统计概览
this.StatsOverview()
// 情感趋势图
this.TrendChart()
// 预警列表
this.AlertList()
}
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#1A1A2E')
.scrollBar(BarState.Auto)
}
// 标题栏
@Builder
TitleBar() {
Row() {
Column() {
Text('情感分析与舆情监控')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#E0E0E0')
Text('实时追踪用户情绪变化')
.fontSize(13)
.fontColor('#9E9E9E')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
Blank()
// 预警指示器
if (this.viewModel.alerts.length > 0) {
Badge({ count: this.viewModel.alerts.length, maxCount: 99 }) {
Text('🔔')
.fontSize(24)
}
}
}
.width('100%')
.padding({ left: 20, right: 20, top: 16, bottom: 16 })
.alignItems(VerticalAlign.Center)
}
// 输入区域
@Builder
InputSection() {
Column() {
TextArea({ placeholder: '输入文本进行情感分析...' })
.width('100%')
.height(100)
.fontSize(16)
.fontColor('#E0E0E0')
.backgroundColor('#16213E')
.borderRadius(12)
.padding(12)
.onChange((value: string) => {
this.viewModel.inputText = value;
})
Row() {
Button('分析情感')
.height(44)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.backgroundColor('#4FC3F7')
.fontColor('#1A1A2E')
.borderRadius(10)
.layoutWeight(1)
.onClick(() => {
this.viewModel.analyzeText();
})
Button('模拟批量')
.height(44)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.backgroundColor('#CE93D8')
.fontColor('#1A1A2E')
.borderRadius(10)
.layoutWeight(1)
.margin({ left: 12 })
.onClick(() => {
// 模拟批量评论数据
const sampleTexts = [
'这个APP非常好用,界面流畅,功能也很实用',
'太差了,一直闪退,根本用不了',
'还行吧,一般般,没什么特别的',
'非常满意,推荐给大家使用',
'垃圾软件,骗人的,千万别下载',
'更新后好多了,之前的问题都修复了',
'客服态度很差,问题一直不解决',
'界面很漂亮,操作也很方便',
];
sampleTexts.forEach(text => {
this.viewModel.inputText = text;
this.viewModel.analyzeText();
});
})
}
.width('100%')
.margin({ top: 12 })
}
.width('100%')
.padding({ left: 20, right: 20, top: 8, bottom: 16 })
}
// 分析结果卡片
@Builder
ResultCard() {
if (this.viewModel.sentimentResult !== null) {
Column() {
Text('分析结果')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#E0E0E0')
.margin({ bottom: 12 })
Row() {
// 情感极性
Column() {
Text(this.viewModel.getPolarityText(this.viewModel.sentimentResult!.polarity))
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor(this.viewModel.getPolarityColor(this.viewModel.sentimentResult!.polarity))
}
Blank()
// 情感分数仪表
Column() {
Text('情感分数')
.fontSize(12)
.fontColor('#9E9E9E')
Text(this.viewModel.sentimentResult!.score.toFixed(2))
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor(this.viewModel.getPolarityColor(this.viewModel.sentimentResult!.polarity))
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
// 情感词命中
Row() {
if (this.viewModel.sentimentResult!.positiveWords.length > 0) {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.viewModel.sentimentResult!.positiveWords, (word: string) => {
Text(word)
.fontSize(12)
.fontColor('#66BB6A')
.backgroundColor('#1B3A1B')
.borderRadius(6)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.margin({ right: 6, top: 6 })
})
}
}
if (this.viewModel.sentimentResult!.negativeWords.length > 0) {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.viewModel.sentimentResult!.negativeWords, (word: string) => {
Text(word)
.fontSize(12)
.fontColor('#EF5350')
.backgroundColor('#3A1B1B')
.borderRadius(6)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.margin({ right: 6, top: 6 })
})
}
}
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(16)
.backgroundColor('#0F3460')
.borderRadius(16)
.margin({ left: 20, right: 20, top: 8 })
}
}
// 统计概览
@Builder
StatsOverview() {
if (this.viewModel.stats !== null) {
Column() {
Text('舆情概览')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#E0E0E0')
.margin({ bottom: 12 })
Row() {
// 正面
this.StatItem('😊 正面', this.viewModel.stats!.positive, '#66BB6A')
// 中性
this.StatItem('😐 中性', this.viewModel.stats!.neutral, '#FFB74D')
// 负面
this.StatItem('😞 负面', this.viewModel.stats!.negative, '#EF5350')
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
// 平均分数
Row() {
Text('平均情感分数')
.fontSize(14)
.fontColor('#9E9E9E')
Blank()
Text(this.viewModel.stats!.averageScore.toFixed(3))
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#4FC3F7')
}
.width('100%')
.margin({ top: 12 })
// 负面率进度条
Row() {
Text('负面率')
.fontSize(14)
.fontColor('#9E9E9E')
Blank()
Text(`${(this.viewModel.stats!.negativeRate * 100).toFixed(1)}%`)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#EF5350')
}
.width('100%')
.margin({ top: 8 })
Progress({ value: this.viewModel.stats!.negativeRate * 100, total: 100 })
.width('100%')
.color('#EF5350')
.backgroundColor('#2D1B1B')
.style(ProgressStyle.Linear)
.margin({ top: 4 })
}
.width('100%')
.padding(16)
.backgroundColor('#0F3460')
.borderRadius(16)
.margin({ left: 20, right: 20, top: 12 })
}
}
// 统计子项
@Builder
StatItem(label: string, count: number, color: string) {
Column() {
Text(label)
.fontSize(13)
.fontColor('#9E9E9E')
Text(`${count}`)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor(color)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Center)
}
// 情感趋势图(简化版柱状图)
@Builder
TrendChart() {
if (this.viewModel.recentScores.length > 0) {
Column() {
Text('情感趋势')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#E0E0E0')
.margin({ bottom: 12 })
// 简化柱状图
Row() {
ForEach(this.viewModel.recentScores, (score: number, index: number) => {
Column() {
// 柱子
Column()
.width(8)
.height(Math.abs(score) * 60 + 4)
.backgroundColor(score >= 0 ? '#66BB6A' : '#EF5350')
.borderRadius(4)
}
.height(70)
.justifyContent(FlexAlign.End)
.margin({ right: 2 })
})
}
.width('100%')
.height(80)
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.End)
// 图例
Row() {
Row() {
Circle().width(8).height(8).fill('#66BB6A')
Text('正面')
.fontSize(11)
.fontColor('#9E9E9E')
.margin({ left: 4 })
}
Row() {
Circle().width(8).height(8).fill('#EF5350')
Text('负面')
.fontSize(11)
.fontColor('#9E9E9E')
.margin({ left: 4 })
}
.margin({ left: 16 })
}
.margin({ top: 8 })
}
.width('100%')
.padding(16)
.backgroundColor('#0F3460')
.borderRadius(16)
.margin({ left: 20, right: 20, top: 12 })
}
}
// 预警列表
@Builder
AlertList() {
if (this.viewModel.alerts.length > 0) {
Column() {
Text('预警信息')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#E0E0E0')
.margin({ bottom: 12 })
List() {
ForEach(this.viewModel.alerts.slice(0, 10), (alert: AlertInfo) => {
ListItem() {
Row() {
// 严重程度指示器
Circle()
.width(12)
.height(12)
.fill(this.viewModel.getSeverityColor(alert.severity))
Column() {
Text(alert.message)
.fontSize(13)
.fontColor('#E0E0E0')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(new Date(alert.timestamp).toLocaleString())
.fontSize(11)
.fontColor('#666')
.margin({ top: 4 })
}
.layoutWeight(1)
.margin({ left: 10 })
// 严重程度标签
Text(alert.severity.toUpperCase())
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(this.viewModel.getSeverityColor(alert.severity))
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor('#1A1A2E')
.borderRadius(4)
}
.width('100%')
.padding(12)
.backgroundColor('#16213E')
.borderRadius(10)
}
.margin({ bottom: 8 })
})
}
.width('100%')
}
.width('100%')
.padding(16)
.backgroundColor('#0F3460')
.borderRadius(16)
.margin({ left: 20, right: 20, top: 12, bottom: 20 })
}
}
}
四、踩坑与注意事项
4.1 否定词的处理陷阱
问题:“不错"应该是正面,但简单匹配到"不”+"好"的变体可能误判为负面。中文否定表达非常复杂,“不”+"正面词"不一定就是负面。
解决方案:
- 建立"否定+正面词"的组合词典,如"不错"→正面0.5,“不赖”→正面0.4
- 考虑双重否定的情况,如"不是不好"→正面
- 引入上下文窗口,检查否定词和情感词之间是否有其他修饰词
4.2 反讽与讽刺的识别
问题:“这APP真是太好用了,一天崩三次”——表面全是正面词,实际是讽刺。
解决方案:
- 检测"情感不一致":正面词和负面词同时出现且比例异常
- 结合标点符号分析:感叹号过多、波浪号等可能是反讽信号
- 使用模型进行二次判断,词典法难以处理反讽
4.3 领域迁移问题
问题:通用情感词典在特定领域的表现很差。比如"便宜"在电商领域是正面词,但在质量评价中可能是负面词。
解决方案:
- 为不同领域构建专属情感词典
- 支持词典的动态加载和切换
- 允许用户自定义情感词和分数
4.4 舆情预警的误报率
问题:Z-Score方法对异常敏感,但容易产生误报。特别是数据量少的时候,一两条负面评论就可能触发预警。
解决方案:
- 设置最小数据量阈值(建议至少20条数据才启动异常检测)
- 引入冷却期,同一类型预警5分钟内不重复触发
- 结合多种检测方法(Z-Score + 趋势斜率 + 负面率),至少两种同时触发才预警
- 允许用户标记误报,用于优化阈值
五、HarmonyOS 6适配
5.1 API变更
| 能力 | HarmonyOS 5.0 | HarmonyOS 6.0 |
|---|---|---|
| 情感分析 | 基础三分类API | 支持细粒度情感(8种情绪类别) |
| 方面级情感 | 不支持 | 新增ABSA能力 |
| 实时流式分析 | 不支持 | 支持逐字流式情感分析 |
| 多模态融合 | 仅文本 | 文本+语音+表情多模态融合 |
5.2 迁移指南
// HarmonyOS 5.0 写法
import { sentimentAnalysis } from '@kit.AiKit';
const result = await sentimentAnalysis.analyze({
text: inputText,
});
// result.polarity: 'positive' | 'negative' | 'neutral'
// HarmonyOS 6.0 写法(细粒度情感)
import { nlp } from '@kit.AiKit';
const analyzer = nlp.createSentimentAnalyzer({
granularity: 'fine', // 'coarse' 粗粒度 | 'fine' 细粒度
enableABSA: true, // 启用方面级情感分析
});
const result = await analyzer.analyze(inputText);
// result.emotions: [{ type: 'joy', score: 0.8 }, { type: 'surprise', score: 0.3 }]
// result.aspects: [{ aspect: '性能', polarity: 'negative', score: -0.6 }]
analyzer.destroy();
5.3 新特性适配建议
- 细粒度情感:利用8种情绪类别(喜悦、愤怒、悲伤、恐惧、惊讶、厌恶、信任、期待)构建更精细的用户画像
- ABSA方面级分析:对产品评论进行方面级拆解,精准定位问题模块
- 流式分析:在用户输入过程中实时反馈情感变化,提升交互体验
六、总结
| 知识点 | 核心内容 |
|---|---|
| 词典法情感分析 | 基础情感词+程度副词+否定词的复合计算,适合快速部署 |
| 舆情趋势追踪 | 滑动窗口+Z-Score异常检测+线性回归趋势分析 |
| 预警机制 | 骤降检测、趋势下降检测、负面突增检测三重保障 |
| 否定词处理 | 组合词典+上下文窗口+双重否定,避免误判 |
| 反讽识别 | 情感不一致检测+标点分析+模型二次判断 |
| 领域适配 | 专属词典+动态加载+用户自定义 |
| HarmonyOS 6适配 | 细粒度情感、ABSA、流式分析、多模态融合 |
核心思想:情感分析不是简单的"数词数",而是一个需要考虑语境、领域、修辞的复杂工程。词典法是地基,模型法是上层建筑,舆情监控是瞭望塔——三者结合,才能构建真正有价值的情感智能系统。
实践建议:
- 先用词典法快速上线,再逐步引入模型提升准确率
- 预警阈值宁可偏高也不要偏低,误报比漏报更让人焦虑
- 定期回顾预警记录,人工标注误报,持续优化系统
- 不同业务场景的情感基线不同,不要用同一套阈值套所有场景
- 点赞
- 收藏
- 关注作者
评论(0)