Edenhuang commited on
Commit
1529727
·
verified ·
1 Parent(s): 28477e1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +197 -318
app.py CHANGED
@@ -3,354 +3,233 @@ import requests
3
  from bs4 import BeautifulSoup
4
  import pandas as pd
5
  import time
6
- import random
7
- from datetime import datetime
8
- import io
9
 
10
- # Set page configuration
11
  st.set_page_config(
12
- page_title="台灣證券交易所公告擷取工具",
13
  page_icon="📊",
14
  layout="wide"
15
  )
16
 
17
- # App title and description
18
- st.title("台灣證券交易所公告擷取工具")
19
- st.markdown("這個應用程式可以擷取台灣證券交易所的公司公告資訊")
20
 
21
- def extract_data_from_html(html_content):
22
- """Extract data from HTML content and return as DataFrame"""
23
- # Parse HTML content
24
- soup = BeautifulSoup(html_content, 'html.parser')
25
-
26
- # Find the table
27
- table = soup.find('table', {'class': 'hasBorder'})
28
-
29
- # Lists to store data
30
- company_codes = []
31
- company_names = []
32
- announcement_dates = []
33
- announcement_times = []
34
- subjects = []
35
-
36
- # If table exists, extract rows
37
- if table:
38
- # Find all rows in tbody (skip header)
39
- tbody = table.find('tbody')
40
- if tbody:
41
- rows = tbody.find_all('tr')
42
- else:
43
- rows = table.find_all('tr')[1:] if len(table.find_all('tr')) > 1 else []
44
-
45
- for row in rows:
46
- # Extract cells
47
- cells = row.find_all('td')
48
-
49
- if len(cells) >= 5:
50
- # Extract cell data
51
- company_codes.append(cells[0].text.strip())
52
- company_names.append(cells[1].text.strip())
53
- announcement_dates.append(cells[2].text.strip())
54
- announcement_times.append(cells[3].text.strip())
55
-
56
- # Get subject from button title attribute if available
57
- subject_cell = cells[4]
58
- subject_button = subject_cell.find('button')
59
- if subject_button and 'title' in subject_button.attrs:
60
- subjects.append(subject_button['title'].strip())
61
- else:
62
- subjects.append(subject_cell.text.strip())
63
-
64
- # Create DataFrame
65
- df = pd.DataFrame({
66
- '公司代號': company_codes,
67
- '公司簡稱': company_names,
68
- '發言日期': announcement_dates,
69
- '發言時間': announcement_times,
70
- '主旨': subjects
71
- })
72
-
73
- return df
74
 
75
- # Function to extract data from the actual website
76
- @st.cache_data(ttl=1800) # Cache data for 30 minutes
77
- def extract_data_from_website(url="https://mopsov.twse.com.tw/mops/web/t05sr01_1", retries=3):
78
- # Rotating User-Agents to avoid detection
79
- user_agents = [
80
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
81
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15',
82
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0',
83
- 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36'
84
- ]
 
 
 
85
 
86
- # More comprehensive headers
87
  headers = {
88
- 'User-Agent': random.choice(user_agents),
89
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
90
- 'Accept-Language': 'zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7',
91
- 'Accept-Encoding': 'gzip, deflate, br',
92
- 'Connection': 'keep-alive',
93
- 'Upgrade-Insecure-Requests': '1',
94
- 'Sec-Fetch-Dest': 'document',
95
- 'Sec-Fetch-Mode': 'navigate',
96
- 'Sec-Fetch-Site': 'none',
97
- 'Sec-Fetch-User': '?1',
98
- 'Cache-Control': 'max-age=0'
99
  }
100
 
101
- # Get current date in Taiwan format (ROC calendar)
102
- now = datetime.now()
103
- roc_year = now.year - 1911
104
- current_date = f"{roc_year}/{now.month:02d}/{now.day:02d}"
105
-
106
- for attempt in range(retries):
107
  try:
108
- with st.spinner(f'正在從網站擷取資料... (嘗試 {attempt+1}/{retries})'):
109
- # Create a session to maintain cookies
110
- session = requests.Session()
111
-
112
- # Initial visit to homepage to get cookies
113
- session.get("https://mopsov.twse.com.tw/mops/web/index", headers=headers, timeout=15)
114
-
115
- # Small delay to avoid triggering anti-scraping measures
116
- time.sleep(random.uniform(1, 3))
117
-
118
- # Visit the announcements page to get the form structure
119
- response = session.get(url, headers=headers, timeout=15)
120
-
121
- # Another small delay
122
- time.sleep(random.uniform(1, 2))
123
-
124
- # Create form data for POST request to get the announcements
125
- # Enhanced form data with more parameters
126
- form_data = {
127
- 'step': '1',
128
- 'firstin': '1',
129
- 'off': '1',
130
- 'keyword4': '',
131
- 'code1': '',
132
- 'TYPEK2': '',
133
- 'checkbtn': '',
134
- 'queryName': 'co_id',
135
- 'inpuType': 'co_id',
136
- 'TYPEK': 'all',
137
- 'co_id': '',
138
- 'year': str(roc_year), # Current ROC year
139
- 'month': str(now.month), # Current month
140
- 'day': str(now.day), # Current day
141
- 'b_date': '',
142
- 'e_date': '',
143
- 'skey': '',
144
- 'date1': '',
145
- 'date2': '',
146
- }
147
-
148
- # Make POST request
149
- post_headers = headers.copy()
150
- post_headers['Content-Type'] = 'application/x-www-form-urlencoded'
151
- post_headers['Origin'] = 'https://mopsov.twse.com.tw'
152
- post_headers['Referer'] = url
153
-
154
- post_response = session.post(
155
- url,
156
- data=form_data,
157
- headers=post_headers,
158
- timeout=20
159
- )
160
-
161
- # Check if the response seems valid
162
- if "hasBorder" in post_response.text and post_response.status_code == 200:
163
- # Parse the HTML content
164
- df = extract_data_from_html(post_response.text)
165
 
166
- if not df.empty:
167
- st.success(f'成功擷取 {len(df)} 筆公告資料!')
168
- return df
 
169
  else:
170
- st.info(f'網站回應成功,但找不到公告資料。可能是當日({current_date})尚無公告。')
171
- # Attempt to look for other messages in the response
172
- soup = BeautifulSoup(post_response.text, 'html.parser')
173
- messages = soup.find_all('td', {'class': 'compName'})
174
- if messages:
175
- st.info(f"網站訊息: {messages[0].text.strip()}")
176
- continue
177
- else:
178
- st.warning(f'網站返回狀態碼: {post_response.status_code}。嘗試重新連接...')
179
- continue
180
 
181
- except requests.exceptions.RequestException as e:
182
- st.warning(f'請求錯誤 (嘗試 {attempt+1}/{retries}): {str(e)}')
183
- time.sleep(2) # Wait before retrying
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  except Exception as e:
185
- st.warning(f'處理錯誤 (嘗試 {attempt+1}/{retries}): {str(e)}')
186
- time.sleep(2) # Wait before retrying
187
-
188
- st.warning(f'嘗試 {retries} 次後仍無法從網站擷取資料,切換到範例資料')
189
- return None
190
-
191
- # Example provided in the original code
192
- default_html_content = """
193
- <table class="hasBorder"><thead><tr class="tblHead_2"><th width="10%" nowrap="">公司代號</th><th width="10%" nowrap="">公司簡稱</th><th nowrap="">發言日期</th><th width="10%" nowrap="">發言時間</th><th>主旨</th></tr></thead><tbody id="tab2"><tr class="even_2" onmouseover="this.className='mouseOn_2';" onmouseout="this.className='even_2';"><td>7724</td><td>諾亞克</td><td>114/04/01</td><td>00:06:30</td><td class="table02"><button style="width:300px;height:28px;text-align:left;background-color:transparent;border:0;cursor:pointer;" onclick="document.fm_t05sr01_1.step.value='1';document.fm_t05sr01_1.SEQ_NO.value='1';document.fm_t05sr01_1.SPOKE_TIME.value='630';document.fm_t05sr01_1.SPOKE_DATE.value='20250401';document.fm_t05sr01_1.COMPANY_NAME.value='諾亞克';document.fm_t05sr01_1.COMPANY_ID.value='7724';document.fm_t05sr01_1.skey.value='7724202504011';document.fm_t05sr01_1.hhc_co_name.value='諾亞克';openWindow(document.fm_t05sr01_1 ,'');" title="公告本公司董事會決議不分配113年度董事及員工酬勞">公告本公司董事會決議不分配113年度董事......</button></td></tr><tr class="odd_2" onmouseover="this.className='mouseOn_2';" onmouseout="this.className='odd_2';"><td>4117</td><td>普生</td><td>114/04/01</td><td>00:04:31</td><td class="table02"><button style="width:300px;height:28px;text-align:left;background-color:transparent;border:0;cursor:pointer;" onclick="document.fm_t05sr01_1.step.value='1';document.fm_t05sr01_1.SEQ_NO.value='7';document.fm_t05sr01_1.SPOKE_TIME.value='431';document.fm_t05sr01_1.SPOKE_DATE.value='20250401';document.fm_t05sr01_1.COMPANY_NAME.value='普生';document.fm_t05sr01_1.COMPANY_ID.value='4117';document.fm_t05sr01_1.skey.value='4117202503317';document.fm_t05sr01_1.hhc_co_name.value='普生';openWindow(document.fm_t05sr01_1 ,'');" title="公告本公司董事會決議不發放股利">公告本公司董事會決議不發放股利</button></td></tr></tbody></table>
194
- """
195
-
196
- # Add date range picker to sidebar
197
- st.sidebar.header("資料來源選項")
198
- data_source = st.sidebar.radio(
199
- "選擇資料來源",
200
- ["從網站擷取資料", "使用範例資料", "貼上HTML代碼"]
201
- )
202
 
203
- # Date range selector (only visible when fetching from website)
204
- if data_source == "從網站擷取資料":
205
- st.sidebar.subheader("日期選擇")
206
-
207
- # Calculate ROC year (Taiwan calendar)
208
- current_year = datetime.now().year
209
- roc_year = current_year - 1911
210
-
211
- # Date inputs
212
- col1, col2 = st.sidebar.columns(2)
213
- with col1:
214
- year = st.number_input("年度(民國)", min_value=100, max_value=roc_year, value=roc_year)
215
- with col2:
216
- month = st.number_input("月份", min_value=1, max_value=12, value=datetime.now().month)
217
-
218
- custom_date = st.sidebar.checkbox("指定日期範圍")
219
- if custom_date:
220
- start_date = st.sidebar.date_input("起始日期")
221
- end_date = st.sidebar.date_input("結束日期")
222
-
223
- # Initialize data frame
224
- df = None
225
-
226
- # Add progress
227
- if data_source == "從網站擷取資料":
228
- with st.expander("網路連線診斷", expanded=False):
229
- st.write("檢查台灣證券交易所網站連線...")
230
- try:
231
- check_response = requests.get("https://mopsov.twse.com.tw/", timeout=5)
232
- st.write(f"網站狀態: {'可連線 ✅' if check_response.status_code == 200 else '無法連線 ❌'}")
233
- st.write(f"HTTP 狀態碼: {check_response.status_code}")
234
- except Exception as e:
235
- st.write(f"網站連線檢查失敗: {e}")
236
-
237
- fetch_data = st.button("開始擷取資料", type="primary")
238
- if fetch_data:
239
- # This will be enhanced to use the date parameters when implemented
240
- df = extract_data_from_website()
241
- if df is None:
242
- st.sidebar.warning("從網站擷取資料失敗,切換到範例資料")
243
- df = extract_data_from_html(default_html_content)
244
-
245
- elif data_source == "使用範例資料":
246
- df = extract_data_from_html(default_html_content)
247
-
248
- else: # "貼上HTML代碼"
249
- html_input = st.sidebar.text_area("貼上HTML代碼", value=default_html_content, height=300)
250
- if st.sidebar.button("解析HTML"):
251
- df = extract_data_from_html(html_input)
252
- st.sidebar.success("HTML解析完成!")
253
 
254
- # Display and filter data
255
- if df is not None and not df.empty:
256
- st.subheader("台灣證券交易所公告資料")
257
-
258
- # Add search filters
259
- col1, col2, col3 = st.columns(3)
260
  with col1:
261
- search_code = st.text_input("依公司代號篩選")
262
- with col2:
263
- search_name = st.text_input("依公司名稱篩選")
264
- with col3:
265
- search_subject = st.text_input("依主旨關鍵字篩選")
266
 
267
- # Apply filters if provided
268
- filtered_df = df.copy()
269
- if search_code:
270
- filtered_df = filtered_df[filtered_df['公司代號'].str.contains(search_code)]
271
- if search_name:
272
- filtered_df = filtered_df[filtered_df['公司簡稱'].str.contains(search_name)]
273
- if search_subject:
274
- filtered_df = filtered_df[filtered_df['主旨'].str.contains(search_subject)]
275
 
276
- # Display the data
277
- st.dataframe(filtered_df, use_container_width=True)
278
 
279
- # Download as CSV
280
- csv = filtered_df.to_csv(index=False).encode('utf-8-sig')
281
- st.download_button(
282
- label="下載為CSV",
283
- data=csv,
284
- file_name="twse_announcements.csv",
285
- mime="text/csv",
286
  )
287
 
288
- # Display statistics
289
- st.subheader("資料統計")
290
- col1, col2, col3 = st.columns(3)
291
- with col1:
292
- st.metric("公告總數", len(filtered_df))
293
- with col2:
294
- company_count = filtered_df['公司代號'].nunique()
295
- st.metric("公司數量", company_count)
296
- with col3:
297
- date_counts = filtered_df['發言日期'].value_counts()
298
- if not date_counts.empty:
299
- latest_date = date_counts.index[0]
300
- latest_count = date_counts.iloc[0]
301
- st.metric(f"最新日期 ({latest_date})", latest_count)
302
-
303
- # Show announcement details on selection
304
- if not filtered_df.empty:
305
- st.subheader("選擇公告以查看詳情")
306
- selected_indices = st.multiselect(
307
- "選擇公告",
308
- options=list(range(len(filtered_df))),
309
- format_func=lambda i: f"{filtered_df.iloc[i]['公司簡稱']} ({filtered_df.iloc[i]['公司代號']}) - {filtered_df.iloc[i]['主旨'][:20]}..."
310
- )
311
-
312
- if selected_indices:
313
- for idx in selected_indices:
314
- with st.expander(f"{filtered_df.iloc[idx]['公司簡稱']} ({filtered_df.iloc[idx]['公司代號']}) - {filtered_df.iloc[idx]['發言日期']}"):
315
- st.write(f"**公司代號:** {filtered_df.iloc[idx]['公司代號']}")
316
- st.write(f"**公司簡稱:** {filtered_df.iloc[idx]['公司簡稱']}")
317
- st.write(f"**發言日期:** {filtered_df.iloc[idx]['發言日期']}")
318
- st.write(f"**發言時間:** {filtered_df.iloc[idx]['發言時間']}")
319
- st.write(f"**主旨內容:** {filtered_df.iloc[idx]['主旨']}")
320
- else:
321
- st.warning("沒有可顯示的資料")
322
-
323
- # Footer
324
- st.markdown("---")
325
- st.markdown("台灣證券交易所公告擷取工具 | 資料來源: [台灣證券交易所](https://mopsov.twse.com.tw/mops/web/index)")
326
-
327
- # Add FAQ section at the bottom
328
- with st.expander("常見問題", expanded=False):
329
- st.subheader("常見問題")
330
-
331
- st.markdown("""
332
- **Q: 為什麼無法從網站擷取資料?**
333
 
334
- A: 可能原因包括:
335
- - 台灣證券交易所網站暫時無法連接
336
- - 當日尚無公告資料
337
- - 網站結構可能有所變更
338
- - 網路連線問題
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
 
340
- **Q: 資料顯示的日期格式是什麼?**
 
341
 
342
- A: 發言日期採用中華民國紀年(民國紀年),例如「114/04/01」表示西元2025年4月1日。
 
 
343
 
344
- **Q: 為什麼有些公告的主旨只顯示部分內容?**
 
345
 
346
- A: 當主旨內容過長時,網站顯示會自動截斷。點選公告查看詳情可能會顯示完整主旨。
 
 
347
 
348
- **Q: 如何取得更多歷史公告?**
 
 
 
 
349
 
350
- A: 本工具目前僅擷取當前頁面資料。若需查詢歷史資料,建議直接前往[台灣證券交易所](https://mopsov.twse.com.tw/mops/web/index)官方網站搜尋。
351
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
- # Add version info
354
- st.sidebar.markdown("---")
355
- st.sidebar.caption("版本: 1.1.1")
356
- st.sidebar.caption("最後更新: 2025-04-01")
 
3
  from bs4 import BeautifulSoup
4
  import pandas as pd
5
  import time
6
+ import base64
7
+ from io import BytesIO
 
8
 
9
+ # 設置頁面配置
10
  st.set_page_config(
11
+ page_title="台灣證券交易所重大訊息爬蟲",
12
  page_icon="📊",
13
  layout="wide"
14
  )
15
 
16
+ # 添加標題和說明
17
+ st.title("台灣證券交易所重大訊息爬蟲")
18
+ st.markdown("這個應用程式會從台灣證券交易所網站爬取上市公司的重大訊息公告。")
19
 
20
+ # 添加側邊欄控制
21
+ with st.sidebar:
22
+ st.header("設定")
23
+ auto_refresh = st.checkbox("啟用自動刷新", value=False)
24
+ refresh_interval = st.slider("刷新間隔 (分鐘)", 1, 60, 15) if auto_refresh else 0
25
+ max_results = st.slider("顯示結果數量", 5, 50, 20)
26
+
27
+ st.header("篩選器")
28
+ filter_enabled = st.checkbox("啟用關鍵字篩選", value=False)
29
+ filter_keyword = st.text_input("關鍵字") if filter_enabled else ""
30
+
31
+ st.header("關於")
32
+ st.info("此應用程式從台灣證券交易所獲取重大訊息公告,僅供參考用途。")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ # 下載CSV功能
35
+ def get_csv_download_link(df, filename="data.csv"):
36
+ """生成CSV下載鏈接"""
37
+ csv = df.to_csv(index=False, encoding='utf-8-sig')
38
+ b64 = base64.b64encode(csv.encode()).decode()
39
+ href = f'<a href="data:file/csv;base64,{b64}" download="{filename}">下載 CSV 檔案</a>'
40
+ return href
41
+
42
+ # 爬蟲功能
43
+ def fetch_mops_announcements():
44
+ """從台灣證券交易所爬取重大訊息"""
45
+ # 設定請求URL
46
+ url = "https://mopsov.twse.com.tw/mops/web/t05sr01_1"
47
 
48
+ # 設定請求頭,模擬瀏覽器行為
49
  headers = {
50
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
51
+ "Accept-Language": "zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7",
52
+ "Referer": "https://mopsov.twse.com.tw/mops/web/index"
 
 
 
 
 
 
 
 
53
  }
54
 
55
+ with st.spinner('正在獲取資料...'):
 
 
 
 
 
56
  try:
57
+ # 發送GET請求獲取頁面
58
+ response = requests.get(url, headers=headers, timeout=10)
59
+ response.encoding = 'utf-8' # 確保正確編碼
60
+
61
+ if response.status_code != 200:
62
+ st.error(f"請求失敗,狀態碼:{response.status_code}")
63
+ return parse_example_data()
64
+
65
+ # 解析HTML內容
66
+ soup = BeautifulSoup(response.text, 'html.parser')
67
+
68
+ # 查找表格內容
69
+ table = soup.find('table', class_='hasBorder')
70
+ if not table:
71
+ st.warning("找不到目標表格,使用範例數據")
72
+ return parse_example_data()
73
+
74
+ # 查找表格體
75
+ tbody = table.find('tbody', id='tab2')
76
+ if not tbody:
77
+ st.warning("找不到tbody#tab2,嘗試直接查找tr元素")
78
+ rows = table.find_all('tr')[1:] # 跳過表頭行
79
+ else:
80
+ rows = tbody.find_all('tr')
81
+
82
+ # 驗證是否找到行
83
+ if not rows:
84
+ st.warning("找不到任何資料行,使用範例數據")
85
+ return parse_example_data()
86
+
87
+ # 準備數據列表
88
+ data = []
89
+
90
+ # 解析每行數據
91
+ for row in rows:
92
+ cols = row.find_all('td')
93
+ if len(cols) >= 5:
94
+ company_code = cols[0].text.strip()
95
+ company_name = cols[1].text.strip()
96
+ announce_date = cols[2].text.strip()
97
+ announce_time = cols[3].text.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
+ # 處理主旨 - 可能在按鈕的title屬性中
100
+ subject_btn = cols[4].find('button')
101
+ if subject_btn and 'title' in subject_btn.attrs:
102
+ subject = subject_btn['title'].strip()
103
  else:
104
+ # 如果沒有按鈕或title屬性,直接獲取文本
105
+ subject = cols[4].text.strip()
 
 
 
 
 
 
 
 
106
 
107
+ # 添加到數據列表
108
+ data.append({
109
+ '公司代號': company_code,
110
+ '公司簡稱': company_name,
111
+ '發言日期': announce_date,
112
+ '發言時間': announce_time,
113
+ '主旨': subject
114
+ })
115
+
116
+ # 如果沒有收集到數據,使用範例數據
117
+ if not data:
118
+ st.warning("未收集到任何數據,使用範例數據")
119
+ return parse_example_data()
120
+
121
+ # 創建DataFrame
122
+ df = pd.DataFrame(data)
123
+ st.success(f"成功獲取 {len(df)} 筆資料")
124
+ return df
125
+
126
  except Exception as e:
127
+ st.error(f"爬取過程發生錯誤: {str(e)}")
128
+ return parse_example_data()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
+ def parse_example_data():
131
+ """使用範例數據創建DataFrame"""
132
+ # 使用範例數據
133
+ data = [
134
+ {'公司代號': '1419', '公司簡稱': '新紡', '發言日期': '114/04/01', '發言時間': '12:36:37', '主旨': '公告本公司財務主管變動'},
135
+ {'公司代號': '1419', '公司簡稱': '新紡', '發言日期': '114/04/01', '發言時間': '12:36:00', '主旨': '公告本公司發言人及代理發言人異動'},
136
+ {'公司代號': '6277', '公司簡稱': '宏正', '發言日期': '114/04/01', '發言時間': '12:03:45', '主旨': '澄清媒體報導'},
137
+ {'公司代號': '2215', '公司簡稱': '匯豐汽車', '發言日期': '114/04/01', '發言時間': '12:03:03', '主旨': '公告本公司財務暨會計主管異動'},
138
+ {'公司代號': '2215', '公司簡稱': '匯豐汽車', '發言日期': '114/04/01', '發言時間': '12:00:20', '主旨': '公告本公司新任董事長'},
139
+ {'公司代號': '6414', '公司簡稱': '樺漢', '發言日期': '114/04/01', '發言時間': '11:42:52', '主旨': '澄清工商時報有關本公司之報導'},
140
+ {'公司代號': '8916', '公司簡稱': '光隆', '發言日期': '114/04/01', '發言時間': '10:56:03', '主旨': '更正公告〔113年度股利分派情形申報作業〕(含普通股及特別股)可分配盈餘及分配後期末未分配盈餘誤植(原114/3/11董事會決議無異動)'},
141
+ {'公司代號': '6597', '公司簡稱': '立誠', '發言日期': '114/04/01', '發言時間': '10:55:35', '主旨': '澄清媒體報導'}
142
+ ]
143
+ return pd.DataFrame(data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
+ # 主應用邏輯
146
+ def main():
147
+ # 添加刷新按鈕
148
+ col1, col2, col3 = st.columns([1, 1, 2])
 
 
149
  with col1:
150
+ refresh_button = st.button("刷新資料")
 
 
 
 
151
 
152
+ # 添加上次更新時間顯示
153
+ if 'last_update' not in st.session_state:
154
+ st.session_state.last_update = None
 
 
 
 
 
155
 
156
+ if 'data' not in st.session_state:
157
+ st.session_state.data = None
158
 
159
+ # 檢查是否需要刷新數據
160
+ current_time = time.time()
161
+ auto_refresh_needed = (
162
+ auto_refresh and
163
+ st.session_state.last_update is not None and
164
+ current_time - st.session_state.last_update > refresh_interval * 60
 
165
  )
166
 
167
+ if refresh_button or auto_refresh_needed or st.session_state.data is None:
168
+ # 獲取資料
169
+ df = fetch_mops_announcements()
170
+ st.session_state.data = df
171
+ st.session_state.last_update = current_time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
+ # 顯示最後更新時間
174
+ with col2:
175
+ if st.session_state.last_update is not None:
176
+ last_update_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(st.session_state.last_update))
177
+ st.info(f"最後更新: {last_update_str}")
178
+
179
+ # 獲取當前資料
180
+ df = st.session_state.data
181
+
182
+ # 套用篩選
183
+ if filter_enabled and filter_keyword:
184
+ filtered_df = df[
185
+ df['公司代號'].str.contains(filter_keyword, case=False, na=False) |
186
+ df['公司簡稱'].str.contains(filter_keyword, case=False, na=False) |
187
+ df['主旨'].str.contains(filter_keyword, case=False, na=False)
188
+ ]
189
+ if len(filtered_df) > 0:
190
+ st.success(f"找到 {len(filtered_df)} 筆符合 '{filter_keyword}' 的資料")
191
+ df = filtered_df
192
+ else:
193
+ st.warning(f"沒有找到包含 '{filter_keyword}' 的資料")
194
 
195
+ # 限制顯示數量
196
+ df_display = df.head(max_results)
197
 
198
+ # 顯示數據表格
199
+ st.subheader("重大訊息公告")
200
+ st.dataframe(df_display, use_container_width=True)
201
 
202
+ # 顯示下載連結
203
+ st.markdown(get_csv_download_link(df, "重大訊息公告.csv"), unsafe_allow_html=True)
204
 
205
+ # 顯示統計信息
206
+ st.subheader("統計資訊")
207
+ col1, col2 = st.columns(2)
208
 
209
+ with col1:
210
+ # 日期統計
211
+ date_counts = df['發言日期'].value_counts().reset_index()
212
+ date_counts.columns = ['日期', '公告數量']
213
+ st.bar_chart(date_counts.set_index('日期'))
214
 
215
+ with col2:
216
+ # 公司統計
217
+ company_counts = df['公司簡稱'].value_counts().head(10).reset_index()
218
+ company_counts.columns = ['公司', '公告數量']
219
+ st.bar_chart(company_counts.set_index('公司'))
220
+
221
+ # 主旨關鍵詞統計
222
+ st.subheader("主旨關鍵詞分析")
223
+ keywords = ['董事長', '財務', '主管', '澄清', '媒體', '股利', '董事會', '異動', '收購']
224
+ keyword_counts = []
225
+
226
+ for keyword in keywords:
227
+ count = df['主旨'].str.contains(keyword).sum()
228
+ keyword_counts.append({'關鍵詞': keyword, '出現次數': count})
229
+
230
+ keyword_df = pd.DataFrame(keyword_counts)
231
+ st.bar_chart(keyword_df.set_index('關鍵詞'))
232
 
233
+ # 執行主應用
234
+ if __name__ == "__main__":
235
+ main()