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() |