mikao007 commited on
Commit
b73817d
·
verified ·
1 Parent(s): 166ffb5

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +349 -0
app.py ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """「ESG_deployed on Hugging Face_Streamlit
3
+
4
+ Automatically generated by Colab.
5
+
6
+ Original file is located at
7
+ https://colab.research.google.com/drive/1-psBpb_NYklJjrCqI8AEIg1bSuMUwXJL
8
+
9
+ #stremlit 框架
10
+ """
11
+
12
+ import requests
13
+ from bs4 import BeautifulSoup
14
+ import pandas as pd
15
+ from typing import List, Tuple
16
+ import time
17
+ from pandas.io.formats.style import Styler
18
+ import streamlit as st
19
+ import os
20
+ from datetime import datetime
21
+ import io
22
+
23
+ BASE_URL = "https://cgc.twse.com.tw/front/chPage"
24
+
25
+ def fetch_page(offset: int, max_per: int = 30, fmt: str = "") -> str:
26
+ params = {"offset": offset, "max": max_per, "format": fmt}
27
+ resp = requests.get(BASE_URL, params=params, timeout=10)
28
+ resp.raise_for_status()
29
+ return resp.text
30
+
31
+ def parse_companies(html: str) -> List[Tuple[str, str, str]]:
32
+ soup = BeautifulSoup(html, "html.parser")
33
+ results = []
34
+ for tr in soup.select("table tr"):
35
+ tds = tr.find_all("td")
36
+ if len(tds) >= 3:
37
+ code = tds[1].get_text(strip=True)
38
+ name = tds[2].get_text(strip=True)
39
+ link_tag = tds[2].find("a")
40
+ url = link_tag["href"].strip() if link_tag and "href" in link_tag.attrs else ""
41
+ if code.isdigit():
42
+ results.append((code, name, url))
43
+ return results
44
+
45
+ def collect_all(start_offset: int = 0, max_per: int = 30, max_pages: int = 100, progress_bar=None, status_text=None) -> pd.DataFrame:
46
+ all_rows = []
47
+ offset = start_offset
48
+
49
+ for i in range(max_pages):
50
+ try:
51
+ # 更新進度條和狀態
52
+ if progress_bar:
53
+ progress_bar.progress((i + 1) / max_pages)
54
+ if status_text:
55
+ status_text.text(f"正在爬取第 {i + 1} 頁,偏移量: {offset}")
56
+
57
+ html = fetch_page(offset, max_per)
58
+ rows = parse_companies(html)
59
+ if not rows:
60
+ if status_text:
61
+ status_text.text(f"已完成爬取,共處理 {i + 1} 頁")
62
+ break
63
+ all_rows.extend(rows)
64
+ offset += max_per
65
+ time.sleep(0.5)
66
+ except Exception as e:
67
+ if status_text:
68
+ status_text.text(f"錯誤發生於偏移量 {offset}: {e}")
69
+ break
70
+
71
+ # 加入編號欄位
72
+ df = pd.DataFrame(all_rows, columns=["公司代碼", "公司名稱", "公司網址"])
73
+ df.insert(0, "編號", range(1, len(df) + 1))
74
+ return df
75
+
76
+ def style_dataframe(df: pd.DataFrame) -> Styler:
77
+ """
78
+ 設定DataFrame的樣式:
79
+ - 編號、公司代碼、公司名稱欄位標題為藍色背景
80
+ - 每個欄位的值交替黃色背景
81
+ """
82
+ def header_style(s):
83
+ """設定標題樣式"""
84
+ styles = []
85
+ for col in s.index:
86
+ if col in ["編號", "公司代碼", "公司名稱"]:
87
+ styles.append('background-color: #4472C4; color: white; font-weight: bold')
88
+ else:
89
+ styles.append('background-color: #D9D9D9; color: black; font-weight: bold')
90
+ return styles
91
+
92
+ def alternating_rows(s):
93
+ """設定交替行顏色"""
94
+ styles = []
95
+ for i, col in enumerate(s.index):
96
+ if col in ["編號", "公司代碼", "公司名稱"]:
97
+ if s.name % 2 == 0: # 偶數行
98
+ styles.append('background-color: #FFF2CC') # 淺黃色
99
+ else: # 奇數行
100
+ styles.append('background-color: #FFFFFF') # 白色
101
+ else:
102
+ styles.append('background-color: #F8F8F8') # 淺灰色
103
+ return styles
104
+
105
+ # 應用樣式
106
+ styled = df.style.apply(alternating_rows, axis=1).apply(header_style, axis=0)
107
+
108
+ # 設定表格整體樣式
109
+ styled = styled.set_table_styles([
110
+ {'selector': 'th', 'props': [('text-align', 'center'), ('padding', '8px')]},
111
+ {'selector': 'td', 'props': [('text-align', 'center'), ('padding', '6px')]},
112
+ {'selector': 'table', 'props': [('border-collapse', 'collapse'), ('margin', 'auto')]},
113
+ {'selector': 'th, td', 'props': [('border', '1px solid #CCCCCC')]}
114
+ ])
115
+
116
+ return styled
117
+
118
+ def save_to_excel(df: pd.DataFrame) -> bytes:
119
+ """儲存為Excel格式並應用樣式,返回bytes"""
120
+ output = io.BytesIO()
121
+
122
+ # 建立樣式化的DataFrame
123
+ with pd.ExcelWriter(output, engine='openpyxl') as writer:
124
+ # 先寫入基本資料
125
+ df.to_excel(writer, sheet_name='公司資料', index=False)
126
+
127
+ # 取得工作表以進行格式設定
128
+ worksheet = writer.sheets['公司資料']
129
+
130
+ # 設定欄寬
131
+ worksheet.column_dimensions['A'].width = 8 # 編號
132
+ worksheet.column_dimensions['B'].width = 12 # 公司代碼
133
+ worksheet.column_dimensions['C'].width = 25 # 公司名稱
134
+ worksheet.column_dimensions['D'].width = 40 # 公司網址
135
+
136
+ # 使用openpyxl進行進階格式設定
137
+ from openpyxl.styles import PatternFill, Font, Alignment, Border, Side
138
+
139
+ # 定義顏色
140
+ blue_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
141
+ yellow_fill = PatternFill(start_color="FFF2CC", end_color="FFF2CC", fill_type="solid")
142
+ white_fill = PatternFill(start_color="FFFFFF", end_color="FFFFFF", fill_type="solid")
143
+ gray_fill = PatternFill(start_color="D9D9D9", end_color="D9D9D9", fill_type="solid")
144
+
145
+ # 定義字體
146
+ header_font = Font(bold=True, color="FFFFFF")
147
+ normal_font = Font(color="000000")
148
+
149
+ # 定義對齊
150
+ center_alignment = Alignment(horizontal="center", vertical="center")
151
+
152
+ # 定義邊框
153
+ thin_border = Border(
154
+ left=Side(style='thin'),
155
+ right=Side(style='thin'),
156
+ top=Side(style='thin'),
157
+ bottom=Side(style='thin')
158
+ )
159
+
160
+ # 設定標題行格式
161
+ for col_num, col_name in enumerate(['編號', '公司代碼', '公司名稱', '公司網址'], 1):
162
+ cell = worksheet.cell(row=1, column=col_num)
163
+ cell.font = header_font
164
+ cell.alignment = center_alignment
165
+ cell.border = thin_border
166
+
167
+ if col_name in ['編號', '公司代碼', '公司名稱']:
168
+ cell.fill = blue_fill
169
+ else:
170
+ cell.fill = gray_fill
171
+
172
+ # 設定資料行格式
173
+ for row_num in range(2, len(df) + 2):
174
+ for col_num in range(1, 5):
175
+ cell = worksheet.cell(row=row_num, column=col_num)
176
+ cell.font = normal_font
177
+ cell.alignment = center_alignment
178
+ cell.border = thin_border
179
+
180
+ # 針對編號、公司代碼、公司名稱欄位設定交替顏色
181
+ if col_num <= 3: # 編號、公司代碼、公司名稱
182
+ if (row_num - 2) % 2 == 0: # 偶數行
183
+ cell.fill = yellow_fill
184
+ else: # 奇數行
185
+ cell.fill = white_fill
186
+
187
+ output.seek(0)
188
+ return output.getvalue()
189
+
190
+ def save_to_csv(df: pd.DataFrame) -> str:
191
+ """儲存為CSV格式,返回CSV字串"""
192
+ return df.to_csv(index=False, encoding="utf-8-sig")
193
+
194
+ def main():
195
+ st.set_page_config(
196
+ page_title="台灣證交所公司資料爬取工具",
197
+ page_icon="🏢",
198
+ layout="wide",
199
+ initial_sidebar_state="expanded"
200
+ )
201
+
202
+ st.title("🏢 台灣證交所公司資料爬取工具")
203
+ st.markdown("這個工具可以幫您從台灣證交所網站爬取上市公司資料,並提供CSV或Excel格式下載。")
204
+
205
+ # 側邊欄參數設定
206
+ with st.sidebar:
207
+ st.header("⚙️ 參數設定")
208
+
209
+ start_offset = st.number_input(
210
+ "起始偏移量",
211
+ min_value=0,
212
+ value=0,
213
+ step=1,
214
+ help="從第幾筆資料開始爬取"
215
+ )
216
+
217
+ max_per = st.slider(
218
+ "每頁筆數",
219
+ min_value=1,
220
+ max_value=100,
221
+ value=30,
222
+ step=1,
223
+ help="每次請求爬取的資料筆數"
224
+ )
225
+
226
+ max_pages = st.slider(
227
+ "最大頁數",
228
+ min_value=1,
229
+ max_value=100,
230
+ value=50,
231
+ step=1,
232
+ help="最多爬取幾頁資料"
233
+ )
234
+
235
+ output_format = st.radio(
236
+ "輸出格式",
237
+ options=["CSV", "Excel", "兩者都要"],
238
+ index=1,
239
+ help="選擇要下載的檔案格式"
240
+ )
241
+
242
+ st.markdown("---")
243
+
244
+ # 使用說明
245
+ with st.expander("📖 使用說明"):
246
+ st.markdown("""
247
+ ### 參數說明:
248
+ - **起始偏移量**:從第幾筆資料開始爬取,通常設為0
249
+ - **每頁筆數**:每次API請求的資料筆數,建議30-50
250
+ - **最大頁數**:最多爬取幾頁,避免設定太大導致執行時間過長
251
+ - **輸出格式**:
252
+ - CSV:純文字格式,適合後續程式處理
253
+ - Excel:包含樣式格式的Excel檔案
254
+ - 兩者都要:同時產生CSV和Excel檔案
255
+
256
+ ### 注意事項:
257
+ - 爬取過程中請勿關閉瀏覽器
258
+ - 建議先用較小的參數測試
259
+ - 檔案會自動加上時間戳記避免重複
260
+ """)
261
+
262
+ # 主要內容區域
263
+ col1, col2 = st.columns([2, 1])
264
+
265
+ with col2:
266
+ start_scraping = st.button("🚀 開始爬取", type="primary", use_container_width=True)
267
+
268
+ # 執行爬取
269
+ if start_scraping:
270
+ # 驗證輸入參數
271
+ if start_offset < 0:
272
+ st.error("起始偏移量不能小於0")
273
+ return
274
+ if max_per <= 0 or max_per > 100:
275
+ st.error("每頁筆數必須在1-100之間")
276
+ return
277
+ if max_pages <= 0 or max_pages > 1000:
278
+ st.error("最大頁數必須在1-1000之間")
279
+ return
280
+
281
+ try:
282
+ # 建立進度條和狀��顯示
283
+ progress_bar = st.progress(0)
284
+ status_text = st.empty()
285
+
286
+ # 開始爬取資料
287
+ status_text.text("開始爬取資料...")
288
+ df = collect_all(start_offset, max_per, max_pages, progress_bar, status_text)
289
+
290
+ if df.empty:
291
+ st.warning("未爬取到任何資料")
292
+ return
293
+
294
+ # 產生時間戳記
295
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
296
+
297
+ # 完成狀態
298
+ progress_bar.progress(1.0)
299
+ status_text.text(f"✅ 成功爬取 {len(df)} 筆公司資料!")
300
+
301
+ # 顯示資料預覽
302
+ st.subheader("📊 資料預覽(前10筆)")
303
+ st.dataframe(df.head(10), use_container_width=True)
304
+
305
+ # 檔案下載區域
306
+ st.subheader("📁 檔案下載")
307
+
308
+ download_col1, download_col2 = st.columns(2)
309
+
310
+ if output_format in ["CSV", "兩者都要"]:
311
+ csv_data = save_to_csv(df)
312
+ with download_col1:
313
+ st.download_button(
314
+ label="⬇️ 下載 CSV 檔案",
315
+ data=csv_data,
316
+ file_name=f"companies_{timestamp}.csv",
317
+ mime="text/csv",
318
+ use_container_width=True
319
+ )
320
+
321
+ if output_format in ["Excel", "兩者都要"]:
322
+ excel_data = save_to_excel(df)
323
+ with download_col2:
324
+ st.download_button(
325
+ label="⬇️ 下載 Excel 檔案",
326
+ data=excel_data,
327
+ file_name=f"companies_styled_{timestamp}.xlsx",
328
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
329
+ use_container_width=True
330
+ )
331
+
332
+ # 顯示統計資訊
333
+ st.subheader("📈 統計資訊")
334
+ stat_col1, stat_col2, stat_col3 = st.columns(3)
335
+
336
+ with stat_col1:
337
+ st.metric("總公司數量", len(df))
338
+
339
+ with stat_col2:
340
+ st.metric("有網址的公司", len(df[df['公司網址'] != '']))
341
+
342
+ with stat_col3:
343
+ st.metric("執行頁數", min(max_pages, (len(df) // max_per) + 1))
344
+
345
+ except Exception as e:
346
+ st.error(f"❌ 爬取過程中發生錯誤:{str(e)}")
347
+
348
+ if __name__ == "__main__":
349
+ main()