teadew's picture
Update app.py
c3ff88f verified
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. 辅助函数
# ============================================
@st.cache_data
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'
}
@st.cache_data
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:
@st.cache_data
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:
@st.cache_data
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)