tbdavid2019 commited on
Commit
d5e7585
·
1 Parent(s): 3f1433a
Files changed (2) hide show
  1. app.py +221 -116
  2. requirements.txt +2 -1
app.py CHANGED
@@ -1,26 +1,168 @@
1
  import gradio as gr
2
  import requests
 
3
  import json
4
  import os
5
  import pandas as pd
6
- import re
7
  from geopy.distance import geodesic
8
 
9
- # 7-11 和全家的 JSON 檔案
10
- seven_eleven_file = "seven_eleven_products.json"
11
- family_mart_stores_file = "family_mart_stores.json"
12
- family_mart_products_file = "family_mart_items.json"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- # 限制搜尋範圍為 3 公里
15
- MAX_DISTANCE = 3000
 
 
 
 
 
 
 
 
 
 
16
 
17
- # 讀取 JSON 檔案
18
- def load_json(filename):
19
- if os.path.exists(filename):
20
- with open(filename, "r", encoding="utf-8") as f:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  return json.load(f)
22
  return []
23
 
 
 
 
24
  def find_nearest_store(address, lat, lon):
25
  print(f"🔍 收到查詢請求: address={address}, lat={lat}, lon={lon}")
26
 
@@ -29,117 +171,69 @@ def find_nearest_store(address, lat, lon):
29
 
30
  user_coords = (lat, lon)
31
 
32
- # ========== 先處理 7-11 資料 ==========
33
- seven_df = pd.DataFrame()
34
- try:
35
- seven_data = load_json(seven_eleven_file)
36
- if not seven_data:
37
- print("⚠️ 7-11 資料是空的 (無法讀取或檔案沒有內容)")
38
- else:
39
- print("✅ 成功讀取 7-11 資料,前五筆為:")
40
- # 直接列印前五筆 raw data(list 切片)
41
- print(seven_data[:5])
42
-
43
- # 假設 7-11 JSON 每筆資料都有這些欄位:
44
- # {
45
- # "StoreName": "7-11 XXX店",
46
- # "latitude": 25.123,
47
- # "longitude": 121.456,
48
- # ...
49
- # }
50
- seven_df = pd.DataFrame(seven_data)
51
-
52
- # 若確定這些欄位名稱存在,就做經緯度轉換
53
- if {"latitude", "longitude"}.issubset(seven_df.columns):
54
- seven_df["latitude"] = seven_df["latitude"].astype(float)
55
- seven_df["longitude"] = seven_df["longitude"].astype(float)
56
- seven_df["distance_m"] = seven_df.apply(
57
- lambda row: geodesic(user_coords, (row["latitude"], row["longitude"])).meters,
58
- axis=1
59
- )
60
- else:
61
- print("⚠️ 7-11 資料裡沒有 'latitude' 或 'longitude' 欄位,無法計算距離。")
62
- seven_df = pd.DataFrame() # 直接清空,代表無法使用
63
- except Exception as e:
64
- print(f"❌ 讀取或處理 7-11 資料時發生錯誤: {e}")
65
- seven_df = pd.DataFrame()
66
 
67
- # ========== 再處理 Family 資料 ==========
68
- family_df = pd.DataFrame()
69
- try:
70
- family_data = load_json(family_mart_stores_file)
71
- if not family_data:
72
- print("⚠️ 全家資料是空的 (無法讀取或檔案沒有內容)")
73
- else:
74
- print("✅ 成功讀取 Family 資料,前五筆為:")
75
- print(family_data[:5])
76
-
77
- # 假設 Family JSON 裡的欄位是 py_wgs84 / px_wgs84 (緯度 / 經度)
78
- family_df = pd.DataFrame(family_data)
79
- if {"py_wgs84", "px_wgs84"}.issubset(family_df.columns):
80
- family_df["latitude"] = family_df["py_wgs84"].astype(float)
81
- family_df["longitude"] = family_df["px_wgs84"].astype(float)
82
- family_df["distance_m"] = family_df.apply(
83
- lambda row: geodesic(user_coords, (row["latitude"], row["longitude"])).meters,
84
- axis=1
85
- )
86
- else:
87
- print("⚠️ 全家資料裡沒有 'py_wgs84' 或 'px_wgs84' 欄位,無法計算距離。")
88
- family_df = pd.DataFrame()
89
- except Exception as e:
90
- print(f"❌ 讀取或處理 Family 資料時發生錯誤: {e}")
91
- family_df = pd.DataFrame()
92
 
93
- # ========== 篩選 3 公里內最近的店家 ==========
94
- # 7-11
95
- nearby_seven = pd.DataFrame()
96
- if not seven_df.empty and "distance_m" in seven_df.columns:
97
- nearby_seven = seven_df[seven_df["distance_m"] <= MAX_DISTANCE].sort_values(by="distance_m").head(5)
98
 
99
- # 全家
100
- nearby_family = pd.DataFrame()
101
- if not family_df.empty and "distance_m" in family_df.columns:
102
- nearby_family = family_df[family_df["distance_m"] <= MAX_DISTANCE].sort_values(by="distance_m").head(5)
 
 
 
 
103
 
104
- if nearby_seven.empty and nearby_family.empty:
105
- return [["❌ 附近 3 公里內沒有便利商店", "", "", "", ""]]
106
 
107
- # ========== 整理成表格輸出 ==========
108
  output = []
 
 
 
109
 
110
- # 7-11 結果
111
- if not nearby_seven.empty:
112
- for _, row in nearby_seven.iterrows():
113
- store_name = row.get("StoreName", "7-11 未提供店名")
114
- dist = f"{row['distance_m']:.2f} m"
115
- output.append([
116
- store_name,
117
- dist,
118
- "7-11 商品(示意)",
119
- "5" # 這裡只是示範
120
- ])
121
- # 全家 結果
122
- if not nearby_family.empty:
123
- for _, row in nearby_family.iterrows():
124
- store_name = row.get("Name", "全家 未提供店名")
125
- dist = f"{row['distance_m']:.2f} m"
126
- output.append([
127
- store_name,
128
- dist,
129
- "全家 商品(示意)",
130
- "5" # 這裡只是示範
131
- ])
132
 
133
  return output
134
 
135
- # Gradio UI
 
 
136
  with gr.Blocks() as demo:
137
- gr.Markdown("## 便利商店門市與商品搜尋")
138
- gr.Markdown("輸入 GPS 座標來搜尋最近的便利商店與推薦商品")
139
 
140
- address = gr.Textbox(label="輸入地址或留空以使用 GPS")
141
- lat = gr.Number(label="GPS 緯度 (可選)", value=0, elem_id="lat")
142
- lon = gr.Number(label="GPS 經度 (可選)", value=0, elem_id="lon")
143
 
144
  with gr.Row():
145
  gps_button = gr.Button("📍 使用目前位置", elem_id="gps-btn")
@@ -161,17 +255,16 @@ with gr.Blocks() as demo:
161
  return new Promise((resolve) => {
162
  if (!navigator.geolocation) {
163
  alert("您的瀏覽器不支援地理位置功能");
164
- resolve([0, 0]); // 回傳 [0,0] 避免錯誤
165
  return;
166
  }
167
-
168
  navigator.geolocation.getCurrentPosition(
169
  (position) => {
170
  resolve([position.coords.latitude, position.coords.longitude]);
171
  },
172
  (error) => {
173
- alert("無法獲取位置:" + error.message);
174
- resolve([0, 0]); // GPS 失敗時回傳 [0,0]
175
  }
176
  );
177
  });
@@ -179,5 +272,17 @@ with gr.Blocks() as demo:
179
  """
180
  )
181
 
182
- # 在 launch 時加上 debug=True 也可以幫助觀察更多 log 資訊
183
- demo.launch(share=True, debug=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import requests
3
+ import re
4
  import json
5
  import os
6
  import pandas as pd
7
+ from xml.etree import ElementTree
8
  from geopy.distance import geodesic
9
 
10
+ # =============== 檔案路徑設定 (你可依需要修改) ===============
11
+ DATA_DIR = "docs/assets" # 或 "./data" 等
12
+ os.makedirs(DATA_DIR, exist_ok=True)
13
+
14
+ SEVEN_ELEVEN_PRODUCTS_FILE = os.path.join(DATA_DIR, "seven_eleven_products.json")
15
+ FAMILY_MART_STORES_FILE = os.path.join(DATA_DIR, "family_mart_stores.json")
16
+ FAMILY_MART_PRODUCTS_FILE = os.path.join(DATA_DIR, "family_mart_products.json")
17
+
18
+ # 3 公里範圍
19
+ MAX_DISTANCE = 3000
20
+
21
+ # -----------------------------------------------------------
22
+ # 1. 下載或更新 7-11 商品資料
23
+ # -----------------------------------------------------------
24
+ def fetch_seven_eleven_products(force_update=False):
25
+ """
26
+ 從 https://www.7-11.com.tw/freshfoods/Read_Food_xml_hot.aspx
27
+ 以各種 category 抓取商品資料(XML),轉成 JSON 存檔。
28
+ force_update=True 時,強制重新抓取。
29
+ """
30
+ if os.path.exists(SEVEN_ELEVEN_PRODUCTS_FILE) and not force_update:
31
+ print("7-11 商品 JSON 已存在,跳過下載 (如要強制更新請設 force_update=True)")
32
+ return
33
+
34
+ base_url = "https://www.7-11.com.tw/freshfoods/Read_Food_xml_hot.aspx"
35
+ headers = {
36
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
37
+ }
38
+
39
+ categories = [
40
+ "19_star", "1_Ricerolls", "16_sandwich", "2_Light", "3_Cuisine",
41
+ "4_Snacks", "5_ForeignDishes", "6_Noodles", "7_Oden", "8_Bigbite",
42
+ "9_Icecream", "10_Slurpee", "11_bread", "hot", "12_steam",
43
+ "13_luwei", "15_health", "17_ohlala", "18_veg", "20_panini", "21_ice", "22_ice"
44
+ ]
45
+
46
+ data_list = []
47
+
48
+ # 按照分類依序爬取
49
+ for index, cat in enumerate(categories):
50
+ # 注意:實際參數可能需要你自行測試
51
+ params = {"": index}
52
+ try:
53
+ resp = requests.get(base_url, headers=headers, params=params, timeout=10)
54
+ if resp.status_code == 200:
55
+ try:
56
+ root = ElementTree.fromstring(resp.content)
57
+ # 解析 XML
58
+ for item in root.findall(".//Item"):
59
+ data_list.append({
60
+ "category": cat,
61
+ "name": item.findtext("name", ""),
62
+ "kcal": item.findtext("kcal", ""),
63
+ "price": item.findtext("price", ""),
64
+ "image": f'https://www.7-11.com.tw/freshfoods/{cat}/' + item.findtext("image", ""),
65
+ "special_sale": item.findtext("special_sale", ""),
66
+ "new": item.findtext("new", ""),
67
+ "content": item.findtext("content", ""),
68
+ })
69
+ except ElementTree.ParseError:
70
+ print(f"分類 {cat} 返回非 XML 格式資料,略過。")
71
+ else:
72
+ print(f"分類 {cat} 請求失敗,HTTP 狀態碼: {resp.status_code}")
73
+ except Exception as e:
74
+ print(f"分類 {cat} 請求錯誤: {e}")
75
+
76
+ # 儲存到 JSON
77
+ with open(SEVEN_ELEVEN_PRODUCTS_FILE, "w", encoding="utf-8") as jf:
78
+ json.dump(data_list, jf, ensure_ascii=False, indent=4)
79
+
80
+ print(f"✅ 7-11 商品資料抓取完成,共 {len(data_list)} 筆,已存為 {SEVEN_ELEVEN_PRODUCTS_FILE}")
81
 
82
+ # -----------------------------------------------------------
83
+ # 2. 下載或更新 全家門市資料
84
+ # -----------------------------------------------------------
85
+ def fetch_family_stores(force_update=False):
86
+ """
87
+ 從 https://family.map.com.tw/famiport/api/dropdownlist/Select_StoreName
88
+ 下載所有全家門市資料(含經緯度 py_wgs84, px_wgs84)並存檔。
89
+ force_update=True 時,強制重新抓取。
90
+ """
91
+ if os.path.exists(FAMILY_MART_STORES_FILE) and not force_update:
92
+ print("全家門市 JSON 已存在,跳過下載 (如要強制更新請設 force_update=True)")
93
+ return
94
 
95
+ url = "https://family.map.com.tw/famiport/api/dropdownlist/Select_StoreName"
96
+ post_data = {"store": ""}
97
+ try:
98
+ resp = requests.post(url, json=post_data, timeout=10)
99
+ if resp.status_code == 200:
100
+ data = resp.json()
101
+ with open(FAMILY_MART_STORES_FILE, "w", encoding="utf-8") as f:
102
+ json.dump(data, f, ensure_ascii=False, indent=4)
103
+ print(f"✅ 全家門市資料抓取完成,共 {len(data)} 筆,已存為 {FAMILY_MART_STORES_FILE}")
104
+ else:
105
+ print(f"❌ 全家門市 API 請求失敗,HTTP 狀態碼: {resp.status_code}")
106
+ except Exception as e:
107
+ print(f"❌ 全家門市 API 請求錯誤: {e}")
108
+
109
+ # -----------------------------------------------------------
110
+ # 3. 下載或更新 全家商品資料
111
+ # -----------------------------------------------------------
112
+ def fetch_family_products(force_update=False):
113
+ """
114
+ 從 https://famihealth.family.com.tw/Calculator 解析網頁 JS 中的
115
+ var categories = [...] 取得商品清單。
116
+ force_update=True 時,強制重新抓取。
117
+ """
118
+ if os.path.exists(FAMILY_MART_PRODUCTS_FILE) and not force_update:
119
+ print("全家商品 JSON 已存在,跳過下載 (如要強制更新請設 force_update=True)")
120
+ return
121
+
122
+ url = "https://famihealth.family.com.tw/Calculator"
123
+ headers = {"User-Agent": "Mozilla/5.0"}
124
+ try:
125
+ resp = requests.get(url, headers=headers, timeout=10)
126
+ if resp.status_code == 200:
127
+ match = re.search(r'var categories = (\[.*?\]);', resp.text, re.S)
128
+ if match:
129
+ categories_data = json.loads(match.group(1))
130
+ results = []
131
+ for cat in categories_data:
132
+ cat_name = cat.get("name", "")
133
+ for product in cat.get("products", []):
134
+ results.append({
135
+ "category": cat_name,
136
+ "title": product.get("name"),
137
+ "picture_url": product.get("imgurl"),
138
+ "protein": product.get("protein", 0),
139
+ "carb": product.get("carb", 0),
140
+ "calories": product.get("calo", 0),
141
+ "fat": product.get("fat", 0),
142
+ "description": product.get("description", ""),
143
+ })
144
+ with open(FAMILY_MART_PRODUCTS_FILE, "w", encoding="utf-8") as jf:
145
+ json.dump(results, jf, ensure_ascii=False, indent=4)
146
+ print(f"✅ 全家商品資料抓取完成,共 {len(results)} 筆,已存為 {FAMILY_MART_PRODUCTS_FILE}")
147
+ else:
148
+ print("❌ 找不到 var categories = ... 之內容,無法解析全家商品。")
149
+ else:
150
+ print(f"❌ 全家商品頁面請求失敗,HTTP 狀態碼: {resp.status_code}")
151
+ except Exception as e:
152
+ print(f"❌ 全家商品頁面請求錯誤: {e}")
153
+
154
+ # -----------------------------------------------------------
155
+ # 工具:讀取 JSON 檔
156
+ # -----------------------------------------------------------
157
+ def load_json(path):
158
+ if os.path.exists(path):
159
+ with open(path, "r", encoding="utf-8") as f:
160
  return json.load(f)
161
  return []
162
 
163
+ # -----------------------------------------------------------
164
+ # 4. 主邏輯:依使用者座標,篩選店家並顯示商品
165
+ # -----------------------------------------------------------
166
  def find_nearest_store(address, lat, lon):
167
  print(f"🔍 收到查詢請求: address={address}, lat={lat}, lon={lon}")
168
 
 
171
 
172
  user_coords = (lat, lon)
173
 
174
+ # 讀取 7-11 商品(注意:目前沒有 7-11「店家」經緯度,無法比對)
175
+ seven_products = load_json(SEVEN_ELEVEN_PRODUCTS_FILE)
176
+ print(f"7-11 商品總數: {len(seven_products)} (但沒有���市座標)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
+ # 讀取全家店家與商品
179
+ family_stores = load_json(FAMILY_MART_STORES_FILE)
180
+ family_products = load_json(FAMILY_MART_PRODUCTS_FILE)
181
+
182
+ # 全家店家轉 DataFrame
183
+ family_df = pd.DataFrame(family_stores)
184
+ # 確認欄位
185
+ if not {"py_wgs84", "px_wgs84"}.issubset(family_df.columns):
186
+ return [["❌ 全家資料中沒有 py_wgs84, px_wgs84 欄位,無法計算距離", "", "", "", ""]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
+ # 轉換經緯度
189
+ family_df["latitude"] = family_df["py_wgs84"].astype(float)
190
+ family_df["longitude"] = family_df["px_wgs84"].astype(float)
 
 
191
 
192
+ # 計算距離
193
+ family_df["distance_m"] = family_df.apply(
194
+ lambda row: geodesic(user_coords, (row["latitude"], row["longitude"])).meters,
195
+ axis=1
196
+ )
197
+
198
+ # 篩選 3 公里內最近的店家
199
+ nearby_family = family_df[family_df["distance_m"] <= MAX_DISTANCE].sort_values("distance_m").head(5)
200
 
201
+ if nearby_family.empty:
202
+ return [["❌ 附近 3 公里內沒有便利商店 (目前只顯示全家)", "", "", "", ""]]
203
 
204
+ # 整理輸出
205
  output = []
206
+ for _, row in nearby_family.iterrows():
207
+ store_name = row.get("Name", "全家 未提供店名")
208
+ dist_str = f"{row['distance_m']:.2f} m"
209
 
210
+ # 這裡僅示範把「全家商品」隨機帶一兩項進來
211
+ # 若你想顯示「所有商品」或「即期品」,就自行加邏輯
212
+ # 例如只顯示 calories < 300 或特定關鍵字 ...
213
+ # 這裡簡化只示範抓前 1 筆做展示
214
+ item_title = ""
215
+ if len(family_products) > 0:
216
+ item_title = family_products[0]["title"] # 示範取第 1 筆
217
+
218
+ output.append([
219
+ store_name, # 門市
220
+ dist_str, # 距離
221
+ item_title, # 食物
222
+ "1" # 數量(示範)
223
+ ])
 
 
 
 
 
 
 
 
224
 
225
  return output
226
 
227
+ # -----------------------------------------------------------
228
+ # 5. 建立 Gradio 介面
229
+ # -----------------------------------------------------------
230
  with gr.Blocks() as demo:
231
+ gr.Markdown("## 便利商店門市與商品搜尋 (示範)")
232
+ gr.Markdown("1. 按下「使用目前位置」或自行輸入緯度/經度\n2. 點選「搜尋」查詢 3 公里內的門市")
233
 
234
+ address = gr.Textbox(label="輸入地址(可留空)")
235
+ lat = gr.Number(label="GPS 緯度", value=0, elem_id="lat")
236
+ lon = gr.Number(label="GPS 經度", value=0, elem_id="lon")
237
 
238
  with gr.Row():
239
  gps_button = gr.Button("📍 使用目前位置", elem_id="gps-btn")
 
255
  return new Promise((resolve) => {
256
  if (!navigator.geolocation) {
257
  alert("您的瀏覽器不支援地理位置功能");
258
+ resolve([0, 0]);
259
  return;
260
  }
 
261
  navigator.geolocation.getCurrentPosition(
262
  (position) => {
263
  resolve([position.coords.latitude, position.coords.longitude]);
264
  },
265
  (error) => {
266
+ alert("無法取得位置:" + error.message);
267
+ resolve([0, 0]);
268
  }
269
  );
270
  });
 
272
  """
273
  )
274
 
275
+ def main():
276
+ """
277
+ 主程式入口,可在本地端執行 python 檔案時呼叫此函式,
278
+ 先下載/更新資料,再啟動 Gradio。
279
+ """
280
+ # 下載 / 更新 所有資料
281
+ fetch_seven_eleven_products(force_update=False)
282
+ fetch_family_stores(force_update=False)
283
+ fetch_family_products(force_update=False)
284
+
285
+ demo.launch(server_name="0.0.0.0", server_port=7860, debug=True)
286
+
287
+ if __name__ == "__main__":
288
+ main()
requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
  gradio
2
  pandas
3
  geopy
4
- requests
 
 
1
  gradio
2
  pandas
3
  geopy
4
+ requests
5
+ lxml