Spaces:
Sleeping
Sleeping
| import requests | |
| import pandas as pd | |
| import streamlit as st | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| import matplotlib.font_manager as fm | |
| import os | |
| import platform | |
| # โโ ้ ้ข่จญๅฎ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| st.set_page_config( | |
| page_title="MOMO ๅๅๅนๆ ผๅๆ", | |
| page_icon="๐", | |
| layout="wide", | |
| ) | |
| st.title("๐ MOMO ้ปๅๅๅๅนๆ ผๅๆๅ่กจๆฟ") | |
| st.markdown("่ผธๅ ฅ้้ตๅญ๏ผๅณๆๆๅ MOMO ๅๅ่ณๆไธฆ่ฆ่ฆบๅๅๆใ") | |
| # โโ ไธญๆๅญๅๅฎ่ฃ๏ผไธไฝฟ็จ wget๏ผ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def install_chinese_font(): | |
| """ | |
| ๅ่ฉฆๅฎ่ฃ็ณป็ตฑไธญๆๅญๅ๏ผ่ฅๆพไธๅฐๅไธ่ผ Noto Sans TCใ | |
| ๅๅณๅญๅ่ทฏๅพ๏ผไพ matplotlib ไฝฟ็จ๏ผPlotly ๅฆ่ก่จญๅฎ๏ผใ | |
| """ | |
| # ๅธธ่ฆ็ณป็ตฑไธญๆๅญๅ่ทฏๅพ | |
| system_fonts = { | |
| "Linux": [ | |
| "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", | |
| "/usr/share/fonts/opentype/noto/NotoSansCJKtc-Regular.otf", | |
| ], | |
| "Darwin": [ # macOS | |
| "/System/Library/Fonts/PingFang.ttc", | |
| "/Library/Fonts/Arial Unicode.ttf", | |
| ], | |
| "Windows": [ | |
| "C:/Windows/Fonts/msjh.ttc", | |
| "C:/Windows/Fonts/mingliu.ttc", | |
| ], | |
| } | |
| sys_name = platform.system() | |
| for path in system_fonts.get(sys_name, []): | |
| if os.path.exists(path): | |
| return path | |
| # ๆพไธๅฐ็ณป็ตฑๅญๅ โ ็จ requests ไธ่ผ Noto Sans TC | |
| font_path = "NotoSansTC-Regular.ttf" | |
| if not os.path.exists(font_path): | |
| url = ( | |
| "https://github.com/googlefonts/noto-cjk/raw/main/Sans/OTF/TraditionalChinese/" | |
| "NotoSansCJKtc-Regular.otf" | |
| ) | |
| try: | |
| r = requests.get(url, timeout=30) | |
| r.raise_for_status() | |
| with open(font_path, "wb") as f: | |
| f.write(r.content) | |
| fm.fontManager.addfont(font_path) | |
| except Exception as e: | |
| st.warning(f"ๅญๅไธ่ผๅคฑๆ๏ผ{e}๏ผๅ่กจไธญๆๅฏ่ฝ้กฏ็คบ็ฐๅธธใ") | |
| return None | |
| return font_path | |
| install_chinese_font() | |
| # โโ ๆๅ MOMO ่ณๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def fetch_momo(keyword: str, max_items: int) -> pd.DataFrame: | |
| url = "https://apisearch.momoshop.com.tw/momoSearchCloud/moec/textSearch" | |
| headers = { | |
| "User-Agent": ( | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/127.0.0.0 Safari/537.36" | |
| ) | |
| } | |
| all_products = [] | |
| page = 1 | |
| per_page = 24 # MOMO ้ ่จญๆฏ้ ็ด 24 ็ญ | |
| while len(all_products) < max_items: | |
| payload = { | |
| "host": "momoshop", | |
| "flag": "searchEngine", | |
| "data": { | |
| "specialGoodsType": "", | |
| "isBrandSeriesPage": "false", | |
| "authorNo": "", | |
| "originalCateCode": "", | |
| "cateType": "", | |
| "searchValue": keyword, | |
| "cateCode": "", | |
| "cateLevel": "-1", | |
| "cp": "N", "NAM": "N", "first": "N", "freeze": "N", | |
| "superstore": "N", "tvshop": "N", "china": "N", | |
| "tomorrow": "N", "stockYN": "N", "prefere": "N", | |
| "threeHours": "N", "video": "N", "cycle": "N", | |
| "cod": "N", "superstorePay": "N", | |
| "showType": "chessboardType", | |
| "curPage": str(page), | |
| "priceS": "0", | |
| "priceE": "9999999", | |
| "searchType": "1", | |
| "reduceKeyword": "", | |
| "isFuzzy": "0", | |
| "rtnCateDatainfo": { | |
| "cateCode": "", | |
| "cateLv": "-1", | |
| "keyword": keyword, | |
| "curPage": str(page), | |
| "historyDoPush": "false", | |
| "timestamp": 1723036027826, | |
| }, | |
| "flag": 2018, | |
| "serviceCode": "MT01", | |
| "addressSearchData": {}, | |
| "adSource": "tenmax", | |
| }, | |
| } | |
| try: | |
| resp = requests.post(url, headers=headers, json=payload, timeout=15) | |
| resp.raise_for_status() | |
| except Exception as e: | |
| st.error(f"็ฌฌ {page} ้ ่ซๆฑๅคฑๆ๏ผ{e}") | |
| break | |
| data = resp.json() | |
| products = data.get("rtnSearchData", {}).get("goodsInfoList", []) | |
| if not products: | |
| break | |
| for p in products: | |
| name = p.get("goodsName", "") | |
| price_raw = p.get("goodsPrice", "0") | |
| try: | |
| price = int( | |
| str(price_raw).replace("$", "").replace(",", "").strip() | |
| ) | |
| except ValueError: | |
| price = 0 | |
| all_products.append({"ๅๅ": name, "ๅนๆ ผ": price}) | |
| page += 1 | |
| if len(products) < per_page: | |
| break # ๅทฒ็กๆดๅค้ | |
| df = pd.DataFrame(all_products[:max_items]) | |
| return df | |
| # โโ ๅด้ๆฌ๏ผไฝฟ็จ่ ่ผธๅ ฅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with st.sidebar: | |
| st.header("โ๏ธ ๆฅ่ฉข่จญๅฎ") | |
| keyword = st.text_input("ๆๅฐ้้ตๅญ", value="่ณๆฉ") | |
| max_items = st.slider("ๆๅค็ญๆธ", min_value=10, max_value=200, value=70, step=10) | |
| search_btn = st.button("๐ ้ๅงๆๅ", use_container_width=True) | |
| st.divider() | |
| st.markdown("่ณๆไพๆบ๏ผ[MOMO ่ณผ็ฉ็ถฒ](https://www.momoshop.com.tw/)") | |
| # โโ ไธปๆต็จ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if search_btn: | |
| if not keyword.strip(): | |
| st.warning("่ซ่ผธๅ ฅๆๅฐ้้ตๅญ๏ผ") | |
| st.stop() | |
| with st.spinner(f"ๆญฃๅจๆๅใ{keyword}ใ็ๅๅ่ณๆ๏ผ่ซ็จๅ..."): | |
| df = fetch_momo(keyword, max_items) | |
| if df.empty: | |
| st.error("ๆชๆๅฐไปปไฝ่ณๆ๏ผ่ซๆชขๆฅ็ถฒ่ทฏๆๆดๆ้้ตๅญใ") | |
| st.stop() | |
| # ็ตฑ่จ่ณ่จ | |
| avg_price = df["ๅนๆ ผ"].mean() | |
| max_price = df["ๅนๆ ผ"].max() | |
| min_price = df["ๅนๆ ผ"].min() | |
| st.success(f"ๅ ฑๆๅ **{len(df)}** ็ญ่ณๆ") | |
| col1, col2, col3 = st.columns(3) | |
| col1.metric("๐ฐ ๅนณๅๅนๆ ผ", f"${avg_price:,.0f}") | |
| col2.metric("๐ ๆ้ซๅนๆ ผ", f"${max_price:,.0f}") | |
| col3.metric("๐ ๆไฝๅนๆ ผ", f"${min_price:,.0f}") | |
| st.divider() | |
| # โโ ๅ่กจๅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| tab1, tab2, tab3 = st.tabs(["๐ ๆ็ทๅ", "๐ฅง ๅ้ค ๅ", "โ๏ธ ๆญๆฅๅ"]) | |
| # ---- Tab1๏ผๆ็ทๅ ---- | |
| with tab1: | |
| st.subheader(f"ใ{keyword}ใๅๅๅฎๅนๆ็ทๅ") | |
| df_line = df.copy().reset_index() | |
| df_line.columns = ["ๅๅๅบ่", "ๅๅ", "ๅนๆ ผ"] | |
| fig_line = go.Figure() | |
| fig_line.add_trace( | |
| go.Scatter( | |
| x=df_line["ๅๅๅบ่"], | |
| y=df_line["ๅนๆ ผ"], | |
| mode="lines+markers", | |
| name="ๅฎๅน", | |
| line=dict(color="#3B82F6", width=2), | |
| marker=dict(size=5), | |
| hovertext=df_line["ๅๅ"], | |
| hovertemplate="<b>%{hovertext}</b><br>ๅนๆ ผ๏ผ$%{y:,}<extra></extra>", | |
| ) | |
| ) | |
| fig_line.add_hline( | |
| y=avg_price, | |
| line_dash="dash", | |
| line_color="red", | |
| annotation_text=f"ๅนณๅ ${avg_price:,.0f}", | |
| annotation_position="bottom right", | |
| ) | |
| fig_line.update_layout( | |
| xaxis_title="ๅๅๅบ่", | |
| yaxis_title="ๅนๆ ผ (NTD)", | |
| height=500, | |
| hovermode="x unified", | |
| plot_bgcolor="rgba(0,0,0,0)", | |
| paper_bgcolor="rgba(0,0,0,0)", | |
| ) | |
| st.plotly_chart(fig_line, use_container_width=True) | |
| # ---- Tab2๏ผๅ้ค ๅ๏ผๅนๆ ผๅ้ๅๅธ๏ผ ---- | |
| with tab2: | |
| st.subheader(f"ใ{keyword}ใๅนๆ ผๅ้ๅๅธๅ้ค ๅ") | |
| bins = [0, 500, 1000, 3000, 5000, 10000, float("inf")] | |
| labels = ["โค500", "501~1000", "1001~3000", "3001~5000", "5001~10000", ">10000"] | |
| df["ๅนๆ ผๅ้"] = pd.cut(df["ๅนๆ ผ"], bins=bins, labels=labels, right=True) | |
| pie_data = df["ๅนๆ ผๅ้"].value_counts().reset_index() | |
| pie_data.columns = ["ๅนๆ ผๅ้", "ๆธ้"] | |
| fig_pie = px.pie( | |
| pie_data, | |
| names="ๅนๆ ผๅ้", | |
| values="ๆธ้", | |
| title=f"{keyword} ๅๅนๆ ผๅ้ๅๅๆธ้", | |
| hole=0.35, | |
| color_discrete_sequence=px.colors.qualitative.Pastel, | |
| ) | |
| fig_pie.update_traces(textposition="inside", textinfo="percent+label") | |
| fig_pie.update_layout( | |
| height=500, | |
| plot_bgcolor="rgba(0,0,0,0)", | |
| paper_bgcolor="rgba(0,0,0,0)", | |
| ) | |
| st.plotly_chart(fig_pie, use_container_width=True) | |
| # ---- Tab3๏ผๆญๆฅๅ๏ผๅ็ ร ๅนๆ ผๅ้๏ผ ---- | |
| with tab3: | |
| st.subheader(f"ใ{keyword}ใๅ็ ร ๅนๆ ผๅ้ๆญๆฅๅ") | |
| st.caption("ๅ็็ฑๅๅๅๅนพๅๅญๆจๆท๏ผๅ ไพๅ่ใ") | |
| # ๅพๅๅๅ N ๅญๅๅบใๅ็ใ๏ผ็ฐกๆ่ฆๅ๏ผ | |
| def extract_brand(name: str, n: int = 4) -> str: | |
| return name[:n] if len(name) >= n else name | |
| df["ๅ็"] = df["ๅๅ"].apply(extract_brand) | |
| sun_data = ( | |
| df.groupby(["ๅนๆ ผๅ้", "ๅ็"]) | |
| .size() | |
| .reset_index(name="ๆธ้") | |
| ) | |
| fig_sun = px.sunburst( | |
| sun_data, | |
| path=["ๅนๆ ผๅ้", "ๅ็"], | |
| values="ๆธ้", | |
| title=f"{keyword} ๅนๆ ผๅ้ ร ๅ็ๆญๆฅๅ", | |
| color="ๆธ้", | |
| color_continuous_scale="RdBu", | |
| ) | |
| fig_sun.update_layout( | |
| height=600, | |
| plot_bgcolor="rgba(0,0,0,0)", | |
| paper_bgcolor="rgba(0,0,0,0)", | |
| ) | |
| st.plotly_chart(fig_sun, use_container_width=True) | |
| st.divider() | |
| # โโ ๅๅง่ณๆ่กจ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with st.expander("๐ ๆฅ็ๅๅง่ณๆ", expanded=False): | |
| st.dataframe(df[["ๅๅ", "ๅนๆ ผ"]].reset_index(drop=True), use_container_width=True) | |
| # โโ ไธ่ผ CSV โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| csv = df[["ๅๅ", "ๅนๆ ผ"]].to_csv(index=False, encoding="utf-8-sig") | |
| st.download_button( | |
| label="โฌ๏ธ ไธ่ผ CSV", | |
| data=csv, | |
| file_name=f"MOMO_{keyword}.csv", | |
| mime="text/csv", | |
| ) |