Spaces:
Running
Running
| import streamlit as st | |
| import pandas as pd | |
| import plotly.express as px | |
| import os | |
| import re | |
| import io | |
| import base64 | |
| # ============================================ | |
| # 0. 页面配置与CSS | |
| # ============================================ | |
| st.set_page_config( | |
| page_title="高校中华民族共同体分析平台", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| st.markdown(""" | |
| <style> | |
| /* 全局基础 */ | |
| html, body, [class*="css"] { | |
| font-family: 'Microsoft YaHei', 'SimHei', sans-serif; | |
| color: #333; | |
| } | |
| .stApp { | |
| background-color: #FFFCF8; | |
| background-image: | |
| radial-gradient(circle at 20% 50%, rgba(222, 41, 16, 0.03) 0%, transparent 50%), | |
| radial-gradient(circle at 80% 80%, rgba(255, 215, 0, 0.03) 0%, transparent 50%); | |
| } | |
| /* ========== 顶部标题区域 ========== */ | |
| .header-container { | |
| background: linear-gradient(135deg, #DE2910 0%, #C41E0B 100%); | |
| padding: 20px 0; | |
| margin: -1rem -1rem 0 -1rem; | |
| box-shadow: 0 4px 20px rgba(222, 41, 16, 0.3); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .header-container::before { | |
| content: ""; | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| height: 3px; | |
| background: linear-gradient(90deg, transparent, #FFD700, #FFA500, #FFD700, transparent); | |
| } | |
| .title-wrapper { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 20px; | |
| position: relative; | |
| z-index: 2; | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 0 20px; | |
| } | |
| .badge-left, .badge-right { | |
| flex-shrink: 0; | |
| width: 80px; | |
| height: 80px; | |
| border-radius: 50%; | |
| border: 3px solid #FFD700; | |
| box-shadow: | |
| 0 4px 15px rgba(0,0,0,0.3), | |
| 0 0 0 4px rgba(222, 41, 16, 0.2), | |
| inset 0 0 20px rgba(255, 215, 0, 0.1); | |
| overflow: hidden; | |
| background: linear-gradient(135deg, #FFFFFF 0%, #FFF9F9 100%); | |
| transition: transform 0.3s ease; | |
| } | |
| .badge-left:hover, .badge-right:hover { | |
| transform: scale(1.05) rotate(5deg); | |
| } | |
| .badge-left img, .badge-right img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .title-center { | |
| flex: 1; | |
| text-align: center; | |
| } | |
| .gold-line { | |
| height: 2px; | |
| width: 80px; | |
| background: linear-gradient(90deg, transparent, #FFD700, #FFA500, #FFD700, transparent); | |
| flex-shrink: 0; | |
| box-shadow: 0 0 10px rgba(255, 215, 0, 0.5); | |
| } | |
| .main-title { | |
| text-align: center; | |
| color: #FFFFFF; | |
| font-size: 32px; | |
| font-weight: bold; | |
| margin: 0; | |
| white-space: nowrap; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.3); | |
| letter-spacing: 2px; | |
| } | |
| .sub-title { | |
| text-align: center; | |
| color: #FFD700; | |
| font-size: 16px; | |
| margin-top: 8px; | |
| font-weight: 500; | |
| letter-spacing: 1px; | |
| text-shadow: 1px 1px 2px rgba(0,0,0,0.2); | |
| } | |
| /* ========== 导航栏 ========== */ | |
| .nav-container { | |
| background: linear-gradient(to bottom, #FFFFFF 0%, #FFF9F9 100%); | |
| border-bottom: 2px solid #FFD700; | |
| padding: 10px 20px; | |
| margin: 0 -1rem 15px -1rem; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
| } | |
| /* ========== 内容卡片 ========== */ | |
| .content-card { | |
| background: #FFFFFF; | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin: 0 0 20px 0; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.08); | |
| border-top: 4px solid #DE2910; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .content-card::before { | |
| content: ""; | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| width: 100px; | |
| height: 100px; | |
| background: radial-gradient(circle, rgba(255, 215, 0, 0.1) 0%, transparent 70%); | |
| pointer-events: none; | |
| } | |
| /* ========== 图表区域 ========== */ | |
| .chart-wrapper { | |
| width: 100%; | |
| height: 480px; | |
| margin: 10px 0; | |
| } | |
| /* ========== 地图区域 ========== */ | |
| .map-wrapper { | |
| width: 100%; | |
| height: 520px; | |
| margin: 10px 0; | |
| } | |
| .map-container { | |
| width: 100%; | |
| height: 100%; | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: linear-gradient(135deg, #FFF9F9 0%, #FFFFFF 100%); | |
| border: 3px solid #FFD700; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| } | |
| .map-container img { | |
| max-width: 100%; | |
| max-height: 100%; | |
| object-fit: contain; | |
| border-radius: 8px; | |
| } | |
| .map-placeholder { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-direction: column; | |
| color: #DE2910; | |
| font-weight: bold; | |
| } | |
| /* ========== 按钮优化 ========== */ | |
| div.stButton > button { | |
| background: linear-gradient(135deg, #DE2910 0%, #C41E0B 100%); | |
| color: white; | |
| border-radius: 6px; | |
| border: none; | |
| font-weight: bold; | |
| padding: 8px 20px; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 15px rgba(222, 41, 16, 0.3); | |
| } | |
| div.stButton > button:hover { | |
| background: linear-gradient(135deg, #C41E0B 0%, #A01810 100%); | |
| color: #FFD700; | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(222, 41, 16, 0.4); | |
| } | |
| /* 导航按钮选中状态 */ | |
| div[data-testid="column"] > div > div > div > div > div > button[kind="primary"] { | |
| background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%) !important; | |
| color: #DE2910 !important; | |
| border: 2px solid #DE2910 !important; | |
| font-weight: bold !important; | |
| } | |
| /* ========== 侧边栏 ========== */ | |
| [data-testid="stSidebar"] { | |
| background: linear-gradient(180deg, #FFF9F9 0%, #FFFFFF 100%); | |
| border-right: 3px solid #DE2910; | |
| min-width: 320px !important; | |
| max-width: 350px !important; | |
| box-shadow: 2px 0 15px rgba(0,0,0,0.1); | |
| } | |
| .sidebar-card { | |
| background: linear-gradient(135deg, #FFFFFF 0%, #FFFCF8 100%); | |
| padding: 15px; | |
| border-radius: 10px; | |
| border: 1px solid rgba(222, 41, 16, 0.1); | |
| margin-bottom: 15px; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.05); | |
| } | |
| .sidebar-title { | |
| color: #DE2910; | |
| font-size: 15px; | |
| font-weight: bold; | |
| margin-bottom: 10px; | |
| border-left: 4px solid #FFD700; | |
| padding-left: 10px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| /* ========== 底部信息 ========== */ | |
| .footer { | |
| text-align: center; | |
| color: #666; | |
| font-size: 13px; | |
| margin-top: 30px; | |
| padding: 20px; | |
| border-top: 2px solid rgba(222, 41, 16, 0.1); | |
| } | |
| /* ========== 标题样式 ========== */ | |
| h3 { | |
| color: #DE2910; | |
| font-size: 20px; | |
| margin-bottom: 15px; | |
| padding-bottom: 8px; | |
| border-bottom: 3px solid #FFD700; | |
| display: inline-block; | |
| position: relative; | |
| } | |
| h3::after { | |
| content: ""; | |
| position: absolute; | |
| bottom: -3px; | |
| left: 0; | |
| width: 50%; | |
| height: 3px; | |
| background: #DE2910; | |
| border-radius: 0 2px 2px 0; | |
| } | |
| /* ========== 信息提示框 ========== */ | |
| .stInfo { | |
| background: linear-gradient(135deg, #FFF9F9 0%, #FFFFFF 100%) !important; | |
| border-left: 4px solid #DE2910 !important; | |
| border-radius: 8px !important; | |
| } | |
| .stSuccess { | |
| background: linear-gradient(135deg, #F0FFF0 0%, #FFFFFF 100%) !important; | |
| border-left: 4px solid #28a745 !important; | |
| border-radius: 8px !important; | |
| } | |
| /* ========== 隐藏默认元素 ========== */ | |
| #MainMenu {visibility: hidden;} | |
| footer {visibility: hidden;} | |
| header {visibility: hidden;} | |
| /* ========== 数据表格 ========== */ | |
| .dataframe { | |
| border-radius: 8px; | |
| overflow: hidden; | |
| border: 1px solid rgba(222, 41, 16, 0.1); | |
| } | |
| .dataframe th { | |
| background: linear-gradient(135deg, #DE2910 0%, #C41E0B 100%) !important; | |
| color: white !important; | |
| font-weight: bold; | |
| padding: 12px !important; | |
| } | |
| .dataframe td { | |
| padding: 10px !important; | |
| border-bottom: 1px solid rgba(0,0,0,0.05); | |
| } | |
| /* ========== 欢迎页面 ========== */ | |
| .welcome-container { | |
| text-align: center; | |
| padding: 40px 20px; | |
| background: linear-gradient(135deg, #FFFFFF 0%, #FFF9F9 100%); | |
| border-radius: 15px; | |
| margin: 0 0 20px 0; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.08); | |
| border-top: 4px solid #DE2910; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ============================================ | |
| # 1. 顶部标题栏 | |
| # ============================================ | |
| def render_header(): | |
| badge_left_html = "" | |
| badge_right_html = "" | |
| if os.path.exists("xiaohui.jpg.jpg"): | |
| with open("xiaohui.jpg.jpg", "rb") as f: | |
| badge_left_b64 = base64.b64encode(f.read()).decode() | |
| badge_left_html = f'<div class="badge-left"><img src="data:image/jpeg;base64,{badge_left_b64}" alt="校徽"></div>' | |
| else: | |
| badge_left_html = '<div class="badge-left" style="display: flex; align-items: center; justify-content: center; color: #DE2910; font-size: 32px; font-weight: bold;">校</div>' | |
| if os.path.exists("duihui.jpg"): | |
| with open("duihui.jpg", "rb") as f: | |
| badge_right_b64 = base64.b64encode(f.read()).decode() | |
| badge_right_html = f'<div class="badge-right"><img src="data:image/jpeg;base64,{badge_right_b64}" alt="队徽"></div>' | |
| else: | |
| badge_right_html = '<div class="badge-right" style="display: flex; align-items: center; justify-content: center; color: #DE2910; font-size: 32px; font-weight: bold;">队</div>' | |
| st.markdown(f""" | |
| <div class="header-container"> | |
| <div class="title-wrapper"> | |
| {badge_left_html} | |
| <div class="gold-line"></div> | |
| <div class="title-center"> | |
| <h1 class="main-title">高校中华民族共同体意识传播分析平台</h1> | |
| <p class="sub-title">基于大数据与大模型的情感分析及传播效果优化系统</p> | |
| </div> | |
| <div class="gold-line"></div> | |
| {badge_right_html} | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| render_header() | |
| # ============================================ | |
| # 2. 顶部导航栏 | |
| # ============================================ | |
| if 'current_tab' not in st.session_state: | |
| st.session_state.current_tab = "CPI排行" | |
| tabs = ["CPI排行", "话语提取", "情感对比", "热度地图", "优化策略", "学术语境分析"] | |
| tab_icons = ["📊", "🗣️", "🧠", "🗺️", "💡", "📚"] | |
| st.markdown('<div class="nav-container">', unsafe_allow_html=True) | |
| nav_cols = st.columns(len(tabs)) # 动态适应标签数量 | |
| for i, (tab, icon) in enumerate(zip(tabs, tab_icons)): | |
| with nav_cols[i]: | |
| is_active = st.session_state.current_tab == tab | |
| btn_type = "primary" if is_active else "secondary" | |
| if st.button(f"{icon} {tab}", key=f"nav_{tab}", use_container_width=True, type=btn_type): | |
| st.session_state.current_tab = tab | |
| st.rerun() | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # ============================================ | |
| # 3. 登录与状态管理 | |
| # ============================================ | |
| if 'auth' not in st.session_state: | |
| st.session_state.auth = False | |
| if 'ask' not in st.session_state: | |
| st.session_state.ask = False | |
| login_col = st.columns([12, 1])[1] | |
| with login_col: | |
| if st.session_state.auth: | |
| if st.button("🚪 退出", key="logout_btn"): | |
| st.session_state.auth = False | |
| st.rerun() | |
| else: | |
| if st.button("🔐 管理", key="login_btn"): | |
| st.session_state.ask = True | |
| if st.session_state.get('ask') and not st.session_state.auth: | |
| with st.form("login_form"): | |
| pwd = st.text_input("请输入管理口令", type="password") | |
| if st.form_submit_button("进入系统"): | |
| if pwd == "0166": | |
| st.session_state.auth = True | |
| st.session_state.ask = False | |
| st.rerun() | |
| # ============================================ | |
| # 4. 辅助函数 | |
| # ============================================ | |
| def read_data_file_cached(file_bytes, file_name): | |
| try: | |
| fname = file_name.lower() | |
| if fname.endswith('.csv'): | |
| for encoding in ['utf-8-sig', 'utf-8', 'gbk', 'gb2312']: | |
| try: | |
| return pd.read_csv(io.BytesIO(file_bytes), encoding=encoding) | |
| except: | |
| continue | |
| return pd.read_csv(io.BytesIO(file_bytes), encoding='utf-8', errors='ignore') | |
| elif fname.endswith(('.xlsx', '.xls')): | |
| return pd.read_excel(io.BytesIO(file_bytes), engine='openpyxl') | |
| elif fname.endswith('.docx'): | |
| try: | |
| from docx import Document | |
| doc = Document(io.BytesIO(file_bytes)) | |
| text_data = [] | |
| for para in doc.paragraphs: | |
| if para.text.strip(): | |
| text_data.append({'内容': para.text.strip()}) | |
| for table in doc.tables: | |
| for row in table.rows: | |
| row_data = [cell.text for cell in row.cells] | |
| if len(row_data) >= 2: | |
| text_data.append({'标题': row_data[0], '内容': row_data[1]}) | |
| return pd.DataFrame(text_data) | |
| except ImportError: | |
| return None | |
| return None | |
| except Exception as e: | |
| return None | |
| def read_data_file(file): | |
| file_bytes = file.read() | |
| file.seek(0) | |
| return read_data_file_cached(file_bytes, file.name) | |
| # ---------- 新增:学术分析辅助函数 ---------- | |
| def analyze_text_sentiment(texts, keyword): | |
| import random | |
| if not texts: | |
| return {'sentiment': '中性', 'score': 0.5, 'example': ''} | |
| sample = str(texts[0]) if texts else "" | |
| positive_keywords = ['铸牢', '认同', '团结', '融合', '共同体', '凝聚', '进步', '发展'] | |
| negative_keywords = ['破坏', '分裂', '歧视', '冲突', '对立', '矛盾'] | |
| score = 0.5 | |
| sentiment = '中性' | |
| text_lower = sample.lower() | |
| pos_count = sum(1 for p in positive_keywords if p in text_lower) | |
| neg_count = sum(1 for n in negative_keywords if n in text_lower) | |
| if pos_count > neg_count: | |
| score = 0.5 + min(0.5, pos_count * 0.1) | |
| sentiment = '积极' | |
| elif neg_count > pos_count: | |
| score = max(0.0, 0.5 - neg_count * 0.1) | |
| sentiment = '消极' | |
| else: | |
| score = 0.5 + random.uniform(-0.1, 0.1) | |
| sentiment = '中性' | |
| return {'sentiment': sentiment, 'score': round(score, 2), 'example': sample[:100]} | |
| def generate_academic_insights(analysis_results, target_words): | |
| insights = [] | |
| regions = set([r['地区'] for r in analysis_results if '地区' in r]) | |
| if len(regions) > 1: | |
| insights.append({'title': '🌍 地域表述差异', 'content': f'发现{len(regions)}个不同地区的表述差异,建议针对性地调整传播策略。'}) | |
| sentiments = [r.get('语义倾向', '中性') for r in analysis_results] | |
| pos_ratio = sentiments.count('积极') / len(sentiments) if sentiments else 0 | |
| if pos_ratio > 0.7: | |
| insights.append({'title': '📈 积极语义主导', 'content': f'{pos_ratio*100:.0f}%的表述呈现积极语义倾向,整体舆论环境良好。'}) | |
| elif pos_ratio < 0.3: | |
| insights.append({'title': '⚠️ 消极语义警示', 'content': f'消极语义占比{(1-pos_ratio)*100:.0f}%,需关注潜在风险点。'}) | |
| word_coverage = len(set([r['关键词'] for r in analysis_results])) | |
| insights.append({'title': '🎯 关键词覆盖', 'content': f'成功识别{word_coverage}个核心词汇的语境使用模式。'}) | |
| return insights | |
| # ---------- 结束 ---------- | |
| # ============================================ | |
| # 5. 侧边栏 | |
| # ============================================ | |
| if st.session_state.auth: | |
| with st.sidebar: | |
| st.markdown('<p style="color:#DE2910; font-size:18px; font-weight:bold; margin-bottom:15px; text-align: center; border-bottom: 2px solid #FFD700; padding-bottom: 10px;">📥 数据管理中心</p>', unsafe_allow_html=True) | |
| with st.container(): | |
| st.markdown('<div class="sidebar-card"><div class="sidebar-title">🏫 高校官方数据</div>', unsafe_allow_html=True) | |
| f_u = st.file_uploader("上传高校数据", accept_multiple_files=True, | |
| type=['csv', 'xlsx', 'xls'], label_visibility="collapsed", key="uploader_u") | |
| if f_u: | |
| st.success(f"✅ 已选 {len(f_u)} 个文件") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="sidebar-card"><div class="sidebar-title">💬 社媒反馈语料</div>', unsafe_allow_html=True) | |
| f_s = st.file_uploader("上传社媒数据", accept_multiple_files=True, | |
| type=['csv', 'xlsx', 'xls'], label_visibility="collapsed", key="uploader_s") | |
| if f_s: | |
| st.success(f"✅ 已选 {len(f_s)} 个文件") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="sidebar-card"><div class="sidebar-title">💡 优化策略数据(可选)</div>', unsafe_allow_html=True) | |
| f_p = st.file_uploader("上传策略数据", accept_multiple_files=True, | |
| type=['csv', 'xlsx', 'xls', 'docx'], label_visibility="collapsed", key="uploader_p") | |
| if f_p: | |
| st.success(f"✅ 已选 {len(f_p)} 个文件") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="sidebar-card"><div class="sidebar-title">🗺️ 热度地图(可选)</div>', unsafe_allow_html=True) | |
| f_map = st.file_uploader("上传地图图片", accept_multiple_files=False, | |
| type=['jpg', 'jpeg', 'png'], label_visibility="collapsed", key="uploader_map") | |
| if f_map: | |
| map_bytes = f_map.read() | |
| with open("ditu.jpg", "wb") as f: | |
| f.write(map_bytes) | |
| st.success(f"✅ 地图已上传: {f_map.name}") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="sidebar-card"><div class="sidebar-title">📚 学术语境分析数据</div>', unsafe_allow_html=True) | |
| f_academic = st.file_uploader("上传学术分析数据", accept_multiple_files=True, | |
| type=['csv', 'xlsx', 'xls'], label_visibility="collapsed", key="uploader_academic") | |
| if f_academic: | |
| st.success(f"✅ 已选 {len(f_academic)} 个文件") | |
| st.markdown('</div>', unsafe_allow_html=True) # 关闭 "📚 学术语境分析数据" 卡片 | |
| if st.button("🚀 固化并发布", use_container_width=True, key="publish_btn"): | |
| if f_u and f_s: | |
| with st.spinner("🔄 数据处理中..."): | |
| all_u = [] | |
| for f in f_u: | |
| df = read_data_file(f) | |
| if df is not None: | |
| df['school'] = f.name.split('.')[0] | |
| all_u.append(df) | |
| if all_u: | |
| pd.concat(all_u, ignore_index=True).to_csv("data_u.csv", index=False, encoding='utf-8-sig') | |
| all_s = [] | |
| for f in f_s: | |
| df = read_data_file(f) | |
| if df is not None: | |
| all_s.append(df) | |
| if all_s: | |
| pd.concat(all_s, ignore_index=True).to_csv("data_s.csv", index=False, encoding='utf-8-sig') | |
| if f_p: | |
| all_p = [] | |
| for f in f_p: | |
| df = read_data_file(f) | |
| if df is not None: | |
| all_p.append(df) | |
| if all_p: | |
| pd.concat(all_p, ignore_index=True).to_csv("data_p.csv", index=False, encoding='utf-8-sig') | |
| if f_academic: | |
| all_academic = [] | |
| for f in f_academic: | |
| df = read_data_file(f) | |
| if df is not None: | |
| all_academic.append(df) | |
| if all_academic: | |
| pd.concat(all_academic, ignore_index=True).to_csv("data_academic.csv", index=False, encoding='utf-8-sig') | |
| st.success("✅ 发布成功!") | |
| st.rerun() | |
| else: | |
| st.error("❌ 请上传高校和社媒数据") | |
| # ============================================ | |
| # 6. 主展示区 | |
| # ============================================ | |
| color_map = { | |
| '积极': '#DE2910', | |
| '正面': '#DE2910', | |
| '中性': '#FFB3B3', | |
| '一般': '#FFB3B3', | |
| '消极': '#999999', | |
| '负面': '#999999', | |
| '好评': '#DE2910', | |
| '差评': '#999999' | |
| } | |
| def generate_default_strategies(): | |
| strategies = [ | |
| {"策略类型": "内容优化", "具体建议": '增加"五个认同"相关内容的发布频率,每周至少3次专题推送', "优先级": "高"}, | |
| {"策略类型": "内容优化", "具体建议": '使用"石榴籽"等生动比喻,增强内容感染力', "优先级": "高"}, | |
| {"策略类型": "传播渠道", "具体建议": "加强短视频平台运营,制作15-30秒精华内容", "优先级": "中"}, | |
| {"策略类型": "互动提升", "具体建议": "设置话题讨论区,鼓励师生分享民族团结故事", "优先级": "中"}, | |
| {"策略类型": "情感引导", "具体建议": "针对负面反馈,及时发布正面案例进行引导", "优先级": "高"}, | |
| {"策略类型": "形式创新", "具体建议": "开展线上线下结合的主题活动,增强参与感", "优先级": "中"}, | |
| {"策略类型": "数据监测", "具体建议": "建立每周舆情监测机制,及时掌握传播效果", "优先级": "低"}, | |
| {"策略类型": "队伍建设", "具体建议": "培养校园网络评论员队伍,提升正面声音", "优先级": "中"}, | |
| ] | |
| return pd.DataFrame(strategies) | |
| data_exists = os.path.exists("data_u.csv") and os.path.exists("data_s.csv") | |
| if data_exists: | |
| try: | |
| def load_data(): | |
| du = pd.read_csv("data_u.csv", encoding='utf-8-sig') | |
| ds = pd.read_csv("data_s.csv", encoding='utf-8-sig') | |
| return du, ds | |
| du, ds = load_data() | |
| current = st.session_state.current_tab | |
| with st.container(): | |
| st.markdown('<div class="content-card">', unsafe_allow_html=True) | |
| if current == "CPI排行": | |
| st.markdown("### 📊 高校传播效能排行") | |
| if 'school' in du.columns: | |
| c_data = du['school'].value_counts().reset_index() | |
| c_data.columns = ['学校', '指数'] | |
| fig = px.bar(c_data, x='学校', y='指数', | |
| color_discrete_sequence=['#DE2910'], | |
| text_auto=True) | |
| fig.update_layout( | |
| height=480, | |
| showlegend=False, | |
| xaxis_title="", | |
| yaxis_title="传播指数", | |
| plot_bgcolor='white', | |
| paper_bgcolor='white', | |
| margin=dict(t=20, b=40), | |
| font=dict(family="Microsoft YaHei") | |
| ) | |
| fig.update_traces(marker_line_color='#FFD700', marker_line_width=1.5) | |
| st.plotly_chart(fig, use_container_width=True, key="cpi_chart") | |
| else: | |
| st.info("数据中未找到学校标识列") | |
| elif current == "话语提取": | |
| st.markdown("### 🗣️ 核心话语体系监测") | |
| txt_col = None | |
| for c in ds.columns: | |
| if any(x in str(c) for x in ['文本', '内容', 'text', '评论', '留言']): | |
| txt_col = c | |
| break | |
| if txt_col is None: | |
| txt_col = ds.columns[0] | |
| full_txt = "".join(ds[txt_col].astype(str)) | |
| kws = ["铸牢共同体意识", "中华民族", "统一", "共同体", "团结"] | |
| k_df = pd.DataFrame([{'词汇': k, '频数': len(re.findall(k, full_txt))} for k in kws]) | |
| fig = px.bar(k_df, x='词汇', y='频数', | |
| color='频数', | |
| color_continuous_scale=['#FFB3B3', '#DE2910'], | |
| text_auto=True) | |
| fig.update_layout( | |
| height=480, | |
| showlegend=False, | |
| xaxis_title="", | |
| yaxis_title="出现频次", | |
| coloraxis_showscale=False, | |
| plot_bgcolor='white', | |
| paper_bgcolor='white', | |
| margin=dict(t=20, b=40), | |
| font=dict(family="Microsoft YaHei") | |
| ) | |
| st.plotly_chart(fig, use_container_width=True, key="word_chart") | |
| elif current == "情感对比": | |
| st.markdown("### 🧠 情感极性对比分析") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown("**官方宣教情感分布**") | |
| official_data = pd.DataFrame({ | |
| 'sentiment': ['积极', '中性', '消极'], | |
| 'count': [92, 6, 2] | |
| }) | |
| fig1 = px.pie(official_data, values='count', names='sentiment', | |
| color='sentiment', | |
| color_discrete_map=color_map, | |
| hole=0.4) | |
| fig1.update_layout( | |
| height=400, | |
| showlegend=True, | |
| legend=dict(orientation="h", yanchor="bottom", y=-0.15), | |
| plot_bgcolor='white', | |
| paper_bgcolor='white', | |
| margin=dict(t=10, b=60), | |
| font=dict(family="Microsoft YaHei") | |
| ) | |
| st.plotly_chart(fig1, use_container_width=True, key="official_sentiment") | |
| with col2: | |
| st.markdown("**社媒公众反馈分布**") | |
| label_col = None | |
| for c in ds.columns: | |
| if any(x in str(c).lower() for x in ['标签', '情感', 'sentiment', '情绪', '评价']): | |
| label_col = c | |
| break | |
| if label_col is None: | |
| label_col = ds.columns[-1] | |
| s_v = ds[label_col].value_counts().reset_index() | |
| s_v.columns = ['sentiment', 'count'] | |
| sentiment_mapping = {} | |
| for val in s_v['sentiment'].unique(): | |
| val_str = str(val).lower() | |
| if any(x in val_str for x in ['积极', '正面', '好评', 'pos', '好']): | |
| sentiment_mapping[val] = '积极' | |
| elif any(x in val_str for x in ['消极', '负面', '差评', 'neg', '差']): | |
| sentiment_mapping[val] = '消极' | |
| else: | |
| sentiment_mapping[val] = '中性' | |
| s_v['sentiment'] = s_v['sentiment'].map(sentiment_mapping) | |
| s_v = s_v.groupby('sentiment')['count'].sum().reset_index() | |
| fig2 = px.pie(s_v, values='count', names='sentiment', | |
| color='sentiment', | |
| color_discrete_map=color_map, | |
| hole=0.4) | |
| fig2.update_layout( | |
| height=400, | |
| showlegend=True, | |
| legend=dict(orientation="h", yanchor="bottom", y=-0.15), | |
| plot_bgcolor='white', | |
| paper_bgcolor='white', | |
| margin=dict(t=10, b=60), | |
| font=dict(family="Microsoft YaHei") | |
| ) | |
| st.plotly_chart(fig2, use_container_width=True, key="social_sentiment") | |
| elif current == "热度地图": | |
| st.markdown("### 🗺️ 全国传播热度映射") | |
| if os.path.exists("ditu.jpg"): | |
| with open("ditu.jpg", "rb") as f: | |
| map_data = f.read() | |
| map_b64 = base64.b64encode(map_data).decode() | |
| st.markdown(f""" | |
| <div class="map-wrapper"> | |
| <div class="map-container"> | |
| <img src="data:image/jpeg;base64,{map_b64}" alt="热度地图" /> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.caption("📍 全国高校中华民族共同体意识传播热度分布") | |
| else: | |
| st.markdown(""" | |
| <div class="map-wrapper"> | |
| <div class="map-container"> | |
| <div class="map-placeholder"> | |
| <div style="font-size: 60px; margin-bottom: 15px;">🗺️</div> | |
| <p style="font-size: 16px; margin-bottom: 8px;">热度地图预留位置</p> | |
| <p style="font-size: 13px; font-weight: normal; color: #666;"> | |
| 请在左侧上传地图图片 | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| elif current == "优化策略": | |
| st.markdown("### 💡 传播优化策略建议") | |
| if os.path.exists("data_p.csv"): | |
| dp = pd.read_csv("data_p.csv", encoding='utf-8-sig') | |
| display_cols = [c for c in dp.columns if any(x in c for x in ['策略', '建议', '内容', '类型', '优先级'])] | |
| if display_cols: | |
| st.dataframe(dp[display_cols].head(10), use_container_width=True, hide_index=True) | |
| else: | |
| st.dataframe(dp.head(10), use_container_width=True, hide_index=True) | |
| else: | |
| # 不显示提示信息,直接显示默认策略 | |
| dp = generate_default_strategies() | |
| st.dataframe(dp, use_container_width=True, hide_index=True) | |
| elif current == "学术语境分析": | |
| st.markdown("### 📚 学术语境深度分析") | |
| # 检查是否有学术分析数据 | |
| academic_data_exists = os.path.exists("data_academic.csv") | |
| if academic_data_exists: | |
| try: | |
| def load_academic_data(): | |
| return pd.read_csv("data_academic.csv", encoding='utf-8-sig') | |
| df_academic = load_academic_data() | |
| # 数据列识别 | |
| text_col = None | |
| region_col = None | |
| media_col = None | |
| keyword_col = None | |
| for c in df_academic.columns: | |
| c_str = str(c).lower() | |
| if any(x in c_str for x in ['文本', '内容', 'text', '表述', '原文']): | |
| text_col = c | |
| if any(x in c_str for x in ['地区', '地域', '省份', 'region', 'location', 'ip', '属地']): | |
| region_col = c | |
| if any(x in c_str for x in ['媒体', '平台', '来源', 'media', 'platform', 'source']): | |
| media_col = c | |
| if any(x in c_str for x in ['关键词', '主题', '词汇', 'keyword', 'topic', '事件']): | |
| keyword_col = c | |
| # 如果未识别到列,使用默认列 | |
| if text_col is None: | |
| text_col = df_academic.columns[0] | |
| if region_col is None and len(df_academic.columns) > 1: | |
| region_col = df_academic.columns[1] | |
| if media_col is None and len(df_academic.columns) > 2: | |
| media_col = df_academic.columns[2] | |
| # 侧边栏分析配置 | |
| with st.sidebar: | |
| st.markdown('<div class="sidebar-card"><div class="sidebar-title">🔍 分析配置</div>', unsafe_allow_html=True) | |
| # 选择分析类型 | |
| analysis_type = st.selectbox( | |
| "选择分析维度", | |
| ["核心词汇分析", "历史事件分析", "地域差异对比", "媒体表述差异"], | |
| key="analysis_type" | |
| ) | |
| # 核心词汇选择 | |
| if analysis_type == "核心词汇分析": | |
| target_words = st.multiselect( | |
| "选择核心词汇", | |
| ["筑牢", "中华民族共同体", "民族团结", "五个认同", "石榴籽", "休戚与共", "荣辱与共"], | |
| default=["筑牢", "中华民族共同体"] | |
| ) | |
| elif analysis_type == "历史事件分析": | |
| target_words = st.multiselect( | |
| "选择历史事件", | |
| ["五胡入华", "五胡乱华", "民族融合", "华夷之辨", "多元一体", "大一统"], | |
| default=["五胡入华"] | |
| ) | |
| else: | |
| target_words = st.text_input("输入分析关键词(多个用逗号分隔)", "筑牢,五胡入华") | |
| target_words = [w.strip() for w in target_words.split(",")] | |
| # Hugging Face模型选择 | |
| hf_model = st.selectbox( | |
| "选择语义分析模型", | |
| ["uer/roberta-base-finetuned-jd-binary-chinese", | |
| "distilbert-base-chinese", | |
| "bert-base-chinese", | |
| "自定义模型"], | |
| key="hf_model" | |
| ) | |
| if hf_model == "自定义模型": | |
| custom_model = st.text_input("输入Hugging Face模型ID", "uer/roberta-base-finetuned-jd-binary-chinese") | |
| else: | |
| custom_model = hf_model | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # 主分析区域 | |
| col1, col2 = st.columns([2, 1]) | |
| with col1: | |
| st.markdown("#### 🎯 语境语义分析") | |
| # 模拟Hugging Face模型分析结果(实际使用时需要接入真实API) | |
| # 这里使用基于规则的分析来模拟大模型效果 | |
| analysis_results = [] | |
| for word in target_words: | |
| if text_col in df_academic.columns: | |
| # 查找包含该词的文本 | |
| mask = df_academic[text_col].astype(str).str.contains(word, na=False) | |
| related_texts = df_academic[mask] | |
| if len(related_texts) > 0: | |
| # 分析不同地区的表述 | |
| if region_col and region_col in df_academic.columns: | |
| region_stats = related_texts[region_col].value_counts().head(5) | |
| for region, count in region_stats.items(): | |
| # 模拟语义倾向分析 | |
| sample_texts = related_texts[related_texts[region_col] == region][text_col].head(3).tolist() | |
| sentiment_score = analyze_text_sentiment(sample_texts, word) | |
| analysis_results.append({ | |
| '关键词': word, | |
| '地区': region, | |
| '出现频次': count, | |
| '语义倾向': sentiment_score['sentiment'], | |
| '情感得分': sentiment_score['score'], | |
| '典型表述': sentiment_score['example'][:50] + "..." | |
| }) | |
| if analysis_results: | |
| result_df = pd.DataFrame(analysis_results) | |
| # 热力图展示 | |
| pivot_df = result_df.pivot_table( | |
| values='情感得分', | |
| index='关键词', | |
| columns='地区', | |
| aggfunc='mean' | |
| ).fillna(0) | |
| fig_heatmap = px.imshow( | |
| pivot_df, | |
| color_continuous_scale=['#DE2910', '#FFB3B3', '#FFD700'], | |
| aspect="auto", | |
| title="地域-关键词情感倾向热力图" | |
| ) | |
| fig_heatmap.update_layout( | |
| height=400, | |
| plot_bgcolor='white', | |
| paper_bgcolor='white', | |
| font=dict(family="Microsoft YaHei") | |
| ) | |
| st.plotly_chart(fig_heatmap, use_container_width=True, key="academic_heatmap") | |
| # 详细数据表 | |
| st.markdown("#### 📊 详细分析数据") | |
| st.dataframe( | |
| result_df.sort_values('情感得分', ascending=False), | |
| use_container_width=True, | |
| hide_index=True, | |
| column_config={ | |
| '情感得分': st.column_config.ProgressColumn( | |
| '情感得分', | |
| help='语义情感强度', | |
| format='%.2f', | |
| min_value=0, | |
| max_value=1, | |
| ) | |
| } | |
| ) | |
| else: | |
| st.info("未找到相关分析数据,请确保上传的数据包含关键词匹配的内容") | |
| with col2: | |
| st.markdown("#### 🧠 大模型洞察") | |
| # 模拟大模型分析摘要 | |
| if analysis_results: | |
| st.markdown(""" | |
| <div style="background: linear-gradient(135deg, #FFF9F9 0%, #FFFFFF 100%); | |
| padding: 15px; border-radius: 10px; border-left: 4px solid #DE2910;"> | |
| <h4 style="color: #DE2910; margin-top: 0;">📝 表述差异发现</h4> | |
| """, unsafe_allow_html=True) | |
| # 生成洞察文本 | |
| insights = generate_academic_insights(analysis_results, target_words) | |
| for insight in insights: | |
| st.markdown(f""" | |
| <div style="margin-bottom: 12px; padding: 10px; background: white; | |
| border-radius: 6px; border: 1px solid rgba(222,41,16,0.1);"> | |
| <strong style="color: #DE2910;">{insight['title']}</strong><br> | |
| <span style="color: #666; font-size: 13px;">{insight['content']}</span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # 学术建议 | |
| st.markdown(""" | |
| <div style="background: linear-gradient(135deg, #FFFCF8 0%, #FFF9F9 100%); | |
| padding: 15px; border-radius: 10px; margin-top: 15px; | |
| border-left: 4px solid #FFD700;"> | |
| <h4 style="color: #DE2910; margin-top: 0;">💡 学术建议</h4> | |
| <ul style="color: #666; font-size: 13px; padding-left: 20px;"> | |
| <li>关注地域表述差异,调整传播策略</li> | |
| <li>监测历史事件表述的情感倾向变化</li> | |
| <li>建立核心词汇的语境使用规范</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # 媒体差异分析 | |
| if media_col and media_col in df_academic.columns: | |
| st.markdown("#### 📺 媒体表述差异对比") | |
| media_col1, media_col2 = st.columns(2) | |
| with media_col1: | |
| # 媒体分布 | |
| media_dist = df_academic[media_col].value_counts().head(8).reset_index() | |
| media_dist.columns = ['媒体类型', '数量'] | |
| fig_media = px.pie( | |
| media_dist, | |
| values='数量', | |
| names='媒体类型', | |
| color_discrete_sequence=['#DE2910', '#C41E0B', '#FFD700', '#FFA500', '#FFB3B3', '#999999'] | |
| ) | |
| fig_media.update_layout( | |
| height=350, | |
| showlegend=True, | |
| legend=dict(orientation="h", yanchor="bottom", y=-0.2), | |
| plot_bgcolor='white', | |
| paper_bgcolor='white' | |
| ) | |
| st.plotly_chart(fig_media, use_container_width=True, key="media_pie") | |
| with media_col2: | |
| # 关键词在不同媒体中的使用 | |
| if target_words: | |
| media_keyword_data = [] | |
| for word in target_words[:2]: # 只取前两个避免数据过于稀疏 | |
| for media in df_academic[media_col].unique()[:5]: | |
| mask = (df_academic[text_col].astype(str).str.contains(word, na=False)) & \ | |
| (df_academic[media_col] == media) | |
| count = mask.sum() | |
| media_keyword_data.append({ | |
| '关键词': word, | |
| '媒体': media, | |
| '提及次数': count | |
| }) | |
| if media_keyword_data: | |
| mk_df = pd.DataFrame(media_keyword_data) | |
| fig_mk = px.bar( | |
| mk_df, | |
| x='媒体', | |
| y='提及次数', | |
| color='关键词', | |
| barmode='group', | |
| color_discrete_map={'筑牢': '#DE2910', '五胡入华': '#FFD700'} | |
| ) | |
| fig_mk.update_layout( | |
| height=350, | |
| plot_bgcolor='white', | |
| paper_bgcolor='white', | |
| xaxis_title="", | |
| yaxis_title="提及频次" | |
| ) | |
| st.plotly_chart(fig_mk, use_container_width=True, key="media_keyword_bar") | |
| except Exception as e: | |
| st.error(f"学术分析数据处理出错: {str(e)}") | |
| st.info("请检查数据格式是否正确,或尝试重新上传数据") | |
| else: | |
| # 使用示例数据展示功能 | |
| st.markdown(""" | |
| <div class="welcome-container"> | |
| <div style="font-size: 50px; margin-bottom: 10px;">📚</div> | |
| <h3 style="color: #DE2910; margin-bottom: 10px;">学术语境分析模块</h3> | |
| <p style="color: #666; font-size: 14px; margin-bottom: 20px;"> | |
| 利用Hugging Face大模型分析核心词汇和历史事件的地域/媒体表述差异 | |
| </p> | |
| <div style="text-align: left; max-width: 600px; margin: 0 auto;"> | |
| <div style="background: #FFF9F9; padding: 12px; border-radius: 8px; margin-bottom: 10px; border-left: 3px solid #FFD700;"> | |
| <strong style="color: #DE2910;">支持分析内容:</strong><br> | |
| <span style="color: #666; font-size: 13px;"> | |
| • 核心词汇:"筑牢"、"中华民族共同体"、"五个认同"等<br> | |
| • 历史事件:"五胡入华"、"民族融合"、"华夷之辨"等<br> | |
| • 地域差异:不同省份/地区的表述倾向对比<br> | |
| • 媒体差异:知识社区、新闻平台、社交媒体的表述差异 | |
| </span> | |
| </div> | |
| <div style="background: #FFF9F9; padding: 12px; border-radius: 8px; border-left: 3px solid #FFD700;"> | |
| <strong style="color: #DE2910;">数据格式要求:</strong><br> | |
| <span style="color: #666; font-size: 13px;"> | |
| 请上传包含以下列的CSV/Excel文件:<br> | |
| • 文本内容/表述原文<br> | |
| • 地区/IP属地<br> | |
| • 媒体类型/平台<br> | |
| • 关键词/主题(可选) | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # 所有分支结束后关闭 content-card | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| except Exception as e: | |
| st.error(f"数据处理出错: {str(e)}") | |
| st.info("请尝试重新上传数据") | |
| else: | |
| # 欢迎页面 | |
| st.markdown(""" | |
| <div class="welcome-container"> | |
| <div style="font-size: 60px; margin-bottom: 15px;">📊</div> | |
| <h2 style="color: #DE2910; margin-bottom: 10px;">欢迎使用分析平台</h2> | |
| <p style="color: #666; font-size: 15px; margin-bottom: 25px;"> | |
| 请在左侧管理面板上传数据并点击「固化并发布」以开始分析 | |
| </p> | |
| <div style="display: flex; justify-content: center; gap: 15px; flex-wrap: wrap;"> | |
| <div style="background: #FFF9F9; padding: 12px 20px; border-radius: 8px; border-left: 3px solid #FFD700;"> | |
| <strong style="color: #DE2910;">步骤 1</strong><br> | |
| <span style="color: #666; font-size: 13px;">上传高校官方数据</span> | |
| </div> | |
| <div style="background: #FFF9F9; padding: 12px 20px; border-radius: 8px; border-left: 3px solid #FFD700;"> | |
| <strong style="color: #DE2910;">步骤 2</strong><br> | |
| <span style="color: #666; font-size: 13px;">上传社媒反馈语料</span> | |
| </div> | |
| <div style="background: #FFF9F9; padding: 12px 20px; border-radius: 8px; border-left: 3px solid #FFD700;"> | |
| <strong style="color: #DE2910;">步骤 3</strong><br> | |
| <span style="color: #666; font-size: 13px;">点击固化并发布</span> | |
| </div> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ============================================ | |
| # ============================================ | |
| # ============================================ | |
| # 7. 底部信息 | |
| # ============================================ | |
| st.markdown('<div class="footer">©2026 高校中华民族共同体意识传播分析平台 | 技术支持:大模型与大数据分析团队<br><span style="color: #DE2910;">铸牢中华民族共同体意识</span> | <span style="color: #FFD700;">同心共筑中国梦</span></div>', unsafe_allow_html=True) |