HarmonyOS开发:情感分析与舆情监控

举报
Jack20 发表于 2026/06/21 13:59:32 2026/06/21
【摘要】 HarmonyOS开发:情感分析与舆情监控核心要点:本文系统讲解HarmonyOS平台上的情感分析技术实现与舆情监控系统搭建,涵盖基于词典的情感分析、基于深度学习的细粒度情感识别、舆情趋势追踪与预警机制,以及在ArkTS中的完整工程实践。 一、背景与动机你有没有过这样的经历——花了三个月打磨的新功能上线后,应用商店的评分突然从4.6掉到了3.8,但你完全不知道为什么?等你一条一条翻完评论,...

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、流式分析、多模态融合

核心思想:情感分析不是简单的"数词数",而是一个需要考虑语境、领域、修辞的复杂工程。词典法是地基,模型法是上层建筑,舆情监控是瞭望塔——三者结合,才能构建真正有价值的情感智能系统。

实践建议

  1. 先用词典法快速上线,再逐步引入模型提升准确率
  2. 预警阈值宁可偏高也不要偏低,误报比漏报更让人焦虑
  3. 定期回顾预警记录,人工标注误报,持续优化系统
  4. 不同业务场景的情感基线不同,不要用同一套阈值套所有场景
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。