cwadayi commited on
Commit
efb533d
·
verified ·
1 Parent(s): 959c7bb

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +303 -152
src/streamlit_app.py CHANGED
@@ -1,161 +1,312 @@
1
  import streamlit as st
2
  import pandas as pd
3
- import numpy as np
4
-
5
- # --- 核心計算函式 (保持不變) ---
6
-
7
- def calculate_theoretical_pga(magnitude, distance):
8
- if distance <= 0:
9
- return np.inf
10
- exp_term = np.exp(0.533 * magnitude)
11
- dist_term = distance ** (-1.607)
12
- return 1.657 * exp_term * dist_term
13
-
14
- def calculate_site_amplification(df):
15
- df_copy = df.copy()
16
- df_copy['theoretical_pga'] = df_copy.apply(
17
- lambda row: calculate_theoretical_pga(row['magnitude'], row['distance_km']),
18
- axis=1
19
- )
20
- df_copy['single_event_amplification'] = df_copy['observed_pga'] / df_copy['theoretical_pga']
21
- site_amplification_factors = df_copy.groupby('station_id')['single_event_amplification'].mean().reset_index()
22
- site_amplification_factors.rename(columns={'single_event_amplification': 'site_amplification_factor_S'}, inplace=True)
23
- return df_copy, site_amplification_factors
24
-
25
- # --- 創意功能函式 ---
26
-
27
- def get_s_factor_interpretation(s_value):
28
- """將 S 因子轉換成白話文解說"""
29
- if s_value > 2.0:
30
- return f"🔴 **極顯著放大 ({s_value:.2f})**: 此處地質鬆軟,搖晃程度可能是堅硬岩盤地區的 **2倍以上**!需特別注意建築結構安全。"
31
- elif s_value > 1.5:
32
- return f"🟠 **顯著放大 ({s_value:.2f})**: 此處有明顯的場址放大效應,搖晃會比理論值強烈很多。"
33
- elif s_value > 1.1:
34
- return f"🟡 **輕微放大 ({s_value:.2f})**: 搖晃程度略高於理論值,存在一定的放大效應。"
35
- elif s_value < 0.9:
36
- return f"🔵 **減弱效應 ({s_value:.2f})**: 此處地質可能較為堅硬,搖晃程度反而比理論值小。"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  else:
38
- return f"🟢 **接近基準 ({s_value:.2f})**: 此處的搖晃程度與標準岩盤地區接近。"
 
39
 
40
- def predict_pga_with_s(magnitude, distance, s_factor):
41
- """使用S因子預測一個假想地震的PGA"""
42
- base_pga = calculate_theoretical_pga(magnitude, distance)
43
- return base_pga * s_factor
44
-
45
- # --- Streamlit 介面 ---
46
-
47
- st.set_page_config(page_title="創意場址放大因子儀表板", layout="wide")
48
-
49
- # 1. 標題與介紹
50
- st.title("🌋 創意場址放大因子儀表板")
51
- st.image("https://media.giphy.com/media/l41lGvinEgARjB2HC/giphy.gif", caption="地震波在地層中傳遞示意圖")
52
- st.markdown("不僅計算 S 因子,更透過地圖、情境模擬,讓您『看見』並『感受』場址效應的威力!")
53
-
54
-
55
- # 2. 側邊欄
56
- st.sidebar.header("Step 1: 載入資料")
57
- data_source = st.sidebar.radio("請選擇資料來源:", ("使用內建範例資料", "上傳自己的 CSV 檔案"))
58
-
59
- input_df = None
60
- if data_source == "使用內建範例資料":
61
- st.sidebar.info("範例資料已加入經緯度(lat, lon),以用於地圖視覺化。")
62
- data = {
63
- 'station_id': ['TPE', 'KHH', 'HUA', 'TPE', 'KHH', 'HUA', 'TPE', 'KHH', 'HUA', 'TPE', 'KHH', 'HUA'],
64
- 'lat': [25.0330, 22.6273, 23.9739, 25.0330, 22.6273, 23.9739, 25.0330, 22.6273, 23.9739, 25.0330, 22.6273, 23.9739],
65
- 'lon': [121.5654, 120.3014, 121.6059, 121.5654, 120.3014, 121.6059, 121.5654, 120.3014, 121.6059, 121.5654, 120.3014, 121.6059],
66
- 'earthquake_id': ['EQ1', 'EQ1', 'EQ1', 'EQ2', 'EQ2', 'EQ2', 'EQ3', 'EQ3', 'EQ3', 'EQ4', 'EQ4', 'EQ4'],
67
- 'magnitude': [6.2, 6.2, 6.2, 5.5, 5.5, 5.5, 7.0, 7.0, 7.0, 6.5, 6.5, 6.5],
68
- 'distance_km': [50, 200, 30, 80, 150, 60, 120, 250, 40, 40, 180, 25],
69
- # KHH(高雄)的地質條件較軟,預期會有較大的放大效應
70
- 'observed_pga': [80, 40, 150, 30, 25, 60, 60, 35, 180, 150, 55, 280]
71
- }
72
- input_df = pd.DataFrame(data)
73
- else:
74
- uploaded_file = st.sidebar.file_uploader("上傳 CSV (需含 lat, lon 欄位)", type=["csv"])
75
- if uploaded_file:
76
- input_df = pd.read_csv(uploaded_file)
77
-
78
- # 3. 主畫面
79
- if input_df is not None:
80
- st.sidebar.header("Step 2: 執行計算")
81
- if st.sidebar.button("🚀 點我開始分析!"):
82
- with st.spinner('科學計算中,請稍候...'):
83
- required_cols = ['station_id', 'lat', 'lon', 'magnitude', 'distance_km', 'observed_pga']
84
- if not all(col in input_df.columns for col in required_cols):
85
- st.error(f"資料格式錯誤!請確保您的 CSV 包含以下欄位: {required_cols}")
86
  else:
87
- intermediate_df, final_factors_df = calculate_site_amplification(input_df)
88
- # 將 station 的經緯度資訊合併到最終結果中
89
- station_locations = input_df[['station_id', 'lat', 'lon']].drop_duplicates().set_index('station_id')
90
- st.session_state.final_results = final_factors_df.join(station_locations, on='station_id')
91
- st.session_state.intermediate_results = intermediate_df
92
- st.success("計算完成!請查看下方分頁結果。")
 
 
93
 
94
- if 'final_results' in st.session_state:
95
- # 使用分頁呈現結果
96
- tab1, tab2, tab3, tab4 = st.tabs(["📊 結果總覽", "🗺️ 地圖視覺化", "🔬 What-If 模擬器", "📄 資料詳情"])
97
-
98
- final_df = st.session_state.final_results
99
-
100
- with tab1:
101
- st.header("📊 各測站平均場址放大因子 (S)")
102
- col1, col2 = st.columns([0.5, 0.5])
103
- with col1:
104
- st.dataframe(final_df[['station_id', 'site_amplification_factor_S']].style.format({'site_amplification_factor_S': "{:.2f}"}))
105
- st.bar_chart(final_df.set_index('station_id')['site_amplification_factor_S'])
106
-
107
- with col2:
108
- st.subheader("💬 結果白話文解說")
109
- for index, row in final_df.iterrows():
110
- st.markdown(f"**{row['station_id']} 測站:**")
111
- st.markdown(get_s_factor_interpretation(row['site_amplification_factor_S']))
112
- st.markdown("---")
113
-
114
- with tab2:
115
- st.header("🗺️ 場址放大效應地理分佈")
116
- st.markdown("地圖上的點越大,代表該地的場址放大效應越強烈。")
117
-
118
- # 為了讓地圖上的點大小差異更明顯,進行正規化
119
- min_s = final_df['site_amplification_factor_S'].min()
120
- max_s = final_df['site_amplification_factor_S'].max()
121
- # 避免 max_s == min_s 的情況
122
- if (max_s - min_s) > 0:
123
- final_df['size'] = 50 + ((final_df['site_amplification_factor_S'] - min_s) / (max_s - min_s)) * 500
124
- else:
125
- final_df['size'] = 100
126
 
127
- st.map(final_df, latitude='lat', longitude='lon', size='size', zoom=6)
128
 
129
- with tab3:
130
- st.header("🔬 What-If 情境模擬器")
131
- st.markdown("如果現在發生一場地震,各地搖晃程度會是多少?")
132
-
133
- sim_col1, sim_col2 = st.columns(2)
134
- with sim_col1:
135
- sim_mag = st.slider("設定假想地震規模 (M)", min_value=4.0, max_value=8.0, value=6.5, step=0.1)
136
- with sim_col2:
137
- sim_dist = st.number_input("設定您與震央的距離 (公里)", min_value=10, max_value=300, value=50)
138
-
139
- st.markdown(f"#### 模擬結果:規模 **{sim_mag}**、距離 **{sim_dist}** 公里的地震")
140
-
141
- sim_results = []
142
- for index, row in final_df.iterrows():
143
- predicted_pga = predict_pga_with_s(sim_mag, sim_dist, row['site_amplification_factor_S'])
144
- sim_results.append({
145
- "測站": row['station_id'],
146
- "場址放大因子 (S)": f"{row['site_amplification_factor_S']:.2f}",
147
- "預估搖晃程度 (PGA)": f"{predicted_pga:.2f}"
148
- })
149
-
150
- st.table(pd.DataFrame(sim_results))
151
- st.info("💡 注意:PGA 越高,代表地表加速度越大,感受到的搖晃越劇烈。")
152
-
153
- with tab4:
154
- st.header("📄 詳細資料與計算過程")
155
- with st.expander("點此查看原始觀測資料"):
156
- st.dataframe(input_df)
157
- with st.expander("點此查看詳細計算過程 (含單次放大效應)"):
158
- st.dataframe(st.session_state.intermediate_results)
159
- else:
160
- st.info("請在左方側邊欄選擇資料來源,然後點擊按鈕開始分析。")
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
  import pandas as pd
3
+ import requests
4
+ from bs4 import BeautifulSoup
5
+ from transformers import pipeline
6
+ import plotly.express as px
7
+ from wordcloud import WordCloud
8
+ import matplotlib.pyplot as plt
9
+ from collections import Counter
10
+ import re
11
+
12
+ # --- 網頁設定與標題 ---
13
+ st.set_page_config(
14
+ page_title="地震輿情分析系統",
15
+ page_icon="📊",
16
+ layout="wide"
17
+ )
18
+
19
+ st.title("📊 地震輿情分析與洞察報告系統")
20
+ st.markdown("依據[規格文件](https.example.com)開發,旨在提供即時的地震相關公眾輿情分析。") # 您可以將連結替換為規格文件的實際網址
21
+
22
+ # --- 核心功能函式 ---
23
+
24
+ # FR1: 關鍵字搜尋與資料獲取 (使用 requests + BeautifulSoup 模擬)
25
+ # 注意:直接、大量的 Google 搜尋可能會被阻擋。在正式專案中,應使用 Google Search API。
26
+ # 這裡我們用 DuckDuckGo 作為一個較不易被阻擋的替代方案來示範。
27
+ def search_and_scrape(keyword, num_results):
28
+ """
29
+ 根據關鍵字搜尋並抓取網頁內容。
30
+ """
31
+ st.info(f"🔍 正在使用 DuckDuckGo 搜尋關鍵字:'{keyword}'...")
32
+ search_url = "https://html.duckduckgo.com/html/"
33
+ params = {"q": keyword}
34
+ headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"}
35
+
36
+ try:
37
+ response = requests.get(search_url, params=params, headers=headers, timeout=10)
38
+ response.raise_for_status()
39
+ soup = BeautifulSoup(response.text, 'html.parser')
40
+ links = [a['href'] for a in soup.find_all('a', class_='result__a')]
41
+ except requests.RequestException as e:
42
+ st.error(f"搜尋失敗:{e}")
43
+ return []
44
+
45
+ scraped_data = []
46
+ if not links:
47
+ st.warning("找不到相關的搜尋結果。")
48
+ return []
49
+
50
+ st.info(f"📈 找到 {len(links)} 個結果,將分析前 {num_results} 個...")
51
+
52
+ progress_bar = st.progress(0)
53
+ for i, url in enumerate(links[:num_results]):
54
+ try:
55
+ # 修正相對路徑的 URL
56
+ if url.startswith("//"):
57
+ url = "https:" + url
58
+
59
+ page_response = requests.get(url, headers=headers, timeout=10)
60
+ page_response.raise_for_status()
61
+ page_soup = BeautifulSoup(page_response.content, 'html.parser', from_encoding='utf-8')
62
+
63
+ # 提取純文字,移除 script 和 style 標籤
64
+ for script_or_style in page_soup(['script', 'style']):
65
+ script_or_style.decompose()
66
+
67
+ text = ' '.join(p.get_text() for p in page_soup.find_all('p'))
68
+ text = re.sub(r'\s+', ' ', text).strip() # 清理多餘空白
69
+
70
+ if len(text) > 100: # 只分析有足夠內容的頁面
71
+ scraped_data.append({"url": url, "content": text})
72
+ except Exception as e:
73
+ st.warning(f"無法抓取或解析 URL: {url} ({e})")
74
+
75
+ progress_bar.progress((i + 1) / num_results)
76
+
77
+ st.success(f"✅ 成功抓取並解析 {len(scraped_data)} 篇文章。")
78
+ return scraped_data
79
+
80
+ # FR2 & FR3: Hugging Face 模型分析 (情緒分析)
81
+ @st.cache_resource
82
+ def get_sentiment_pipeline(token):
83
+ """
84
+ 載入情緒分析模型。使用快取避免重複載入。
85
+ """
86
+ try:
87
+ # 使用一個支援多語言(含中文)的模型
88
+ sentiment_pipeline = pipeline(
89
+ "sentiment-analysis",
90
+ model="lxyuan/distilbert-base-multilingual-cased-sent-analysis-finance",
91
+ use_auth_token=token
92
+ )
93
+ return sentiment_pipeline
94
+ except Exception as e:
95
+ st.error(f"無法載入情緒分析模型,請確認您的 Token 是否正確且有權限存取此模型。錯誤:{e}")
96
+ return None
97
+
98
+ # FR4: 熱議焦點分析 (命名實體辨識)
99
+ @st.cache_resource
100
+ def get_ner_pipeline(token):
101
+ """
102
+ 載入命名實體辨識模型。
103
+ """
104
+ try:
105
+ # 使用 ckiplab 的模型,對繁體中文效果很好
106
+ ner_pipeline = pipeline(
107
+ "ner",
108
+ model="ckiplab/bert-base-chinese-ner",
109
+ use_auth_token=token
110
+ )
111
+ return ner_pipeline
112
+ except Exception as e:
113
+ st.error(f"無法載入命名實體辨識模型。錯誤:{e}")
114
+ return None
115
+
116
+ def analyze_content(data, sentiment_pipeline, ner_pipeline):
117
+ """
118
+ 對抓取到的內容進行全面的 NLP 分析。
119
+ """
120
+ sentiments = []
121
+ all_entities = []
122
+
123
+ st.info("🧠 正在進行 NLP 分析...")
124
+ progress_bar = st.progress(0)
125
+
126
+ for i, item in enumerate(data):
127
+ # 情緒分析
128
+ try:
129
+ # Hugging Face 模型通常有最大長度限制,這裡截斷處理
130
+ sentiment_result = sentiment_pipeline(item['content'][:512])
131
+ sentiments.append(sentiment_result[0]['label'])
132
+ except Exception:
133
+ sentiments.append('neutral') # 分析失敗則視為中性
134
+
135
+ # 命名實體辨識
136
+ try:
137
+ ner_results = ner_pipeline(item['content'])
138
+ # 只提取我們關心的實體,且長度大於1
139
+ entities = [entity['word'] for entity in ner_results if entity['entity'] in ['GPE', 'ORG', 'PERSON', 'LOC'] and len(entity['word']) > 1]
140
+ all_entities.extend(entities)
141
+ except Exception:
142
+ pass # 分析失敗則忽略
143
+
144
+ progress_bar.progress((i + 1) / len(data))
145
+
146
+ st.success("🤖 NLP 分析完成!")
147
+ return sentiments, all_entities
148
+
149
+ # FR5: 結果視覺化
150
+ def create_sentiment_chart(sentiments):
151
+ """
152
+ 建立情緒分佈圖。
153
+ """
154
+ df = pd.DataFrame(sentiments, columns=['sentiment'])
155
+ sentiment_counts = df['sentiment'].value_counts().reset_index()
156
+ sentiment_counts.columns = ['sentiment', 'count']
157
+
158
+ fig = px.pie(sentiment_counts, names='sentiment', values='count',
159
+ title='整體情緒分佈',
160
+ color_discrete_map={'positive':'green', 'negative':'red', 'neutral':'blue'})
161
+ return fig
162
+
163
+ def create_word_cloud(entities):
164
+ """
165
+ 建立熱議焦點文字雲。
166
+ """
167
+ # 確保字體檔案路徑正確
168
+ font_path = 'NotoSansTC-Regular.otf'
169
+ text = ' '.join(entities)
170
+
171
+ if not text:
172
+ return None
173
+
174
+ try:
175
+ wordcloud = WordCloud(
176
+ width=800,
177
+ height=400,
178
+ background_color='white',
179
+ font_path=font_path
180
+ ).generate(text)
181
+
182
+ fig, ax = plt.subplots(figsize=(10, 5))
183
+ ax.imshow(wordcloud, interpolation='bilinear')
184
+ ax.axis('off')
185
+ return fig
186
+ except Exception as e:
187
+ st.error(f"無法生成文字雲,請確認字體檔案 'NotoSansTC-Regular.otf' 已上傳。錯誤:{e}")
188
+ return None
189
+
190
+ # FR6: 報告自動產出
191
+ def generate_report(sentiment_counts, top_entities, keyword):
192
+ """
193
+ 生成最終的分析報告。
194
+ """
195
+ total = sentiment_counts['count'].sum()
196
+ positive_pct = (sentiment_counts[sentiment_counts['sentiment'] == 'positive']['count'].sum() / total) * 100
197
+ negative_pct = (sentiment_counts[sentiment_counts['sentiment'] == 'negative']['count'].sum() / total) * 100
198
+ neutral_pct = 100 - positive_pct - negative_pct
199
+
200
+ # 罐頭建議
201
+ recommendation = "初步建議:輿情整體平穩,可持續觀察。"
202
+ if negative_pct > 40:
203
+ recommendation = f"初步建議:負面聲量較高,建議針對 '{', '.join(dict(top_entities).keys())}' 等熱議議題發布說明,以緩解公眾疑慮。"
204
+ elif positive_pct > 40:
205
+ recommendation = "初步建議:正面聲量佔優,可加強相關正面訊息的傳播。"
206
+
207
+ report = f"""
208
+ # 地震輿情分析報告
209
+
210
+ ## 1. 總覽
211
+ - **分析關鍵字**: {keyword}
212
+ - **分析摘要**: 針對網路輿情進行分析,目前情緒分佈以 **{sentiment_counts['sentiment'].iloc[0]}** 為主。
213
+
214
+ ## 2. 情緒儀表板
215
+ - **正面情緒**: {positive_pct:.2f}%
216
+ - **負面情緒**: {negative_pct:.2f}%
217
+ - **中性情緒**: {neutral_pct:.2f}%
218
+
219
+ ## 3. 熱議焦點 (Top 5)
220
+ """
221
+ for entity, count in top_entities:
222
+ report += f"- {entity} (提及 {count} 次)\n"
223
+
224
+ report += f"""
225
+ ## 4. 初步建議
226
+ {recommendation}
227
+ """
228
+ return report
229
+
230
+ # --- UI 介面 ---
231
+ # FR7: 使用者介面 (側邊欄)
232
+ st.sidebar.header("⚙️ 控制面板")
233
+
234
+ keyword = st.sidebar.text_input("1. 輸入搜尋關鍵字", "花蓮地震")
235
+ hf_token = st.sidebar.text_input("2. 輸入您的 Hugging Face Token", type="password")
236
+ num_results = st.sidebar.slider("3. 設定分析文章數量", min_value=5, max_value=20, value=10, step=1)
237
+
238
+ analyze_button = st.sidebar.button("🚀 開始分析", type="primary")
239
+
240
+
241
+ # --- 主流程 ---
242
+ if analyze_button:
243
+ if not keyword:
244
+ st.sidebar.error("請輸入關鍵字!")
245
+ elif not hf_token:
246
+ st.sidebar.error("請輸入 Hugging Face Token!")
247
  else:
248
+ # 執行分析
249
+ scraped_data = search_and_scrape(keyword, num_results)
250
 
251
+ if scraped_data:
252
+ sentiment_pipeline = get_sentiment_pipeline(hf_token)
253
+ ner_pipeline = get_ner_pipeline(hf_token)
254
+
255
+ if sentiment_pipeline and ner_pipeline:
256
+ sentiments, all_entities = analyze_content(scraped_data, sentiment_pipeline, ner_pipeline)
257
+
258
+ # 儲存結果以供後續使用
259
+ st.session_state['analysis_complete'] = True
260
+ st.session_state['sentiments'] = sentiments
261
+ st.session_state['all_entities'] = all_entities
262
+ st.session_state['keyword'] = keyword
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  else:
264
+ st.session_state['analysis_complete'] = False
265
+ else:
266
+ st.error("未能獲取任何可分析的資料。")
267
+ st.session_state['analysis_complete'] = False
268
+
269
+ # 在分析完成後顯示結果
270
+ if 'analysis_complete' in st.session_state and st.session_state['analysis_complete']:
271
+ st.header("📈 輿情儀表板")
272
 
273
+ sentiments = st.session_state['sentiments']
274
+ all_entities = st.session_state['all_entities']
275
+ keyword = st.session_state['keyword']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
+ col1, col2 = st.columns(2)
278
 
279
+ with col1:
280
+ # 情緒分析圖
281
+ sentiment_fig = create_sentiment_chart(sentiments)
282
+ st.plotly_chart(sentiment_fig, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
+ with col2:
285
+ # 熱議焦點文字雲
286
+ st.subheader("熱議焦點")
287
+ wordcloud_fig = create_word_cloud(all_entities)
288
+ if wordcloud_fig:
289
+ st.pyplot(wordcloud_fig)
290
+ else:
291
+ st.info("沒有足夠的關鍵詞來生成文字雲。")
292
+
293
+ st.header("📄 分析報告產出")
294
+
295
+ # 計算報告所需數據
296
+ sentiment_df = pd.DataFrame(sentiments, columns=['sentiment'])
297
+ sentiment_counts = sentiment_df['sentiment'].value_counts().reset_index()
298
+ sentiment_counts.columns = ['sentiment', 'count']
299
+
300
+ entity_counts = Counter(all_entities).most_common(5)
301
+
302
+ # 生成並顯示報告
303
+ report_text = generate_report(sentiment_counts, entity_counts, keyword)
304
+ st.markdown(report_text)
305
+
306
+ # 下載按鈕
307
+ st.download_button(
308
+ label="📥 下載完整報告 (.md)",
309
+ data=report_text,
310
+ file_name=f"輿情分析報告_{keyword}.md",
311
+ mime="text/markdown",
312
+ )