跳轉到

水果營養查詢Fruityvice

基於gradio實作水果營養查詢器

import requests
import gradio as gr


### API 設定
BASE_URL = "https://www.fruityvice.com/api/fruit"


### 查詢水果資料
def get_fruit_info(fruit_name: str):
    """
    根據使用者輸入的水果英文名稱,
    向 Fruityvice API 查詢水果與營養資料。
    """

    fruit_name = fruit_name.strip().lower() # 移除前後空白並統一轉成小寫

    if not fruit_name:
        return "請輸入水果英文名稱", "", "", "", "", "", "", "", ""

    try:

        response = requests.get(
            f"{BASE_URL}/{fruit_name}",
            timeout=10
        ) # 向 Fruityvice API 發送 GET Request


        if response.status_code == 404:
            return "找不到這個水果", "", "", "", "", "", "", "", ""


        response.raise_for_status() # HTTP 狀態碼不是成功狀態時產生例外

        data = response.json() # 將 JSON Response 轉成 Python dict


        ### 取得水果基本資料
        name = data["name"]
        family = data["family"]
        genus = data["genus"]
        order = data["order"]


        ### 取得水果營養資料
        nutritions = data["nutritions"]

        calories = nutritions["calories"]
        fat = nutritions["fat"]
        sugar = nutritions["sugar"]
        carbohydrates = nutritions["carbohydrates"]
        protein = nutritions["protein"]


        return (
            name,
            family,
            genus,
            order,
            calories,
            fat,
            sugar,
            carbohydrates,
            protein
        )


    except requests.RequestException as error:

        return (
            f"API 連線失敗:{error}",
            "",
            "",
            "",
            "",
            "",
            "",
            "",
            ""
        )


    except Exception as error:

        return (
            f"程式發生錯誤:{error}",
            "",
            "",
            "",
            "",
            "",
            "",
            "",
            ""
        )


### 建立 Gradio 介面
with gr.Blocks(title="水果營養查詢器") as demo:

    gr.Markdown(
        """
        # 🍎 Fruityvice 水果營養查詢器

        輸入水果的英文名稱,
        即可查詢水果分類與營養資訊。

        例如:`apple`、`banana`、`strawberry`、`orange`
        """
    )


    ### 水果名稱輸入
    fruit_input = gr.Textbox(
        label="水果英文名稱",
        placeholder="例如 apple",
        value="apple"
    )


    ### 查詢按鈕
    search_button = gr.Button(
        "🔍 查詢水果",
        variant="primary"
    )


    ### 水果基本資料
    gr.Markdown("## 🍎 水果基本資料")

    with gr.Row():

        name_output = gr.Textbox(
            label="水果名稱"
        )

        family_output = gr.Textbox(
            label="科 Family"
        )


    with gr.Row():

        genus_output = gr.Textbox(
            label="屬 Genus"
        )

        order_output = gr.Textbox(
            label="目 Order"
        )


    ### 水果營養資料
    gr.Markdown("## 🥗 營養資訊")

    with gr.Row():

        calories_output = gr.Number(
            label="熱量 Calories"
        )

        sugar_output = gr.Number(
            label="糖分 Sugar"
        )

        carbohydrates_output = gr.Number(
            label="碳水化合物 Carbohydrates"
        )


    with gr.Row():

        protein_output = gr.Number(
            label="蛋白質 Protein"
        )

        fat_output = gr.Number(
            label="脂肪 Fat"
        )


    ### 按下按鈕後執行水果查詢
    search_button.click(
        fn=get_fruit_info,
        inputs=fruit_input,
        outputs=[
            name_output,
            family_output,
            genus_output,
            order_output,
            calories_output,
            fat_output,
            sugar_output,
            carbohydrates_output,
            protein_output
        ]
    )


    ### 按 Enter 也可以直接查詢
    fruit_input.submit(
        fn=get_fruit_info,
        inputs=fruit_input,
        outputs=[
            name_output,
            family_output,
            genus_output,
            order_output,
            calories_output,
            fat_output,
            sugar_output,
            carbohydrates_output,
            protein_output
        ]
    )


### 啟動 Gradio
demo.launch()

基於Streamlit實作Fruityvice 水果營養分析儀表板

import requests
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st


### API 設定
BASE_URL = "https://www.fruityvice.com/api/fruit"


### Streamlit 頁面設定
st.set_page_config(
    page_title="Fruityvice 水果營養分析儀表板", # 瀏覽器分頁標題
    page_icon="🍎", # 瀏覽器分頁圖示
    layout="wide" # 使用寬版頁面
)


### 自訂 CSS 樣式
st.markdown(
    """
    <style>
    .main-title {
        font-size: 46px;
        font-weight: 900;
        margin-bottom: 0px;
    }

    .subtitle {
        font-size: 18px;
        color: #8a8a8a;
        margin-bottom: 24px;
    }

    .fruit-card {
        padding: 24px;
        border-radius: 24px;
        background: linear-gradient(135deg, #fff5dc, #ffffff);
        box-shadow: 0 8px 30px rgba(0,0,0,0.08);
        border: 1px solid rgba(0,0,0,0.06);
        color: #2b2b2b; # 固定卡片內主要文字顏色
    }

    .fruit-name {
        font-size: 36px;
        font-weight: 900;
        color: #ff7a00;
        margin-bottom: 18px;
    }

    .fruit-card p {
        color: #333333;
        font-size: 16px;
        line-height: 1.8;
        margin: 8px 0px;
    }

    .fruit-card b {
        color: #111111;
        font-weight: 800;
    }

    .small-note {
        color: #666666;
        font-size: 14px;
        margin-top: 16px;
    }
    </style>
    """,
    unsafe_allow_html=True
)


### 建立 API GET Request 函式
def get_json(url: str):
    """
    向指定 API 網址發送 GET Request,
    並將回傳的 JSON 資料轉成 Python 物件。
    """

    response = requests.get(
        url,
        timeout=10 # API 最多等待 10 秒
    )

    if response.status_code == 404:
        raise ValueError("找不到水果資料")

    response.raise_for_status() # HTTP 狀態碼不是成功狀態時產生例外

    return response.json() # 將 JSON Response 轉成 Python 物件


### 取得所有水果資料
@st.cache_data(show_spinner=False)
def get_all_fruits() -> list[dict]:
    """
    取得 Fruityvice 所有水果資料。

    使用 Streamlit cache,
    避免每次重新執行程式都重新呼叫 API。
    """

    return get_json(
        f"{BASE_URL}/all"
    )


### 取得指定水果資料
@st.cache_data(show_spinner=False)
def get_fruit(fruit_name: str) -> dict:
    """
    根據水果英文名稱,
    取得指定水果的完整資料。
    """

    fruit_name = fruit_name.strip().lower() # 移除前後空白並轉成小寫

    return get_json(
        f"{BASE_URL}/{fruit_name}"
    )


### 建立所有水果 DataFrame
def build_fruits_df(fruits: list[dict]) -> pd.DataFrame:
    """
    將 Fruityvice 回傳的水果 JSON,
    整理成 Pandas DataFrame。
    """

    rows = []

    for fruit in fruits:

        nutrition = fruit["nutritions"] # 取得目前水果的營養資料

        rows.append(
            {
                "水果": fruit["name"],
                "科": fruit["family"],
                "屬": fruit["genus"],
                "目": fruit["order"],
                "熱量": nutrition["calories"],
                "脂肪": nutrition["fat"],
                "糖分": nutrition["sugar"],
                "碳水化合物": nutrition["carbohydrates"],
                "蛋白質": nutrition["protein"]
            }
        )

    return pd.DataFrame(rows)


### 建立單一水果營養 DataFrame
def build_nutrition_df(fruit: dict) -> pd.DataFrame:
    """
    將單一水果的營養資訊,
    整理成適合製作圖表的 DataFrame。
    """

    nutrition = fruit["nutritions"]

    data = {
        "營養素": [
            "脂肪",
            "糖分",
            "碳水化合物",
            "蛋白質"
        ],
        "數值": [
            nutrition["fat"],
            nutrition["sugar"],
            nutrition["carbohydrates"],
            nutrition["protein"]
        ]
    }

    return pd.DataFrame(data)


### 建立營養長條圖
def render_bar_chart(nutrition_df: pd.DataFrame) -> None:
    """
    使用 Plotly 建立水果營養長條圖。
    """

    fig = px.bar(
        nutrition_df,
        x="營養素",
        y="數值",
        text="數值",
        color="營養素",
        title="營養成分比較"
    )

    fig.update_traces(
        textposition="outside" # 將數值顯示在長條上方
    )

    fig.update_layout(
        height=430,
        showlegend=False
    )

    st.plotly_chart(
        fig,
        width="stretch"
    )


### 建立營養雷達圖
def render_radar_chart(
    nutrition_df: pd.DataFrame,
    fruit_name: str
) -> None:
    """
    使用 Plotly 建立水果營養雷達圖。
    """

    fig = go.Figure()

    fig.add_trace(
        go.Scatterpolar(
            r=nutrition_df["數值"],
            theta=nutrition_df["營養素"],
            fill="toself",
            name=fruit_name
        )
    )

    max_value = max(
        20,
        float(
            nutrition_df["數值"].max()
        ) * 1.2
    ) # 雷達圖最大刻度稍微高於最高營養值

    fig.update_layout(
        polar=dict(
            radialaxis=dict(
                visible=True,
                range=[
                    0,
                    max_value
                ]
            )
        ),
        showlegend=False,
        height=430
    )

    st.plotly_chart(
        fig,
        width="stretch"
    )


### 顯示網站標題
st.markdown(
    "<div class='main-title'>🍎 Fruityvice 水果營養分析儀表板</div>",
    unsafe_allow_html=True
)

st.markdown(
    """
    <div class='subtitle'>
    查詢水果營養資訊、比較營養成分,並分析不同水果的營養差異。
    </div>
    """,
    unsafe_allow_html=True
)


### 主程式
try:

    ### 取得所有水果資料
    with st.spinner(
        "正在載入水果資料..."
    ):

        fruits = get_all_fruits()

        fruits_df = build_fruits_df(
            fruits
        )


    ### 建立水果名稱清單
    fruit_names = sorted(
        [
            fruit["name"]
            for fruit in fruits
        ]
    )


    ### Sidebar 查詢設定
    with st.sidebar:

        st.header(
            "🍎 查詢設定"
        )


        ### 選擇水果
        selected_fruit = st.selectbox(
            "選擇水果",
            fruit_names,
            index=fruit_names.index("Apple")
            if "Apple" in fruit_names
            else 0
        )


        ### 搜尋水果名稱
        keyword = st.text_input(
            "或輸入水果英文名稱",
            placeholder="例如 apple、banana、orange"
        )


        search_button = st.button(
            "🔍 開始查詢",
            type="primary",
            width="stretch"
        )


        st.divider()


        ### 排行榜設定
        ranking_type = st.selectbox(
            "排行榜依據",
            [
                "熱量",
                "糖分",
                "碳水化合物",
                "蛋白質",
                "脂肪"
            ]
        )


        ranking_count = st.slider(
            "排行榜顯示數量",
            min_value=5,
            max_value=20,
            value=10
        )


        st.caption(
            "營養資料皆以每 100g 水果為基準。"
        )


    ### 建立 Session State
    if "fruit_name" not in st.session_state:
        st.session_state.fruit_name = selected_fruit


    ### 按下查詢按鈕後更新水果名稱
    if search_button:

        if keyword.strip():

            st.session_state.fruit_name = (
                keyword.strip()
            )

        else:

            st.session_state.fruit_name = (
                selected_fruit
            )


    ### 下拉選單改變時使用目前選擇的水果
    if not search_button and not keyword.strip():

        st.session_state.fruit_name = (
            selected_fruit
        )


    fruit_name = (
        st.session_state.fruit_name
    )


    ### 查詢目前選擇的水果
    fruit = get_fruit(
        fruit_name
    )


    ### 取得水果基本資料
    name = fruit["name"]
    family = fruit["family"]
    genus = fruit["genus"]
    order = fruit["order"]
    fruit_id = fruit["id"]

    nutrition = fruit["nutritions"]


    ### 建立營養 DataFrame
    nutrition_df = build_nutrition_df(
        fruit
    )


    ### 顯示水果基本資料
    st.markdown(
        f"""
        <div class="fruit-card">
            <div class="fruit-name">🍊 {name}</div>
            <p><b>ID:</b>{fruit_id}</p>
            <p><b>科 Family:</b>{family}</p>
            <p><b>屬 Genus:</b>{genus}</p>
            <p><b>目 Order:</b>{order}</p>
            <div class="small-note">
                以下營養數值以每 100g 水果為基準
            </div>
        </div>
        """,
        unsafe_allow_html=True
    )


    st.write("")


    ### 顯示營養指標
    metric_col1, metric_col2, metric_col3, metric_col4, metric_col5 = (
        st.columns(5)
    )


    metric_col1.metric(
        "🔥 熱量",
        f"{nutrition['calories']} kcal"
    )


    metric_col2.metric(
        "🍬 糖分",
        f"{nutrition['sugar']} g"
    )


    metric_col3.metric(
        "🍞 碳水化合物",
        f"{nutrition['carbohydrates']} g"
    )


    metric_col4.metric(
        "💪 蛋白質",
        f"{nutrition['protein']} g"
    )


    metric_col5.metric(
        "🥑 脂肪",
        f"{nutrition['fat']} g"
    )


    st.divider()


    ### 建立功能分頁
    tab1, tab2, tab3, tab4 = st.tabs(
        [
            "📊 營養分析",
            "🏆 水果排行榜",
            "📋 水果資料庫",
            "🔎 原始資料"
        ]
    )


    ### Tab 1:營養分析
    with tab1:

        st.subheader(
            f"📊 {name} 營養分析"
        )


        chart_col1, chart_col2 = st.columns(2)


        ### 左側顯示長條圖
        with chart_col1:

            render_bar_chart(
                nutrition_df
            )


        ### 右側顯示雷達圖
        with chart_col2:

            render_radar_chart(
                nutrition_df,
                name
            )


        ### 計算最高營養成分
        highest_nutrition = (
            nutrition_df
            .sort_values(
                "數值",
                ascending=False
            )
            .iloc[0]
        )


        ### 顯示分析摘要
        st.info(
            f"{name} 在脂肪、糖分、碳水化合物、蛋白質之中,"
            f"數值最高的是「{highest_nutrition['營養素']}」,"
            f"數值為 {highest_nutrition['數值']} g。"
        )


    ### Tab 2:水果排行榜
    with tab2:

        st.subheader(
            f"🏆 {ranking_type}排行榜"
        )


        ### 根據選擇的營養素進行排序
        ranking_df = (
            fruits_df
            .sort_values(
                ranking_type,
                ascending=False
            )
            .head(
                ranking_count
            )
        )


        ### 建立排行榜長條圖
        ranking_fig = px.bar(
            ranking_df,
            x="水果",
            y=ranking_type,
            color=ranking_type,
            text=ranking_type,
            title=f"水果 {ranking_type} Top {ranking_count}"
        )


        ranking_fig.update_traces(
            textposition="outside"
        )


        ranking_fig.update_layout(
            height=500
        )


        st.plotly_chart(
            ranking_fig,
            width="stretch"
        )


        ### 顯示排行榜資料
        st.dataframe(
            ranking_df[
                [
                    "水果",
                    ranking_type
                ]
            ],
            width="stretch",
            hide_index=True
        )


    ### Tab 3:水果資料庫
    with tab3:

        st.subheader(
            "📋 Fruityvice 水果資料庫"
        )


        ### 顯示所有水果資料
        st.dataframe(
            fruits_df,
            width="stretch",
            height=520,
            hide_index=True
        )


        ### 將 DataFrame 轉成 CSV
        csv_data = fruits_df.to_csv(
            index=False
        ).encode(
            "utf-8-sig" # 避免 Excel 開啟中文時出現亂碼
        )


        ### 建立 CSV 下載按鈕
        st.download_button(
            label="📥 下載水果營養資料 CSV",
            data=csv_data,
            file_name="fruityvice_fruits.csv",
            mime="text/csv"
        )


    ### Tab 4:原始資料
    with tab4:

        st.subheader(
            f"🔎 {name} 原始 JSON 資料"
        )


        ### 顯示 API 原始 JSON
        st.json(
            fruit
        )


### 找不到水果
except ValueError:

    st.error(
        "找不到這個水果。"
        "請輸入英文名稱,例如 apple、banana、orange。"
    )


### API 連線錯誤
except requests.RequestException as error:

    st.error(
        f"API 連線失敗:{error}"
    )


### 其他程式錯誤
except Exception as error:

    st.error(
        f"程式發生錯誤:{error}"
    )