File size: 9,813 Bytes
26c5941
 
 
 
1705eb7
1529727
 
26c5941
1529727
26c5941
1529727
26c5941
 
 
 
1529727
 
 
26c5941
1529727
 
 
 
 
 
 
 
 
 
 
 
 
26c5941
1529727
 
 
 
 
 
 
 
 
 
 
 
 
1705eb7
1529727
26c5941
1529727
 
 
26c5941
 
1529727
1705eb7
1529727
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1705eb7
1529727
 
 
 
1705eb7
1529727
 
1705eb7
1529727
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1705eb7
1529727
 
26c5941
1529727
 
 
 
 
 
 
 
 
 
 
 
 
 
26c5941
1529727
 
 
 
26c5941
1529727
26c5941
1529727
 
 
26c5941
1529727
 
26c5941
1529727
 
 
 
 
 
40653f6
26c5941
1529727
 
 
 
 
1705eb7
1529727
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1705eb7
1529727
 
1705eb7
1529727
 
 
1705eb7
1529727
 
1705eb7
1529727
 
 
1705eb7
1529727
 
 
 
 
1705eb7
1529727
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1705eb7
1529727
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import streamlit as st
import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
import base64
from io import BytesIO

# 設置頁面配置
st.set_page_config(
    page_title="台灣證券交易所重大訊息爬蟲",
    page_icon="📊",
    layout="wide"
)

# 添加標題和說明
st.title("台灣證券交易所重大訊息爬蟲")
st.markdown("這個應用程式會從台灣證券交易所網站爬取上市公司的重大訊息公告。")

# 添加側邊欄控制
with st.sidebar:
    st.header("設定")
    auto_refresh = st.checkbox("啟用自動刷新", value=False)
    refresh_interval = st.slider("刷新間隔 (分鐘)", 1, 60, 15) if auto_refresh else 0
    max_results = st.slider("顯示結果數量", 5, 50, 20)
    
    st.header("篩選器")
    filter_enabled = st.checkbox("啟用關鍵字篩選", value=False)
    filter_keyword = st.text_input("關鍵字") if filter_enabled else ""
    
    st.header("關於")
    st.info("此應用程式從台灣證券交易所獲取重大訊息公告,僅供參考用途。")

# 下載CSV功能
def get_csv_download_link(df, filename="data.csv"):
    """生成CSV下載鏈接"""
    csv = df.to_csv(index=False, encoding='utf-8-sig')
    b64 = base64.b64encode(csv.encode()).decode()
    href = f'<a href="data:file/csv;base64,{b64}" download="{filename}">下載 CSV 檔案</a>'
    return href

# 爬蟲功能
def fetch_mops_announcements():
    """從台灣證券交易所爬取重大訊息"""
    # 設定請求URL
    url = "https://mopsov.twse.com.tw/mops/web/t05sr01_1"
    
    # 設定請求頭,模擬瀏覽器行為
    headers = {
        "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",
        "Accept-Language": "zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7",
        "Referer": "https://mopsov.twse.com.tw/mops/web/index"
    }
    
    with st.spinner('正在獲取資料...'):
        try:
            # 發送GET請求獲取頁面
            response = requests.get(url, headers=headers, timeout=10)
            response.encoding = 'utf-8'  # 確保正確編碼
            
            if response.status_code != 200:
                st.error(f"請求失敗,狀態碼:{response.status_code}")
                return parse_example_data()
            
            # 解析HTML內容
            soup = BeautifulSoup(response.text, 'html.parser')
            
            # 查找表格內容
            table = soup.find('table', class_='hasBorder')
            if not table:
                st.warning("找不到目標表格,使用範例數據")
                return parse_example_data()
            
            # 查找表格體
            tbody = table.find('tbody', id='tab2')
            if not tbody:
                st.warning("找不到tbody#tab2,嘗試直接查找tr元素")
                rows = table.find_all('tr')[1:]  # 跳過表頭行
            else:
                rows = tbody.find_all('tr')
            
            # 驗證是否找到行
            if not rows:
                st.warning("找不到任何資料行,使用範例數據")
                return parse_example_data()
            
            # 準備數據列表
            data = []
            
            # 解析每行數據
            for row in rows:
                cols = row.find_all('td')
                if len(cols) >= 5:
                    company_code = cols[0].text.strip()
                    company_name = cols[1].text.strip()
                    announce_date = cols[2].text.strip()
                    announce_time = cols[3].text.strip()
                    
                    # 處理主旨 - 可能在按鈕的title屬性中
                    subject_btn = cols[4].find('button')
                    if subject_btn and 'title' in subject_btn.attrs:
                        subject = subject_btn['title'].strip()
                    else:
                        # 如果沒有按鈕或title屬性,直接獲取文本
                        subject = cols[4].text.strip()
                    
                    # 添加到數據列表
                    data.append({
                        '公司代號': company_code,
                        '公司簡稱': company_name,
                        '發言日期': announce_date,
                        '發言時間': announce_time,
                        '主旨': subject
                    })
            
            # 如果沒有收集到數據,使用範例數據
            if not data:
                st.warning("未收集到任何數據,使用範例數據")
                return parse_example_data()
            
            # 創建DataFrame
            df = pd.DataFrame(data)
            st.success(f"成功獲取 {len(df)} 筆資料")
            return df
            
        except Exception as e:
            st.error(f"爬取過程發生錯誤: {str(e)}")
            return parse_example_data()

def parse_example_data():
    """使用範例數據創建DataFrame"""
    # 使用範例數據
    data = [
        {'公司代號': '1419', '公司簡稱': '新紡', '發言日期': '114/04/01', '發言時間': '12:36:37', '主旨': '公告本公司財務主管變動'},
        {'公司代號': '1419', '公司簡稱': '新紡', '發言日期': '114/04/01', '發言時間': '12:36:00', '主旨': '公告本公司發言人及代理發言人異動'},
        {'公司代號': '6277', '公司簡稱': '宏正', '發言日期': '114/04/01', '發言時間': '12:03:45', '主旨': '澄清媒體報導'},
        {'公司代號': '2215', '公司簡稱': '匯豐汽車', '發言日期': '114/04/01', '發言時間': '12:03:03', '主旨': '公告本公司財務暨會計主管異動'},
        {'公司代號': '2215', '公司簡稱': '匯豐汽車', '發言日期': '114/04/01', '發言時間': '12:00:20', '主旨': '公告本公司新任董事長'},
        {'公司代號': '6414', '公司簡稱': '樺漢', '發言日期': '114/04/01', '發言時間': '11:42:52', '主旨': '澄清工商時報有關本公司之報導'},
        {'公司代號': '8916', '公司簡稱': '光隆', '發言日期': '114/04/01', '發言時間': '10:56:03', '主旨': '更正公告〔113年度股利分派情形申報作業〕(含普通股及特別股)可分配盈餘及分配後期末未分配盈餘誤植(原114/3/11董事會決議無異動)'},
        {'公司代號': '6597', '公司簡稱': '立誠', '發言日期': '114/04/01', '發言時間': '10:55:35', '主旨': '澄清媒體報導'}
    ]
    return pd.DataFrame(data)

# 主應用邏輯
def main():
    # 添加刷新按鈕
    col1, col2, col3 = st.columns([1, 1, 2])
    with col1:
        refresh_button = st.button("刷新資料")
    
    # 添加上次更新時間顯示
    if 'last_update' not in st.session_state:
        st.session_state.last_update = None
    
    if 'data' not in st.session_state:
        st.session_state.data = None
    
    # 檢查是否需要刷新數據
    current_time = time.time()
    auto_refresh_needed = (
        auto_refresh and 
        st.session_state.last_update is not None and 
        current_time - st.session_state.last_update > refresh_interval * 60
    )
    
    if refresh_button or auto_refresh_needed or st.session_state.data is None:
        # 獲取資料
        df = fetch_mops_announcements()
        st.session_state.data = df
        st.session_state.last_update = current_time
    
    # 顯示最後更新時間
    with col2:
        if st.session_state.last_update is not None:
            last_update_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(st.session_state.last_update))
            st.info(f"最後更新: {last_update_str}")
    
    # 獲取當前資料
    df = st.session_state.data
    
    # 套用篩選
    if filter_enabled and filter_keyword:
        filtered_df = df[
            df['公司代號'].str.contains(filter_keyword, case=False, na=False) |
            df['公司簡稱'].str.contains(filter_keyword, case=False, na=False) |
            df['主旨'].str.contains(filter_keyword, case=False, na=False)
        ]
        if len(filtered_df) > 0:
            st.success(f"找到 {len(filtered_df)} 筆符合 '{filter_keyword}' 的資料")
            df = filtered_df
        else:
            st.warning(f"沒有找到包含 '{filter_keyword}' 的資料")
    
    # 限制顯示數量
    df_display = df.head(max_results)
    
    # 顯示數據表格
    st.subheader("重大訊息公告")
    st.dataframe(df_display, use_container_width=True)
    
    # 顯示下載連結
    st.markdown(get_csv_download_link(df, "重大訊息公告.csv"), unsafe_allow_html=True)
    
    # 顯示統計信息
    st.subheader("統計資訊")
    col1, col2 = st.columns(2)
    
    with col1:
        # 日期統計
        date_counts = df['發言日期'].value_counts().reset_index()
        date_counts.columns = ['日期', '公告數量']
        st.bar_chart(date_counts.set_index('日期'))
    
    with col2:
        # 公司統計
        company_counts = df['公司簡稱'].value_counts().head(10).reset_index()
        company_counts.columns = ['公司', '公告數量']
        st.bar_chart(company_counts.set_index('公司'))
    
    # 主旨關鍵詞統計
    st.subheader("主旨關鍵詞分析")
    keywords = ['董事長', '財務', '主管', '澄清', '媒體', '股利', '董事會', '異動', '收購']
    keyword_counts = []
    
    for keyword in keywords:
        count = df['主旨'].str.contains(keyword).sum()
        keyword_counts.append({'關鍵詞': keyword, '出現次數': count})
    
    keyword_df = pd.DataFrame(keyword_counts)
    st.bar_chart(keyword_df.set_index('關鍵詞'))

# 執行主應用
if __name__ == "__main__":
    main()