Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import requests | |
| from bs4 import BeautifulSoup | |
| import pandas as pd | |
| import base64 | |
| from pathlib import Path | |
| # 頁面配置 | |
| st.set_page_config( | |
| page_title="台南寵物醫院查詢", | |
| page_icon="🐕", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # 添加背景圖片的CSS樣式 | |
| def add_bg_from_local(image_file): | |
| """添加本地背景圖片""" | |
| try: | |
| with open(image_file, "rb") as image_file: | |
| encoded_string = base64.b64encode(image_file.read()) | |
| st.markdown( | |
| f""" | |
| <style> | |
| .stApp {{ | |
| background-image: url(data:image/{"png"};base64,{encoded_string.decode()}); | |
| background-size: cover; | |
| background-position: center; | |
| background-repeat: no-repeat; | |
| background-attachment: fixed; | |
| }} | |
| .main-content {{ | |
| background-color: rgba(255, 255, 255, 0.9); | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin: 20px 0; | |
| }} | |
| .stSelectbox > div > div {{ | |
| background-color: rgba(255, 255, 255, 0.9); | |
| }} | |
| .stNumberInput > div > div {{ | |
| background-color: rgba(255, 255, 255, 0.9); | |
| }} | |
| </style> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |
| except FileNotFoundError: | |
| st.warning("背景圖片 'ania.png' 未找到,使用預設背景") | |
| # 嘗試添加背景圖片 | |
| add_bg_from_local('src/ania.png') | |
| # 爬蟲函數 | |
| def scrape_pet_hospitals(selected_sections, max_pages=3): | |
| """爬取寵物醫院資料""" | |
| data = [] | |
| # 進度條 | |
| progress_bar = st.progress(0) | |
| total_sections = len(selected_sections) | |
| for i, section in enumerate(selected_sections): | |
| for page in range(0, max_pages): | |
| url = f'https://petoplay.com/hospital/tainan/{section}?page={page}' | |
| try: | |
| response = requests.get(url, timeout=10) | |
| response.raise_for_status() | |
| soup = BeautifulSoup(response.text, 'html.parser') | |
| hospitals = soup.find_all('a', href=True) | |
| for tag in hospitals: | |
| if f'/hospital/tainan/{section}/' in tag['href'] and tag.text.strip(): | |
| name = tag.text.strip() | |
| parent = tag.find_parent() | |
| address_tag = parent.find_next('div', class_='petadr') | |
| address = address_tag.get_text(strip=True).replace('\n', '') if address_tag else 'N/A' | |
| score_tag = parent.find_next('span', class_='gscore') | |
| score = score_tag.text.strip() if score_tag else 'N/A' | |
| data.append({ | |
| '店名': name, | |
| '地址': address, | |
| '評分': score, | |
| '區段': section | |
| }) | |
| except requests.exceptions.RequestException as e: | |
| st.warning(f"區段 {section} 第 {page+1} 頁爬取失敗: {e}") | |
| continue | |
| # 更新進度條 | |
| progress_bar.progress((i + 1) / total_sections) | |
| return pd.DataFrame(data) | |
| # LINE Bot 發送函數 | |
| def send_line_message(message, channel_access_token): | |
| """發送LINE訊息""" | |
| try: | |
| from linebot import LineBotApi | |
| from linebot.models import TextSendMessage | |
| line_bot_api = LineBotApi(channel_access_token) | |
| text_message = TextSendMessage(text=message) | |
| line_bot_api.broadcast(text_message) | |
| return True, "訊息發送成功!" | |
| except ImportError: | |
| return False, "請先安裝 line-bot-sdk: pip install line-bot-sdk" | |
| except Exception as e: | |
| return False, f"發送失敗: {str(e)}" | |
| # 主介面 | |
| def main(): | |
| st.markdown('<div class="main-content">', unsafe_allow_html=True) | |
| # 標題 | |
| st.title("🐕 台南寵物醫院查詢系統") | |
| st.markdown("---") | |
| # 側邊欄設定 | |
| with st.sidebar: | |
| st.header("🔧 查詢設定") | |
| # 區段選擇 | |
| all_sections = [1, 2, 3, 4, 5, 6, 7, 8, 9, 14, 18, 19, 20, 22, 23, 25, 27, 29, 32, 33, 36] | |
| selected_sections = st.multiselect( | |
| "選擇要查詢的區段", | |
| options=all_sections, | |
| default=all_sections[:5], # 預設選擇前5個區段 | |
| help="選擇你想要查詢的台南市區段代碼" | |
| ) | |
| # 頁數設定 | |
| max_pages = st.slider("每個區段查詢頁數", 1, 5, 3) | |
| # 評分篩選 | |
| min_score = st.number_input( | |
| "最低評分篩選", | |
| min_value=0.0, | |
| max_value=5.0, | |
| value=4.0, | |
| step=0.1, | |
| help="只顯示評分高於此值的醫院" | |
| ) | |
| # LINE Bot 設定 | |
| st.header("📱 LINE 通知設定") | |
| channel_access_token = st.text_input( | |
| "LINE Channel Access Token", | |
| type="password", | |
| help="輸入你的 LINE Bot Channel Access Token" | |
| ) | |
| # 主要內容區域 | |
| col1, col2 = st.columns([3, 1]) | |
| with col2: | |
| search_button = st.button("🔍 開始查詢", type="primary", use_container_width=True) | |
| if search_button and selected_sections: | |
| with st.spinner("正在爬取寵物醫院資料..."): | |
| # 爬取資料 | |
| df = scrape_pet_hospitals(selected_sections, max_pages) | |
| if not df.empty: | |
| # 處理評分資料 | |
| df['評分_數值'] = pd.to_numeric(df['評分'], errors='coerce') | |
| # 篩選資料 | |
| filtered_df = df[df['評分_數值'] >= min_score].dropna(subset=['評分_數值']) | |
| # 顯示結果 | |
| st.success(f"成功爬取到 {len(df)} 家寵物醫院,篩選後顯示 {len(filtered_df)} 家") | |
| if not filtered_df.empty: | |
| # 資料統計 | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric("總醫院數", len(df)) | |
| with col2: | |
| st.metric("篩選後數量", len(filtered_df)) | |
| with col3: | |
| st.metric("平均評分", f"{filtered_df['評分_數值'].mean():.2f}") | |
| with col4: | |
| st.metric("最高評分", f"{filtered_df['評分_數值'].max():.2f}") | |
| st.markdown("---") | |
| # 顯示表格 | |
| st.subheader("📋 查詢結果") | |
| # 先按評分排序,然後選擇要顯示的欄位 | |
| sorted_df = filtered_df.sort_values('評分_數值', ascending=False) | |
| display_df = sorted_df[['店名', '地址', '評分', '區段']] | |
| st.dataframe( | |
| display_df, | |
| use_container_width=True, | |
| hide_index=True | |
| ) | |
| # 下載功能 | |
| csv = display_df.to_csv(index=False, encoding='utf-8-sig') | |
| st.download_button( | |
| label="📥 下載 CSV 檔案", | |
| data=csv, | |
| file_name=f"台南寵物醫院_{min_score}分以上.csv", | |
| mime="text/csv" | |
| ) | |
| # LINE 發送功能 | |
| if channel_access_token: | |
| st.markdown("---") | |
| col1, col2 = st.columns([3, 1]) | |
| with col1: | |
| st.subheader("📱 發送到 LINE") | |
| with col2: | |
| if st.button("發送訊息", type="secondary"): | |
| # 準備訊息內容(取前5個結果) | |
| top_5 = display_df.head(5) | |
| message = f"台南寵物醫院推薦 (評分≥{min_score})\n\n" | |
| for _, row in top_5.iterrows(): | |
| message += f"🏥 {row['店名']}\n" | |
| message += f"📍 {row['地址']}\n" | |
| message += f"⭐ {row['評分']}\n\n" | |
| success, msg = send_line_message(message, channel_access_token) | |
| if success: | |
| st.success(msg) | |
| else: | |
| st.error(msg) | |
| else: | |
| st.warning(f"沒有找到評分 {min_score} 分以上的寵物醫院") | |
| else: | |
| st.error("未能獲取到任何資料,請檢查網路連線或稍後再試") | |
| elif search_button and not selected_sections: | |
| st.warning("請至少選擇一個區段進行查詢") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # 頁尾 | |
| st.markdown("---") | |
| st.markdown( | |
| """ | |
| <div style='text-align: center; color: #666; padding: 20px; background-color: rgba(255, 255, 255, 0.8); border-radius: 10px;'> | |
| <p>🐾 台南寵物醫院查詢系統 | 資料來源: petoplay.com</p> | |
| <p>💡 提示: 將 ania.png 圖片檔案放置在與此腳本相同的目錄下作為背景</p> | |
| </div> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |
| if __name__ == "__main__": | |
| main() |