MOMO_streamlit / app.py
Angelguava's picture
Create app.py
de6c369 verified
Raw
History Blame Contribute Delete
11.1 kB
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๏ผ‰ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@st.cache_resource
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",
)