shiue2000 commited on
Commit
418284c
·
verified ·
1 Parent(s): 3c9a501

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +53 -32
app.py CHANGED
@@ -10,22 +10,18 @@ from datetime import datetime, timedelta
10
  import gradio as gr
11
  import logging
12
  from jinja2 import Template
13
-
14
  # ===== 字型與樣式 =====
15
  plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'Noto Sans TC', 'SimHei', 'Arial Unicode MS']
16
  plt.rcParams['axes.unicode_minus'] = False
17
  plt.style.use("seaborn-v0_8")
18
-
19
  # ===== 日誌 =====
20
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
21
-
22
  # ===== 參數 =====
23
  candidates = ["許智傑", "邱議瑩", "賴瑞隆", "林岱樺", "柯志恩"]
24
  days_back = 7
25
  max_tweets_per_candidate = 20
26
  news_file = "news_sample.csv"
27
  history_file = "history_sentiment.csv"
28
-
29
  # ===== 情緒分析 =====
30
  try:
31
  from transformers import pipeline
@@ -41,7 +37,6 @@ except:
41
  "label": random.choice(["positive", "negative", "neutral"]),
42
  "score": random.uniform(0.3, 0.9)
43
  }
44
-
45
  # ===== 模擬貼文抓取 =====
46
  def fetch_tweets(candidate):
47
  sample_texts = {
@@ -60,7 +55,6 @@ def fetch_tweets(candidate):
60
  }
61
  for i in range(random.randint(5, max_tweets_per_candidate))
62
  ])
63
-
64
  # ===== 工具: Matplotlib → base64 =====
65
  def fig_to_base64():
66
  buf = io.BytesIO()
@@ -70,11 +64,9 @@ def fig_to_base64():
70
  buf.close()
71
  plt.close()
72
  return img_b64
73
-
74
  # ===== 多圖產生器 =====
75
  def generate_charts(all_df, summary, df_hist):
76
  results = {}
77
-
78
  # 1. 每日情緒比例
79
  fig = plt.figure(figsize=(8, 5))
80
  summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].plot(
@@ -84,7 +76,6 @@ def generate_charts(all_df, summary, df_hist):
84
  plt.ylabel("比例")
85
  plt.xlabel("候選人")
86
  results["img_b64_today"] = fig_to_base64()
87
-
88
  # 2. 歷史情緒趨勢
89
  fig = plt.figure(figsize=(10, 5))
90
  for c in candidates:
@@ -98,7 +89,6 @@ def generate_charts(all_df, summary, df_hist):
98
  plt.ylabel("比例")
99
  plt.legend()
100
  results["img_b64_trend"] = fig_to_base64()
101
-
102
  # 3. 社群情緒趨勢
103
  sentiment_trend = all_df.groupby([pd.Grouper(key='Date', freq='D'), 'Sentiment']).size().unstack(fill_value=0)
104
  sentiment_trend = sentiment_trend.div(sentiment_trend.sum(axis=1), axis=0).fillna(0)
@@ -111,7 +101,6 @@ def generate_charts(all_df, summary, df_hist):
111
  plt.ylabel("比例")
112
  plt.legend()
113
  results["img_social_sentiment"] = fig_to_base64()
114
-
115
  # 4. 平台表現
116
  platforms = ["X", "Facebook", "Instagram", "PTT", "Line"]
117
  platform_counts = pd.Series({p: random.randint(10, 100) for p in platforms})
@@ -121,7 +110,6 @@ def generate_charts(all_df, summary, df_hist):
121
  plt.xlabel("平台")
122
  plt.ylabel("貼文數量")
123
  results["img_platform_performance"] = fig_to_base64()
124
-
125
  # 5. 候選人聲量趨勢
126
  candidate_trend = all_df.groupby([pd.Grouper(key='Date', freq='D'), 'Candidate']).size().unstack(fill_value=0)
127
  fig = plt.figure(figsize=(8, 5))
@@ -133,7 +121,6 @@ def generate_charts(all_df, summary, df_hist):
133
  plt.ylabel("貼文數量")
134
  plt.legend()
135
  results["img_candidate_volume"] = fig_to_base64()
136
-
137
  # 6. 候選人情緒分析
138
  fig = plt.figure(figsize=(8, 5))
139
  summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].plot(
@@ -143,7 +130,6 @@ def generate_charts(all_df, summary, df_hist):
143
  plt.ylabel("比例")
144
  plt.xlabel("候選人")
145
  results["img_candidate_sentiment"] = fig_to_base64()
146
-
147
  # 7. 知識圖譜
148
  fig, ax = plt.subplots(figsize=(8, 6))
149
  G = nx.Graph()
@@ -153,28 +139,72 @@ def generate_charts(all_df, summary, df_hist):
153
  G.add_edge(candidates[i], candidates[i + 1])
154
  nx.draw(G, nx.spring_layout(G), with_labels=True, node_color='lightgreen', font_size=12, ax=ax)
155
  results["img_knowledge_graph"] = fig_to_base64()
156
-
157
  return results
158
-
159
  # ===== 主分析函數 =====
160
  def run_analysis():
161
  try:
162
- template_path = "templates/index.html"
163
- if not os.path.exists(template_path):
164
- return f"<pre>❌ 模板檔案 {template_path} 未找到</pre>"
165
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  # --- 貼文 & 情緒分析 ---
167
  all_df = pd.concat([fetch_tweets(c) for c in candidates], ignore_index=True)
168
  all_df['Sentiment'] = all_df['Content'].apply(lambda x: sentiment(x)['label'])
169
  all_df['Confidence'] = all_df['Content'].apply(lambda x: sentiment(x)['score'])
170
-
171
  # --- 統計 ---
172
  summary = all_df.groupby(['Candidate', 'Sentiment']).size().unstack(fill_value=0)
173
  summary['Total Posts'] = summary.sum(axis=1)
174
  summary['Positive Ratio'] = summary.get('positive', 0) / summary['Total Posts'].replace(0, 1)
175
  summary['Negative Ratio'] = summary.get('negative', 0) / summary['Total Posts'].replace(0, 1)
176
  summary['Neutral Ratio'] = summary.get('neutral', 0) / summary['Total Posts'].replace(0, 1)
177
-
178
  # --- 歷史資料 ---
179
  today_str = datetime.now().strftime('%Y-%m-%d')
180
  hist_row = summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].copy()
@@ -185,10 +215,8 @@ def run_analysis():
185
  ignore_index=True
186
  ) if os.path.exists(history_file) else hist_row
187
  df_hist.to_csv(history_file, index=False)
188
-
189
  # --- 圖表 ---
190
  charts = generate_charts(all_df, summary, df_hist)
191
-
192
  # --- 新聞 ---
193
  if os.path.exists(news_file):
194
  df_news = pd.read_csv(news_file)
@@ -201,10 +229,8 @@ def run_analysis():
201
  "爭議": "林岱樺涉助理費爭議。"
202
  }
203
  news_table = "<p>無新聞資料</p>"
204
-
205
  # Convert news_summary to list of tuples to support iteration in template
206
  news_summary = list(news_summary.items())
207
-
208
  # --- 參與表 ---
209
  engagement_table = f"""
210
  <table class="min-w-full bg-white border border-gray-200">
@@ -221,9 +247,6 @@ def run_analysis():
221
  </table>
222
  """
223
  # --- HTML 渲染 ---
224
- with open(template_path, encoding='utf-8') as f:
225
- html_template = f.read()
226
-
227
  template = Template(html_template)
228
  html_content = template.render(
229
  report_date=datetime.now().strftime('%Y-%m-%d %H:%M'),
@@ -232,12 +255,10 @@ def run_analysis():
232
  news_table=news_table if news_table else "<p>未提供新聞資料</p>",
233
  **charts
234
  )
235
-
236
  return html_content
237
-
238
  except Exception:
239
  return f"<pre>❌ 分析失敗:\n{traceback.format_exc()}</pre>"
240
-
241
  # ===== Gradio 前端 =====
242
  if __name__ == "__main__":
243
  iface = gr.Interface(
 
10
  import gradio as gr
11
  import logging
12
  from jinja2 import Template
 
13
  # ===== 字型與樣式 =====
14
  plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'Noto Sans TC', 'SimHei', 'Arial Unicode MS']
15
  plt.rcParams['axes.unicode_minus'] = False
16
  plt.style.use("seaborn-v0_8")
 
17
  # ===== 日誌 =====
18
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
19
  # ===== 參數 =====
20
  candidates = ["許智傑", "邱議瑩", "賴瑞隆", "林岱樺", "柯志恩"]
21
  days_back = 7
22
  max_tweets_per_candidate = 20
23
  news_file = "news_sample.csv"
24
  history_file = "history_sentiment.csv"
 
25
  # ===== 情緒分析 =====
26
  try:
27
  from transformers import pipeline
 
37
  "label": random.choice(["positive", "negative", "neutral"]),
38
  "score": random.uniform(0.3, 0.9)
39
  }
 
40
  # ===== 模擬貼文抓取 =====
41
  def fetch_tweets(candidate):
42
  sample_texts = {
 
55
  }
56
  for i in range(random.randint(5, max_tweets_per_candidate))
57
  ])
 
58
  # ===== 工具: Matplotlib → base64 =====
59
  def fig_to_base64():
60
  buf = io.BytesIO()
 
64
  buf.close()
65
  plt.close()
66
  return img_b64
 
67
  # ===== 多圖產生器 =====
68
  def generate_charts(all_df, summary, df_hist):
69
  results = {}
 
70
  # 1. 每日情緒比例
71
  fig = plt.figure(figsize=(8, 5))
72
  summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].plot(
 
76
  plt.ylabel("比例")
77
  plt.xlabel("候選人")
78
  results["img_b64_today"] = fig_to_base64()
 
79
  # 2. 歷史情緒趨勢
80
  fig = plt.figure(figsize=(10, 5))
81
  for c in candidates:
 
89
  plt.ylabel("比例")
90
  plt.legend()
91
  results["img_b64_trend"] = fig_to_base64()
 
92
  # 3. 社群情緒趨勢
93
  sentiment_trend = all_df.groupby([pd.Grouper(key='Date', freq='D'), 'Sentiment']).size().unstack(fill_value=0)
94
  sentiment_trend = sentiment_trend.div(sentiment_trend.sum(axis=1), axis=0).fillna(0)
 
101
  plt.ylabel("比例")
102
  plt.legend()
103
  results["img_social_sentiment"] = fig_to_base64()
 
104
  # 4. 平台表現
105
  platforms = ["X", "Facebook", "Instagram", "PTT", "Line"]
106
  platform_counts = pd.Series({p: random.randint(10, 100) for p in platforms})
 
110
  plt.xlabel("平台")
111
  plt.ylabel("貼文數量")
112
  results["img_platform_performance"] = fig_to_base64()
 
113
  # 5. 候選人聲量趨勢
114
  candidate_trend = all_df.groupby([pd.Grouper(key='Date', freq='D'), 'Candidate']).size().unstack(fill_value=0)
115
  fig = plt.figure(figsize=(8, 5))
 
121
  plt.ylabel("貼文數量")
122
  plt.legend()
123
  results["img_candidate_volume"] = fig_to_base64()
 
124
  # 6. 候選人情緒分析
125
  fig = plt.figure(figsize=(8, 5))
126
  summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].plot(
 
130
  plt.ylabel("比例")
131
  plt.xlabel("候選人")
132
  results["img_candidate_sentiment"] = fig_to_base64()
 
133
  # 7. 知識圖譜
134
  fig, ax = plt.subplots(figsize=(8, 6))
135
  G = nx.Graph()
 
139
  G.add_edge(candidates[i], candidates[i + 1])
140
  nx.draw(G, nx.spring_layout(G), with_labels=True, node_color='lightgreen', font_size=12, ax=ax)
141
  results["img_knowledge_graph"] = fig_to_base64()
 
142
  return results
 
143
  # ===== 主分析函數 =====
144
  def run_analysis():
145
  try:
146
+ # Embed the template as a string to avoid file dependency and ensure syntax is correct
147
+ html_template = """
148
+ <!DOCTYPE html>
149
+ <html lang="zh-TW">
150
+ <head>
151
+ <meta charset="UTF-8">
152
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
153
+ <title>2026 高雄市長選舉輿情分析報告</title>
154
+ <script src="https://cdn.tailwindcss.com"></script>
155
+ </head>
156
+ <body class="bg-gray-100 font-sans leading-normal tracking-normal">
157
+ <div class="container mx-auto p-4">
158
+ <h1 class="text-3xl font-bold mb-4">2026 高雄市長選舉輿情分析報告</h1>
159
+ <p class="mb-4">報告日期: {{ report_date }}</p>
160
+
161
+ <h2 class="text-2xl font-bold mb-2">參與度摘要</h2>
162
+ {{ engagement_table | safe }}
163
+
164
+ <h2 class="text-2xl font-bold mb-2">新聞摘要</h2>
165
+ <ul class="list-disc pl-5 mb-4">
166
+ {% for key, value in news_summary %}
167
+ <li><strong>{{ key }}</strong>: {{ value }}</li>
168
+ {% endfor %}
169
+ </ul>
170
+
171
+ <h2 class="text-2xl font-bold mb-2">新聞詳情</h2>
172
+ {{ news_table | safe }}
173
+
174
+ <h2 class="text-2xl font-bold mb-2">今日情緒比例</h2>
175
+ <img src="data:image/png;base64,{{ img_b64_today }}" alt="今日情緒比例" class="mb-4">
176
+
177
+ <h2 class="text-2xl font-bold mb-2">歷史情緒趨勢</h2>
178
+ <img src="data:image/png;base64,{{ img_b64_trend }}" alt="歷史情緒趨勢" class="mb-4">
179
+
180
+ <h2 class="text-2xl font-bold mb-2">社群情緒趨勢</h2>
181
+ <img src="data:image/png;base64,{{ img_social_sentiment }}" alt="社群情緒趨勢" class="mb-4">
182
+
183
+ <h2 class="text-2xl font-bold mb-2">平台表現</h2>
184
+ <img src="data:image/png;base64,{{ img_platform_performance }}" alt="平台表現" class="mb-4">
185
+
186
+ <h2 class="text-2xl font-bold mb-2">候選人聲量趨勢</h2>
187
+ <img src="data:image/png;base64,{{ img_candidate_volume }}" alt="候選人聲量趨勢" class="mb-4">
188
+
189
+ <h2 class="text-2xl font-bold mb-2">候選人情緒分析</h2>
190
+ <img src="data:image/png;base64,{{ img_candidate_sentiment }}" alt="候選人情緒分析" class="mb-4">
191
+
192
+ <h2 class="text-2xl font-bold mb-2">知識圖譜</h2>
193
+ <img src="data:image/png;base64,{{ img_knowledge_graph }}" alt="知識圖譜" class="mb-4">
194
+ </div>
195
+ </body>
196
+ </html>
197
+ """
198
  # --- 貼文 & 情緒分析 ---
199
  all_df = pd.concat([fetch_tweets(c) for c in candidates], ignore_index=True)
200
  all_df['Sentiment'] = all_df['Content'].apply(lambda x: sentiment(x)['label'])
201
  all_df['Confidence'] = all_df['Content'].apply(lambda x: sentiment(x)['score'])
 
202
  # --- 統計 ---
203
  summary = all_df.groupby(['Candidate', 'Sentiment']).size().unstack(fill_value=0)
204
  summary['Total Posts'] = summary.sum(axis=1)
205
  summary['Positive Ratio'] = summary.get('positive', 0) / summary['Total Posts'].replace(0, 1)
206
  summary['Negative Ratio'] = summary.get('negative', 0) / summary['Total Posts'].replace(0, 1)
207
  summary['Neutral Ratio'] = summary.get('neutral', 0) / summary['Total Posts'].replace(0, 1)
 
208
  # --- 歷史資料 ---
209
  today_str = datetime.now().strftime('%Y-%m-%d')
210
  hist_row = summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].copy()
 
215
  ignore_index=True
216
  ) if os.path.exists(history_file) else hist_row
217
  df_hist.to_csv(history_file, index=False)
 
218
  # --- 圖表 ---
219
  charts = generate_charts(all_df, summary, df_hist)
 
220
  # --- 新聞 ---
221
  if os.path.exists(news_file):
222
  df_news = pd.read_csv(news_file)
 
229
  "爭議": "林岱樺涉助理費爭議。"
230
  }
231
  news_table = "<p>無新聞資料</p>"
 
232
  # Convert news_summary to list of tuples to support iteration in template
233
  news_summary = list(news_summary.items())
 
234
  # --- 參與表 ---
235
  engagement_table = f"""
236
  <table class="min-w-full bg-white border border-gray-200">
 
247
  </table>
248
  """
249
  # --- HTML 渲染 ---
 
 
 
250
  template = Template(html_template)
251
  html_content = template.render(
252
  report_date=datetime.now().strftime('%Y-%m-%d %H:%M'),
 
255
  news_table=news_table if news_table else "<p>未提供新聞資料</p>",
256
  **charts
257
  )
258
+
259
  return html_content
 
260
  except Exception:
261
  return f"<pre>❌ 分析失敗:\n{traceback.format_exc()}</pre>"
 
262
  # ===== Gradio 前端 =====
263
  if __name__ == "__main__":
264
  iface = gr.Interface(