Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -3,354 +3,233 @@ import requests
|
|
3 |
from bs4 import BeautifulSoup
|
4 |
import pandas as pd
|
5 |
import time
|
6 |
-
import
|
7 |
-
from
|
8 |
-
import io
|
9 |
|
10 |
-
#
|
11 |
st.set_page_config(
|
12 |
-
page_title="
|
13 |
page_icon="📊",
|
14 |
layout="wide"
|
15 |
)
|
16 |
|
17 |
-
#
|
18 |
-
st.title("
|
19 |
-
st.markdown("
|
20 |
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
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 |
-
#
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
|
|
|
|
|
|
85 |
|
86 |
-
#
|
87 |
headers = {
|
88 |
-
|
89 |
-
|
90 |
-
|
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 |
-
|
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 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
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 |
-
|
167 |
-
|
168 |
-
|
|
|
169 |
else:
|
170 |
-
|
171 |
-
|
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 |
-
|
182 |
-
|
183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
184 |
except Exception as e:
|
185 |
-
st.
|
186 |
-
|
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 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
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 |
-
#
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
# Add search filters
|
259 |
-
col1, col2, col3 = st.columns(3)
|
260 |
with col1:
|
261 |
-
|
262 |
-
with col2:
|
263 |
-
search_name = st.text_input("依公司名稱篩選")
|
264 |
-
with col3:
|
265 |
-
search_subject = st.text_input("依主旨關鍵字篩選")
|
266 |
|
267 |
-
#
|
268 |
-
|
269 |
-
|
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 |
-
|
277 |
-
|
278 |
|
279 |
-
#
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
mime="text/csv",
|
286 |
)
|
287 |
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
st.
|
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 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
339 |
|
340 |
-
|
|
|
341 |
|
342 |
-
|
|
|
|
|
343 |
|
344 |
-
|
|
|
345 |
|
346 |
-
|
|
|
|
|
347 |
|
348 |
-
|
|
|
|
|
|
|
|
|
349 |
|
350 |
-
|
351 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
352 |
|
353 |
-
#
|
354 |
-
|
355 |
-
|
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()
|
|