CoolFace
Apppublic

Ruby20260314/MomoStreamlit

sourceHugging Faceotherupdated 6mo agoView on Hugging Face
0likes
app.py251 linesDownload Raw Back to root
1# -*- coding: utf-8 -*-2import requests3import pandas as pd4import urllib.request5import streamlit as st6import plotly.express as px7import plotly.graph_objects as go8 9# ══════════════════════════════════════════10# 頁面設定11# ══════════════════════════════════════════12st.set_page_config(13    page_title="MOMO 商品價格分析",14    page_icon="🛍️",15    layout="wide"16)17st.title("🛍️ MOMO 商品價格爬取與分析")18st.caption("資料來源:MOMO 購物網")19 20# ══════════════════════════════════════════21# 側邊欄:使用者設定22# ══════════════════════════════════════════23with st.sidebar:24    st.header("⚙️ 搜尋設定")25    keyword   = st.text_input("搜尋關鍵字", value="耳機", placeholder="例如:耳機、口紅…")26    max_pages = st.slider("抓取頁數(每頁約24筆)", min_value=1, max_value=10, value=2, step=1)27    today     = st.text_input("日期標籤", value="20260314")28    run_btn   = st.button("🔍 開始爬取與分析", use_container_width=True)29 30# ══════════════════════════════════════════31# 下載中文字型(只下載一次)32# ══════════════════════════════════════════33@st.cache_resource34def load_font():35    font_url  = "https://drive.google.com/uc?id=1eGAsTN1HBpJAkeVM57_C7ccp7hbgSz3_&export=download"36    font_path = "TaipeiSansTCBeta-Regular.ttf"37    urllib.request.urlretrieve(font_url, font_path)38    return font_path39 40# ══════════════════════════════════════════41# 爬取 MOMO 資料42# ══════════════════════════════════════════43@st.cache_data(show_spinner=False)44def fetch_momo(keyword: str, max_pages: int) -> pd.DataFrame:45    url = "https://apisearch.momoshop.com.tw/momoSearchCloud/moec/textSearch"46    headers = {47        "User-Agent": (48            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "49            "AppleWebKit/537.36 (KHTML, like Gecko) "50            "Chrome/127.0.0.0 Safari/537.36"51        )52    }53    all_products = []54 55    for page in range(1, max_pages + 1):56        payload = {57            "host": "momoshop",58            "flag": "searchEngine",59            "data": {60                "specialGoodsType": "",61                "isBrandSeriesPage": "false",62                "authorNo": "",63                "originalCateCode": "",64                "cateType": "",65                "searchValue": keyword,66                "cateCode": "",67                "cateLevel": "-1",68                "cp": "N",69                "NAM": "N",70                "first": "N",71                "freeze": "N",72                "superstore": "N",73                "tvshop": "N",74                "china": "N",75                "tomorrow": "N",76                "stockYN": "N",77                "prefere": "N",78                "threeHours": "N",79                "video": "N",80                "cycle": "N",81                "cod": "N",82                "superstorePay": "N",83                "showType": "chessboardType",84                "curPage": str(page),85                "priceS": "0",86                "priceE": "9999999",87                "searchType": "1",88                "reduceKeyword": "",89                "isFuzzy": "0",90                "rtnCateDatainfo": {91                    "cateCode": "",92                    "cateLv": "-1",93                    "keyword": keyword,94                    "curPage": str(page),95                    "historyDoPush": "false",96                    "timestamp": 172303602782697                },98                "flag": 2018,99                "serviceCode": "MT01",100                "addressSearchData": {},101                "adSource": "tenmax"102            }103        }104        try:105            response = requests.post(url, headers=headers, json=payload, timeout=10)106            if response.status_code == 200:107                data     = response.json()108                products = data.get("rtnSearchData", {}).get("goodsInfoList", [])109                if not products:110                    break111                for product in products:112                    name  = product.get("goodsName", "")113                    price = product.get("goodsPrice", "")114                    all_products.append({"品名": name, "價格": price})115            else:116                st.warning(f"第 {page} 頁請求失敗,狀態碼:{response.status_code}")117        except Exception as e:118            st.warning(f"第 {page} 頁發生錯誤:{e}")119 120    if not all_products:121        return pd.DataFrame()122 123    df = pd.DataFrame(all_products)124    df["價格"] = (125        df["價格"]126        .astype(str)127        .str.replace("$", "", regex=False)128        .str.replace(",", "", regex=False)129        .str.replace(r"[^\d]", "", regex=True)130    )131    df = df[df["價格"] != ""]132    df["價格"] = df["價格"].astype(int)133    return df.reset_index(drop=True)134 135# ══════════════════════════════════════════136# 主程式:按下按鈕後執行137# ══════════════════════════════════════════138if run_btn:139    if not keyword.strip():140        st.error("請輸入搜尋關鍵字!")141        st.stop()142 143    with st.spinner("載入中文字型..."):144        load_font()145 146    with st.spinner(f"爬取「{keyword}」資料中,共 {max_pages} 頁..."):147        df01 = fetch_momo(keyword, max_pages)148 149    if df01.empty:150        st.error("未取得任何資料,請確認關鍵字或網路連線。")151        st.stop()152 153    # 儲存 CSV154    csv_name = f"{today}_MOMO_{keyword}.csv"155    df01.to_csv(csv_name, encoding="utf-8-sig", index=False)156 157    # 統計數字158    mean_price = df01["價格"].mean()159    max_price  = df01["價格"].max()160    min_price  = df01["價格"].min()161 162    st.success(f"✅ 共抓取 {len(df01)} 筆資料,已儲存至 {csv_name}")163 164    col1, col2, col3 = st.columns(3)165    col1.metric("💰 平均價格", f"{mean_price:,.0f} 元")166    col2.metric("📈 最高價格", f"{max_price:,.0f} 元")167    col3.metric("📉 最低價格", f"{min_price:,.0f} 元")168 169    with st.expander("📋 查看原始資料"):170        st.dataframe(df01, use_container_width=True)171 172    st.divider()173 174    # ══════════════════════════════════════175    # 圖表一:折線圖176    # ══════════════════════════════════════177    st.subheader("📈 售價折線圖")178    fig_line = go.Figure()179    fig_line.add_trace(go.Scatter(180        x=df01.index,181        y=df01["價格"],182        mode="lines+markers",183        name="售價",184        line=dict(color="#EF553B", width=2),185        marker=dict(size=5),186        hovertemplate="商品:%{text}<br>售價:%{y:,} 元<extra></extra>",187        text=df01["品名"]188    ))189    fig_line.add_hline(190        y=mean_price,191        line_dash="dash",192        line_color="blue",193        annotation_text=f"平均價 {mean_price:,.0f} 元",194        annotation_position="top right"195    )196    fig_line.update_layout(197        title=f"{today} MOMO「{keyword}」售價折線圖",198        xaxis_title="商品編號",199        yaxis_title="價格(元)",200        hovermode="x unified",201        template="plotly_white",202        height=450203    )204    st.plotly_chart(fig_line, use_container_width=True)205 206    # ══════════════════════════════════════207    # 圖表二:圓餅圖208    # ══════════════════════════════════════209    st.subheader("🥧 價格區間圓餅圖")210    bins   = [0, 1000, 5000, 10000, 50000, float("inf")]211    labels = ["1,000以下", "1,001–5,000", "5,001–10,000", "10,001–50,000", "50,000以上"]212    df01["price_range"] = pd.cut(df01["價格"], bins=bins, labels=labels)213    pie_data = df01["price_range"].value_counts().reset_index()214    pie_data.columns = ["價格區間", "數量"]215    fig_pie = px.pie(216        pie_data,217        names="價格區間",218        values="數量",219        title=f"{today} MOMO「{keyword}」價格區間分布",220        color_discrete_sequence=px.colors.qualitative.Pastel,221        hole=0.3222    )223    fig_pie.update_traces(textposition="inside", textinfo="percent+label")224    st.plotly_chart(fig_pie, use_container_width=True)225 226    # ══════════════════════════════════════227    # 圖表三:旭日圖228    # ══════════════════════════════════════229    st.subheader("☀️ 價格區間旭日圖")230    df_sun = df01.copy()231    df_sun["price_range"] = df_sun["price_range"].astype(str)232    df_sun["short_name"]  = df_sun["品名"].str[:20]233    fig_sun = px.sunburst(234        df_sun,235        path=["price_range", "short_name"],236        values="價格",237        title=f"{today} MOMO「{keyword}」旭日圖",238        color="價格",239        color_continuous_scale="RdBu"240    )241    fig_sun.update_layout(margin=dict(t=60, l=0, r=0, b=0), height=600)242    st.plotly_chart(fig_sun, use_container_width=True)243 244    # 下載按鈕245    st.divider()246    st.download_button(247        label="⬇️ 下載 CSV 資料",248        data=df01.to_csv(index=False, encoding="utf-8-sig").encode("utf-8-sig"),249        file_name=csv_name,250        mime="text/csv"251    )