Pchome / app.py
Angelguava's picture
Create app.py
b1873fe verified
Raw
History Blame Contribute Delete
9.88 kB
# -*- coding: utf-8 -*-
import streamlit as st
import requests
import json
import pandas as pd
import time
import plotly.express as px
import plotly.graph_objects as go
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import matplotlib as mpl
import os
import tempfile
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ้ ้ข่จญๅฎš
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
st.set_page_config(
page_title="PChome ๅ•†ๅ“ๅƒนๆ ผๅˆ†ๆž",
page_icon="๐Ÿ›’",
layout="wide",
)
st.title("๐Ÿ›’ PChome ้›ปๅ•†ๅ•†ๅ“ๅƒนๆ ผๅˆ†ๆž")
st.markdown("่ผธๅ…ฅ้—œ้ตๅญ—่ˆ‡็ญ†ๆ•ธ๏ผŒ่‡ชๅ‹•็ˆฌๅ– PChome ๅ•†ๅ“่ณ‡ๆ–™ไธฆ่ฆ–่ฆบๅŒ–ๅˆ†ๆžใ€‚")
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ๅด้‚Šๆฌ„๏ผšไฝฟ็”จ่€…่ผธๅ…ฅ
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with st.sidebar:
st.header("โš™๏ธ ๆœๅฐ‹่จญๅฎš")
keyword = st.text_input("ๆœๅฐ‹้—œ้ตๅญ—", value="่€ณๆฉŸ", placeholder="ไพ‹๏ผš่€ณๆฉŸใ€็ญ†้›ปใ€ๆป‘้ผ ")
max_items = st.slider("ๆœ€ๅคš็ญ†ๆ•ธ", min_value=20, max_value=200, value=60, step=20)
sort_option = st.selectbox(
"ๆŽ’ๅบๆ–นๅผ",
options=["sale/dc", "price/ac", "price/dc", "new/dc"],
format_func=lambda x: {
"sale/dc": "้Šท้‡้ซ˜โ†’ไฝŽ",
"price/ac": "ๅƒนๆ ผไฝŽโ†’้ซ˜",
"price/dc": "ๅƒนๆ ผ้ซ˜โ†’ไฝŽ",
"new/dc": "ๆœ€ๆ–ฐไธŠๆžถ",
}[x],
)
sleep_sec = st.slider("ๆฏ้ ่ซ‹ๆฑ‚้–“้š”๏ผˆ็ง’๏ผ‰", min_value=1, max_value=10, value=2)
run_btn = st.button("๐Ÿš€ ้–‹ๅง‹็ˆฌๅ–", use_container_width=True)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ็ˆฌ่Ÿฒๅ‡ฝๅผ
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
ITEMS_PER_PAGE = 20 # PChome ๆฏ้ ๅ›บๅฎš 20 ็ญ†
def fetch_pchome(keyword: str, max_items: int, sort: str, sleep_sec: int) -> pd.DataFrame:
total_pages = -(-max_items // ITEMS_PER_PAGE) # ็„กๆขไปถ้€ฒไฝ้™คๆณ•
all_data = pd.DataFrame()
progress = st.progress(0, text="็ˆฌๅ–ไธญโ€ฆ")
for i in range(1, total_pages + 1):
url = (
f"https://ecshweb.pchome.com.tw/search/v3.3/all/results"
f"?q={keyword}&page={i}&sort={sort}"
)
try:
resp = requests.get(url, timeout=15)
resp.raise_for_status()
data = json.loads(resp.content)
if "prods" not in data or not data["prods"]:
break
df_page = pd.DataFrame(data["prods"])
all_data = pd.concat([all_data, df_page], ignore_index=True)
except Exception as e:
st.warning(f"็ฌฌ {i} ้ ็ˆฌๅ–ๅคฑๆ•—๏ผš{e}")
break
progress.progress(i / total_pages, text=f"ๅทฒ็ˆฌๅ–็ฌฌ {i}/{total_pages} ้ โ€ฆ")
if i < total_pages:
time.sleep(sleep_sec)
progress.empty()
# ๆˆชๆ–ทๅˆฐไฝฟ็”จ่€…่ฆๆฑ‚็š„็ญ†ๆ•ธ
return all_data.head(max_items)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ไธปๆต็จ‹
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if run_btn:
if not keyword.strip():
st.error("่ซ‹่ผธๅ…ฅๆœๅฐ‹้—œ้ตๅญ—๏ผ")
st.stop()
with st.spinner("ๆญฃๅœจ็ˆฌๅ–่ณ‡ๆ–™๏ผŒ่ซ‹็จๅ€™โ€ฆ"):
raw_df = fetch_pchome(keyword, max_items, sort_option, sleep_sec)
if raw_df.empty:
st.error("ๆœชๅ–ๅพ—ไปปไฝ•่ณ‡ๆ–™๏ผŒ่ซ‹็ขบ่ช้—œ้ตๅญ—ๆˆ–็จๅพŒๅ†่ฉฆใ€‚")
st.stop()
# ๅชไฟ็•™้œ€่ฆ็š„ๆฌ„ไฝ๏ผˆๆœ‰ไบ›ๅ•†ๅ“ๅฏ่ƒฝ็ผบๆฌ„๏ผ‰
cols_needed = [c for c in ["name", "price", "brand", "storeId", "picS"] if c in raw_df.columns]
df = raw_df[cols_needed].copy()
df["price"] = pd.to_numeric(df["price"], errors="coerce")
df = df.dropna(subset=["price"])
df = df.reset_index(drop=True)
df.index += 1 # ๅพž 1 ้–‹ๅง‹
avg_price = df["price"].mean()
max_price = df["price"].max()
min_price = df["price"].min()
# โ”€โ”€ ็ตฑ่จˆๆŒ‡ๆจ™ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
st.subheader("๐Ÿ“Š ๅƒนๆ ผ็ตฑ่จˆ")
c1, c2, c3, c4 = st.columns(4)
c1.metric("็ญ†ๆ•ธ", f"{len(df)} ็ญ†")
c2.metric("ๅนณๅ‡ๅƒนๆ ผ", f"NT$ {avg_price:,.0f}")
c3.metric("ๆœ€้ซ˜ๅƒนๆ ผ", f"NT$ {max_price:,.0f}")
c4.metric("ๆœ€ไฝŽๅƒนๆ ผ", f"NT$ {min_price:,.0f}")
# โ”€โ”€ ไธ‹่ผ‰ CSV โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import datetime
today = datetime.date.today().strftime("%Y%m%d")
csv_bytes = df.to_csv(index=False, encoding="utf-8-sig").encode("utf-8-sig")
st.download_button(
label="๐Ÿ“ฅ ไธ‹่ผ‰ CSV",
data=csv_bytes,
file_name=f"{today}_PCHOME_{keyword}.csv",
mime="text/csv",
)
# โ”€โ”€ ่ณ‡ๆ–™่กจ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with st.expander("๐Ÿ” ๆŸฅ็œ‹ๅŽŸๅง‹่ณ‡ๆ–™", expanded=False):
st.dataframe(df, use_container_width=True)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๅœ–่กจๅ€
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
st.subheader("๐Ÿ“ˆ ่ฆ–่ฆบๅŒ–ๅˆ†ๆž")
tab1, tab2, tab3 = st.tabs(["ๆŠ˜็ทšๅœ–", "ๅœ“้ค…ๅœ–", "ๆ—ญๆ—ฅๅœ–"])
# โ”€โ”€ Tab1๏ผšๆŠ˜็ทšๅœ– โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with tab1:
fig_line = go.Figure()
fig_line.add_trace(
go.Scatter(
x=df.index,
y=df["price"],
mode="lines+markers",
name="ๅ”ฎๅƒน",
line=dict(color="#4C9BE8", width=2),
marker=dict(size=5),
hovertext=df["name"] if "name" in df.columns else None,
hovertemplate="<b>%{hovertext}</b><br>ๅ”ฎๅƒน๏ผšNT$ %{y:,.0f}<extra></extra>",
)
)
fig_line.add_hline(
y=avg_price,
line_dash="dash",
line_color="red",
annotation_text=f"ๅนณๅ‡ NT$ {avg_price:,.0f}",
annotation_position="top left",
)
fig_line.update_layout(
title=f"{today} PChome ้›ปๅ•†ใ€Œ{keyword}ใ€ๅ”ฎๅƒน่ตฐๅ‹ข",
xaxis_title="ๅ•†ๅ“็ทจ่™Ÿ",
yaxis_title="ๅƒนๆ ผ๏ผˆNT$๏ผ‰",
hovermode="x unified",
height=500,
)
st.plotly_chart(fig_line, use_container_width=True)
# โ”€โ”€ Tab2๏ผšๅœ“้ค…ๅœ–๏ผˆไพๅƒนๆ ผๅ€้–“๏ผ‰ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with tab2:
bins = [0, 500, 1000, 3000, 5000, 10000, float("inf")]
labels = ["โ‰ค500", "501~1000", "1001~3000", "3001~5000", "5001~10000", ">10000"]
df["price_range"] = pd.cut(df["price"], bins=bins, labels=labels, right=True)
pie_data = df["price_range"].value_counts().reset_index()
pie_data.columns = ["price_range", "count"]
pie_data = pie_data.sort_values("price_range")
fig_pie = px.pie(
pie_data,
names="price_range",
values="count",
title=f"ใ€Œ{keyword}ใ€ๅ„ๅƒนๆ ผๅ€้–“ๅ•†ๅ“ๅ ๆฏ”",
hole=0.35,
color_discrete_sequence=px.colors.qualitative.Set3,
)
fig_pie.update_traces(textposition="inside", textinfo="percent+label")
fig_pie.update_layout(height=500)
st.plotly_chart(fig_pie, use_container_width=True)
# โ”€โ”€ Tab3๏ผšๆ—ญๆ—ฅๅœ–๏ผˆๅ“็‰Œ โ†’ ๅƒนๆ ผๅ€้–“๏ผ‰ โ”€โ”€โ”€โ”€โ”€โ”€
with tab3:
if "brand" in df.columns and df["brand"].notna().any():
sun_df = df[["brand", "price_range"]].dropna()
sun_df["brand"] = sun_df["brand"].fillna("ๆœช็Ÿฅๅ“็‰Œ").replace("", "ๆœช็Ÿฅๅ“็‰Œ")
sun_df["count"] = 1
sun_df = sun_df.groupby(["brand", "price_range"], as_index=False)["count"].sum()
fig_sun = px.sunburst(
sun_df,
path=["brand", "price_range"],
values="count",
title=f"ใ€Œ{keyword}ใ€ๅ“็‰Œ ร— ๅƒนๆ ผๅ€้–“ๆ—ญๆ—ฅๅœ–",
color="count",
color_continuous_scale="Blues",
)
fig_sun.update_layout(height=600)
st.plotly_chart(fig_sun, use_container_width=True)
else:
# brand ๆฌ„ไธๅญ˜ๅœจๆ™‚๏ผŒๆ”น็”จๅ•†ๅ“ๅ็จฑๅ‰็ถด ร— ๅƒนๆ ผๅ€้–“
df["name_short"] = df["name"].str[:6] + "โ€ฆ" if "name" in df.columns else "ๅ•†ๅ“"
sun_df = df[["name_short", "price_range"]].dropna()
sun_df["count"] = 1
sun_df = sun_df.groupby(["name_short", "price_range"], as_index=False)["count"].sum()
fig_sun = px.sunburst(
sun_df,
path=["name_short", "price_range"],
values="count",
title=f"ใ€Œ{keyword}ใ€ๅ•†ๅ“ ร— ๅƒนๆ ผๅ€้–“ๆ—ญๆ—ฅๅœ–",
color="count",
color_continuous_scale="Blues",
)
fig_sun.update_layout(height=600)
st.plotly_chart(fig_sun, use_container_width=True)
else:
st.info("๐Ÿ‘ˆ ่ซ‹ๅœจๅทฆๅด่จญๅฎš้—œ้ตๅญ—่ˆ‡็ญ†ๆ•ธ๏ผŒ็„ถๅพŒ้ปžๆ“Šใ€Œ้–‹ๅง‹็ˆฌๅ–ใ€ใ€‚")