Ruby20260314/MomoStreamlit
0
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 )