Spaces:
Runtime error
Runtime error
| # -*- 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("๐ ่ซๅจๅทฆๅด่จญๅฎ้้ตๅญ่็ญๆธ๏ผ็ถๅพ้ปๆใ้ๅง็ฌๅใใ") |