Pi_search / app.py
Lashtw's picture
Update app.py
5ffd753 verified
import streamlit as st
import pandas as pd
import requests
import json
from io import StringIO
import plotly.express as px
import qrcode
from PIL import Image
import io
# 自訂 CSS 確保標題不換行、調整圖片大小、優化按鈕布局、右下角資訊並調整頒獎台樣式
st.markdown("""
<style>
.stApp header h1 {
white-space: nowrap; /* 防止標題換行 */
overflow: hidden;
text-overflow: ellipsis;
font-size: 2rem; /* 調整字體大小為 2rem,約 32px */
}
.stImage > img {
max-height: 80vh; /* 圖片高度接近視窗高度 */
object-fit: contain; /* 保持比例 */
}
/* 調整按鈕樣式以確保對稱 */
.stButton > button {
width: 100px; /* 固定按鈕寬度 */
text-align: center; /* 文字居中 */
padding: 5px 10px; /* 調整內部間距 */
margin: 0 auto; /* 水平居中 */
}
/* 確保列寬度均等 */
.st-ds {
justify-content: space-between; /* 兩列間距均等 */
}
/* 右下角資訊 */
.footer {
position: fixed;
bottom: 10px;
right: 10px;
font-size: 0.9rem;
color: #666;
padding: 5px;
}
/* 頒獎台樣式(正常顯示) */
.podium-container {
display: flex;
justify-content: center;
align-items: flex-end; /* 確保下緣對齊 */
margin-top: 20px;
margin-bottom: 20px;
}
.podium {
text-align: center;
width: 120px;
margin: 0 10px;
display: flex;
flex-direction: column;
}
.podium-filler {
background-color: #1a1a1a; /* 與背景同色 */
flex-grow: 0;
}
.podium-step {
border: 2px solid #000;
color: #333;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
flex-grow: 0;
}
.podium-step.first {
height: 150px; /* 第一名完整高度 */
background-color: #ffd700; /* 金色 */
}
.podium-step.second {
height: 120px; /* 第二名高度 */
background-color: #c0c0c0; /* 銀色 */
}
.podium-step.third {
height: 90px; /* 第三名高度 */
background-color: #cd7f32; /* 銅色 */
}
.podium-label {
margin-top: 5px;
font-size: 1rem;
color: #fff; /* 文字改為白色 */
border: 2px solid #fff; /* 添加白色邊框 */
padding: 5px; /* 內距 */
background-color: rgba(0, 0, 0, 0.5); /* 半透明背景,提升可讀性 */
white-space: pre-line; /* 允許換行 */
}
</style>
""", unsafe_allow_html=True)
# 圓周率與無限猴子定理的介紹與分頁圖片展示
st.title("探索圓周率與無限猴子定理的奇妙世界")
st.write("""
歡迎參加這個有趣的活動!我們將一起探索圓周率 (π) 和無限猴子定理的奧秘,並透過幸運數字查詢系統,發現你選的數字在 π 中的秘密。
""")
# 定義圖片和標題
image_data = [
{"path": "m1.GIF", "caption": "圓周率與無限猴子定理的奇妙關聯"},
{"path": "m2.GIF", "caption": "重複與不重複"},
{"path": "m3.GIF", "caption": "圓周率裡藏著你的幸運號碼"},
{"path": "m4.GIF", "caption": "無限猴子定理是什麼?"},
{"path": "m5.GIF", "caption": "圓周率是無限長的數字打字機"},
]
# 初始化 session state 用於追蹤當前頁面
if 'page' not in st.session_state:
st.session_state.page = 0
# 顯示當前頁面的圖片,放大至幾乎全版面
st.image(image_data[st.session_state.page]["path"],
caption=image_data[st.session_state.page]["caption"],
use_container_width=True)
# 換頁按鈕
col1, col2 = st.columns(2)
with col1:
if st.button("上一頁", disabled=st.session_state.page <= 0):
st.session_state.page -= 1
st.rerun()
with col2:
if st.button("下一頁", disabled=st.session_state.page >= len(image_data) - 1):
st.session_state.page += 1
st.rerun()
# 顯示當前頁數
st.write(f"頁面: {st.session_state.page + 1} / {len(image_data)}")
# 分隔線
st.markdown("---")
# 查詢數字的函數
def process_number(number):
"""查詢特定數字並返回結果"""
url = f"https://www.angio.net/newpi/piquery?q={number}"
try:
response = requests.get(url)
response.raise_for_status()
data = response.json()
# 從回應中提取 'p' 值
if data.get('status') == 'OK' and data.get('r'):
return data['r'][0].get('p')
return None
except Exception as e:
st.error(f"處理數字 {number} 時發生錯誤: {str(e)}")
return None
def generate_qr_code(url):
"""生成 QR Code 並返回圖片"""
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
return img
def main():
st.title("幸運數字查詢系統")
# 新增功能:輸入表單連結並生成 QR Code
st.subheader("請輸入 Google 表單連結")
form_url = st.text_input("表單連結", placeholder="請輸入 Google 表單的完整 URL")
if form_url:
try:
# 驗證 URL 是否有效(簡單檢查)
if not form_url.startswith("https://"):
st.error("請輸入有效的 URL(以 https:// 開頭)")
else:
st.write("以下是表單連結的 QR Code,學生可以使用平板掃描填寫:")
# 生成 QR Code
qr_img = generate_qr_code(form_url)
# 將 PIL 圖片轉為字節流以顯示在 Streamlit
buf = io.BytesIO()
qr_img.save(buf, format="PNG")
byte_im = buf.getvalue()
st.image(byte_im, caption="掃描此 QR Code 前往表單", use_container_width=False)
except Exception as e:
st.error(f"生成 QR Code 時發生錯誤: {str(e)}")
# 分隔線
st.markdown("---")
# 讓使用者選擇幸運數字的位數
selected_digits = st.selectbox(
"請選擇幸運數字的位數",
options=range(4, 11), # 4 到 10 位
index=3 # 預設選 7 位
)
# 檔案上傳部分
uploaded_file = st.file_uploader("請上傳CSV檔案", type=['csv'])
if uploaded_file:
try:
# 嘗試讀取 CSV,假設有表頭
df = pd.read_csv(
StringIO(uploaded_file.getvalue().decode('utf-8')),
header=0, # 預設有表頭
dtype=str # 所有欄位都以字串形式讀取
)
# 定義可能的欄位映射
required_columns = {
"您的姓名": "姓名",
"你的幸運號碼是?(可重複,前面也可以是0。Ex. 0000013、1111111)": "幸運號碼",
"姓名": "姓名",
"自選號碼": "幸運號碼"
}
# 檢查是否有必要的欄位
available_columns = [col for col in required_columns.keys() if col in df.columns]
if not available_columns or len(available_columns) < 2:
# 若無表頭或缺少必要欄位,假設無表頭,第一列為姓名,第二列為幸運號碼
df = pd.read_csv(
StringIO(uploaded_file.getvalue().decode('utf-8')),
header=None,
dtype=str
)
if len(df.columns) >= 2:
df.columns = ["姓名", "幸運號碼"]
else:
st.error("CSV 檔案格式錯誤,缺少 '姓名' 或 '幸運號碼' 欄位。")
return
# 篩選並重新命名需要的欄位,忽略「時間戳記」
df = df[[col for col in df.columns if col in required_columns]].rename(columns=required_columns)
# 顯示原始資料
st.subheader("原始資料")
st.dataframe(df)
# 處理每個幸運數字
st.subheader("處理結果")
# 建立進度條
progress_bar = st.progress(0)
status_text = st.empty()
# 新增結果欄位
df['查詢結果'] = None
# 處理每個數字
total_rows = len(df)
for index, row in df.iterrows():
# 更新進度
progress = (index + 1) / total_rows
progress_bar.progress(progress)
status_text.text(f"正在處理: {index + 1}/{total_rows}")
# 獲取幸運數字並檢查位數
original_number = str(row['幸運號碼'])
number_length = len(original_number)
if number_length > selected_digits:
# 超過指定位數,標記為不符規定
df.at[index, '幸運號碼'] = original_number # 保留原始輸入
df.at[index, '查詢結果'] = "數字不符規定"
else:
# 小於或等於指定位數,補零後查詢
lucky_number = original_number.zfill(selected_digits)
result = process_number(lucky_number)
df.at[index, '幸運號碼'] = lucky_number # 更新為補零後的數字
df.at[index, '查詢結果'] = result
# 顯示結果
st.subheader("完整結果")
st.dataframe(df)
# 提供下載功能,確保輸出時保留前導零
csv = df.to_csv(index=False, encoding='utf-8')
st.download_button(
label="下載處理結果",
data=csv,
file_name="processed_results.csv",
mime="text/csv"
)
# 新增長條圖功能
st.subheader("查詢結果長條圖")
# 篩選有效數據(查詢結果不為 "數字不符規定" 且不為 None)
valid_df = df[df['查詢結果'].notna() & (df['查詢結果'] != "數字不符規定")].copy()
if not valid_df.empty:
# 將查詢結果轉為數值型
valid_df['查詢結果'] = valid_df['查詢結果'].astype(float)
# 拼接姓名和幸運號碼作為橫軸標籤
valid_df['姓名_幸運號碼'] = valid_df['姓名'] + "_" + valid_df['幸運號碼']
# 按照查詢結果從大到小排序
valid_df = valid_df.sort_values(by='查詢結果', ascending=False)
# 使用 Plotly 繪製長條圖
fig = px.bar(
valid_df,
x='姓名_幸運號碼',
y='查詢結果',
labels={'姓名_幸運號碼': '姓名與幸運號碼', '查詢結果': '查詢結果數值'},
title="幸運數字查詢結果長條圖(按數值從大到小排序)"
)
# 調整圖表佈局
fig.update_layout(
xaxis_title="姓名與幸運號碼",
yaxis_title="查詢結果數值",
xaxis_tickangle=-45, # 旋轉橫軸標籤以避免重疊
margin=dict(l=50, r=50, t=80, b=150) # 增加底部邊距以顯示完整標籤
)
# 顯示圖表
st.plotly_chart(fig, use_container_width=True)
# 新增頒獎台功能
st.subheader("頒獎台:查詢結果前三名")
# 獲取前三名(已按查詢結果降序排序)
top_3 = valid_df.head(3)
if len(top_3) > 0:
# 正常顯示頒獎台
col1, col2, col3 = st.columns(3) # 創建三列
with col1:
if len(top_3) >= 2:
second = top_3.iloc[1]
st.markdown(
f'<div class="podium">'
f'<div class="podium-filler" style="height: 30px;"></div>' # 填充 30px
f'<div class="podium-step second">{int(second["查詢結果"])}</div>'
f'<div class="podium-label">{second["姓名"]}\n{second["幸運號碼"]}</div>'
f'</div>',
unsafe_allow_html=True
)
else:
st.markdown('<div class="podium"><div class="podium-filler" style="height: 30px;"></div><div class="podium-step second"></div><div class="podium-label"></div></div>', unsafe_allow_html=True)
with col2:
if len(top_3) >= 1:
first = top_3.iloc[0]
st.markdown(
f'<div class="podium">'
f'<div class="podium-step first">{int(first["查詢結果"])}</div>'
f'<div class="podium-label">{first["姓名"]}\n{first["幸運號碼"]}</div>'
f'</div>',
unsafe_allow_html=True
)
else:
st.markdown('<div class="podium"><div class="podium-step first"></div><div class="podium-label"></div></div>', unsafe_allow_html=True)
with col3:
if len(top_3) >= 3:
third = top_3.iloc[2]
st.markdown(
f'<div class="podium">'
f'<div class="podium-filler" style="height: 60px;"></div>' # 填充 60px
f'<div class="podium-step third">{int(third["查詢結果"])}</div>'
f'<div class="podium-label">{third["姓名"]}\n{third["幸運號碼"]}</div>'
f'</div>',
unsafe_allow_html=True
)
else:
st.markdown('<div class="podium"><div class="podium-filler" style="height: 60px;"></div><div class="podium-step third"></div><div class="podium-label"></div></div>', unsafe_allow_html=True)
else:
st.warning("無有效的查詢結果可供顯示頒獎台。")
else:
st.warning("無有效的查詢結果可供繪製長條圖或頒獎台。請確認資料是否正確。")
except Exception as e:
st.error(f"處理檔案時發生錯誤: {str(e)}")
# 添加右下角資訊
st.markdown('<div class="footer">程式設計:新竹縣立精華國中 藍星宇老師</div>', unsafe_allow_html=True)
if __name__ == "__main__":
main()