Spaces:
Running
Running
Commit
·
0b75bed
1
Parent(s):
8260f15
提示地址可留白
Browse files
README.md
CHANGED
@@ -25,7 +25,7 @@ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-
|
|
25 |
> 3. 本專案僅供研究教學之用,請勿用於商業或非法目的。
|
26 |
|
27 |
## 功能
|
28 |
-
- 以 GPS
|
29 |
- 可自訂搜尋範圍(3 / 5 / 7 / 13 / 21 公里)。
|
30 |
- 顯示每間門市的即期食品清單與剩餘數量。
|
31 |
|
@@ -33,7 +33,7 @@ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-
|
|
33 |
1. 安裝 Python 3.8+ 及套件:
|
34 |
```bash
|
35 |
pip install gradio requests pandas geopy
|
36 |
-
|
37 |
|
38 |
2. 執行:
|
39 |
|
@@ -45,8 +45,11 @@ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-
|
|
45 |
|
46 |
|
47 |
注意事項
|
48 |
-
|
49 |
-
|
|
|
|
|
|
|
50 |
|
51 |
---
|
52 |
Convenience Store Expiring-Food Query (7-11 / FamilyMart)
|
@@ -59,14 +62,14 @@ Note:
|
|
59 |
3. This project is for educational and research purposes only. Please do not use it for commercial or illegal purposes.
|
60 |
|
61 |
Features
|
62 |
-
• Search nearby 7-11 / FamilyMart stores by GPS coordinates.
|
63 |
• Customizable search radius (3 / 5 / 7 / 13 / 21 km).
|
64 |
• Display each store’s expiring-food items and remaining quantity.
|
65 |
|
66 |
Usage
|
67 |
1. Install Python 3.8+ and dependencies:
|
68 |
```
|
69 |
-
pip install gradio requests pandas
|
70 |
```
|
71 |
|
72 |
2. Run:
|
|
|
25 |
> 3. 本專案僅供研究教學之用,請勿用於商業或非法目的。
|
26 |
|
27 |
## 功能
|
28 |
+
- 以 GPS 座標或地址(自動轉換為經緯度,支援 Google Geocoding API)搜尋附近的 7-11 / 全家門市。
|
29 |
- 可自訂搜尋範圍(3 / 5 / 7 / 13 / 21 公里)。
|
30 |
- 顯示每間門市的即期食品清單與剩餘數量。
|
31 |
|
|
|
33 |
1. 安裝 Python 3.8+ 及套件:
|
34 |
```bash
|
35 |
pip install gradio requests pandas geopy
|
36 |
+
```
|
37 |
|
38 |
2. 執行:
|
39 |
|
|
|
45 |
|
46 |
|
47 |
注意事項
|
48 |
+
- 此為個人練習與技術示範,非官方專案。
|
49 |
+
- 若出現「憑證過期」或「Token 失敗」等訊息,表示 MID_V 失效,需要更新。
|
50 |
+
- 地址查詢需設定 Google Geocoding API 金鑰於環境變數 `googlekey`(Huggingface Space Secrets)。
|
51 |
+
|
52 |
+
- For address search, set your Google Geocoding API key in the environment variable `googlekey` (Huggingface Space Secrets).
|
53 |
|
54 |
---
|
55 |
Convenience Store Expiring-Food Query (7-11 / FamilyMart)
|
|
|
62 |
3. This project is for educational and research purposes only. Please do not use it for commercial or illegal purposes.
|
63 |
|
64 |
Features
|
65 |
+
• Search nearby 7-11 / FamilyMart stores by GPS coordinates or address (auto geocoding via Google API).
|
66 |
• Customizable search radius (3 / 5 / 7 / 13 / 21 km).
|
67 |
• Display each store’s expiring-food items and remaining quantity.
|
68 |
|
69 |
Usage
|
70 |
1. Install Python 3.8+ and dependencies:
|
71 |
```
|
72 |
+
pip install gradio requests pandas
|
73 |
```
|
74 |
|
75 |
2. Run:
|
app.py
CHANGED
@@ -215,7 +215,7 @@ def main():
|
|
215 |
3. 意見反應 telegram @a7a8a9abc
|
216 |
""")
|
217 |
|
218 |
-
address = gr.Textbox(label="
|
219 |
lat = gr.Number(label="GPS 緯度", value=0, elem_id="lat")
|
220 |
lon = gr.Number(label="GPS 經度", value=0, elem_id="lon")
|
221 |
|
|
|
215 |
3. 意見反應 telegram @a7a8a9abc
|
216 |
""")
|
217 |
|
218 |
+
address = gr.Textbox(label="地址(可留空)", placeholder="可留空白,通常不用填")
|
219 |
lat = gr.Number(label="GPS 緯度", value=0, elem_id="lat")
|
220 |
lon = gr.Number(label="GPS 經度", value=0, elem_id="lon")
|
221 |
|
app3.py
DELETED
@@ -1,255 +0,0 @@
|
|
1 |
-
import gradio as gr
|
2 |
-
import requests
|
3 |
-
import json
|
4 |
-
import os
|
5 |
-
import pandas as pd
|
6 |
-
from geopy.distance import geodesic
|
7 |
-
|
8 |
-
# =============== 7-11 所需常數 ===============
|
9 |
-
# 請確認此處的 MID_V 是否有效,若過期請更新
|
10 |
-
MID_V = "W0_DiF4DlgU5OeQoRswrRcaaNHMWOL7K3ra3385ocZcv-bBOWySZvoUtH6j-7pjiccl0C5h30uRUNbJXsABCKMqiekSb7tdiBNdVq8Ro5jgk6sgvhZla5iV0H3-8dZfASc7AhEm85679LIK3hxN7Sam6D0LAnYK9Lb0DZhn7xeTeksB4IsBx4Msr_VI" # 請填入有效的 mid_v
|
11 |
-
USER_AGENT_7_11 = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
|
12 |
-
API_7_11_BASE = "https://lovefood.openpoint.com.tw/LoveFood/api"
|
13 |
-
|
14 |
-
# =============== FamilyMart 所需常數 ===============
|
15 |
-
FAMILY_PROJECT_CODE = "202106302" # 若有需要請自行調整
|
16 |
-
API_FAMILY = "https://stamp.family.com.tw/api/maps/MapProductInfo"
|
17 |
-
|
18 |
-
# 3 公里範圍
|
19 |
-
MAX_DISTANCE = 3000
|
20 |
-
|
21 |
-
# -----------------------------------------------------------
|
22 |
-
# 7-11: 取得 AccessToken
|
23 |
-
# -----------------------------------------------------------
|
24 |
-
def get_7_11_token():
|
25 |
-
url = f"{API_7_11_BASE}/Auth/FrontendAuth/AccessToken?mid_v={MID_V}"
|
26 |
-
headers = {
|
27 |
-
"user-agent": USER_AGENT_7_11
|
28 |
-
}
|
29 |
-
resp = requests.post(url, headers=headers, data="")
|
30 |
-
resp.raise_for_status()
|
31 |
-
js = resp.json()
|
32 |
-
if not js.get("isSuccess"):
|
33 |
-
raise RuntimeError(f"取得 7-11 token 失敗: {js}")
|
34 |
-
token = js["element"]
|
35 |
-
return token
|
36 |
-
|
37 |
-
# -----------------------------------------------------------
|
38 |
-
# 7-11: 取得附近門市清單 (含剩餘即期品總數量)
|
39 |
-
# -----------------------------------------------------------
|
40 |
-
def get_7_11_nearby_stores(token, lat, lon):
|
41 |
-
url = f"{API_7_11_BASE}/Search/FrontendStoreItemStock/GetNearbyStoreList?token={token}"
|
42 |
-
headers = {
|
43 |
-
"user-agent": USER_AGENT_7_11,
|
44 |
-
"content-type": "application/json",
|
45 |
-
}
|
46 |
-
body = {
|
47 |
-
"CurrentLocation": {"Latitude": lat, "Longitude": lon},
|
48 |
-
"SearchLocation": {"Latitude": lat, "Longitude": lon}
|
49 |
-
}
|
50 |
-
resp = requests.post(url, headers=headers, json=body)
|
51 |
-
resp.raise_for_status()
|
52 |
-
js = resp.json()
|
53 |
-
if not js.get("isSuccess"):
|
54 |
-
raise RuntimeError(f"取得 7-11 附近門市失敗: {js}")
|
55 |
-
return js["element"].get("StoreStockItemList", [])
|
56 |
-
|
57 |
-
# -----------------------------------------------------------
|
58 |
-
# 7-11: 取得單一門市的即期品清單
|
59 |
-
# -----------------------------------------------------------
|
60 |
-
def get_7_11_store_detail(token, lat, lon, store_no):
|
61 |
-
url = f"{API_7_11_BASE}/Search/FrontendStoreItemStock/GetStoreDetail?token={token}"
|
62 |
-
headers = {
|
63 |
-
"user-agent": USER_AGENT_7_11,
|
64 |
-
"content-type": "application/json",
|
65 |
-
}
|
66 |
-
body = {
|
67 |
-
"CurrentLocation": {"Latitude": lat, "Longitude": lon},
|
68 |
-
"StoreNo": store_no
|
69 |
-
}
|
70 |
-
resp = requests.post(url, headers=headers, json=body)
|
71 |
-
resp.raise_for_status()
|
72 |
-
js = resp.json()
|
73 |
-
if not js.get("isSuccess"):
|
74 |
-
raise RuntimeError(f"取得 7-11 門市({store_no})資料失敗: {js}")
|
75 |
-
return js["element"].get("StoreStockItem", {})
|
76 |
-
|
77 |
-
# -----------------------------------------------------------
|
78 |
-
# FamilyMart: 取得附近門市即期品清單 (單次呼叫可拿到所有商品細項)
|
79 |
-
# -----------------------------------------------------------
|
80 |
-
def get_family_nearby_stores(lat, lon):
|
81 |
-
headers = {
|
82 |
-
"Content-Type": "application/json;charset=utf-8",
|
83 |
-
}
|
84 |
-
body = {
|
85 |
-
"ProjectCode": FAMILY_PROJECT_CODE,
|
86 |
-
"latitude": lat,
|
87 |
-
"longitude": lon
|
88 |
-
}
|
89 |
-
resp = requests.post(API_FAMILY, headers=headers, json=body)
|
90 |
-
resp.raise_for_status()
|
91 |
-
js = resp.json()
|
92 |
-
# 根據回傳範例,成功時 code 為 1
|
93 |
-
if js.get("code") != 1:
|
94 |
-
raise RuntimeError(f"取得全家門市資料失敗: {js}")
|
95 |
-
return js["data"]
|
96 |
-
|
97 |
-
# -----------------------------------------------------------
|
98 |
-
# Gradio 查詢邏輯
|
99 |
-
# -----------------------------------------------------------
|
100 |
-
def find_nearest_store(address, lat, lon):
|
101 |
-
print(f"🔍 收到查詢請求: address={address}, lat={lat}, lon={lon}")
|
102 |
-
if lat == 0 or lon == 0:
|
103 |
-
return [["❌ 請輸入地址或提供 GPS 座標", "", "", "", ""]]
|
104 |
-
|
105 |
-
result_rows = []
|
106 |
-
|
107 |
-
# ------------------ 7-11 ------------------
|
108 |
-
try:
|
109 |
-
token_711 = get_7_11_token()
|
110 |
-
nearby_stores_711 = get_7_11_nearby_stores(token_711, lat, lon)
|
111 |
-
for store in nearby_stores_711:
|
112 |
-
dist_m = store.get("Distance", 999999)
|
113 |
-
if dist_m <= MAX_DISTANCE:
|
114 |
-
store_no = store.get("StoreNo")
|
115 |
-
store_name = store.get("StoreName", "7-11 未提供店名")
|
116 |
-
remaining_qty = store.get("RemainingQty", 0)
|
117 |
-
if remaining_qty > 0:
|
118 |
-
detail = get_7_11_store_detail(token_711, lat, lon, store_no)
|
119 |
-
for cat in detail.get("CategoryStockItems", []):
|
120 |
-
cat_name = cat.get("Name", "")
|
121 |
-
for item in cat.get("ItemList", []):
|
122 |
-
item_name = item.get("ItemName", "")
|
123 |
-
item_qty = item.get("RemainingQty", 0)
|
124 |
-
# 在最後加一個 float 距離欄位以便排序
|
125 |
-
row = [
|
126 |
-
f"7-11 {store_name}",
|
127 |
-
f"{dist_m:.1f} m",
|
128 |
-
f"{cat_name} - {item_name}",
|
129 |
-
str(item_qty),
|
130 |
-
dist_m # 供排序用
|
131 |
-
]
|
132 |
-
result_rows.append(row)
|
133 |
-
else:
|
134 |
-
row = [
|
135 |
-
f"7-11 {store_name}",
|
136 |
-
f"{dist_m:.1f} m",
|
137 |
-
"即期品 0 項",
|
138 |
-
"0",
|
139 |
-
dist_m # 供排序用
|
140 |
-
]
|
141 |
-
result_rows.append(row)
|
142 |
-
except Exception as e:
|
143 |
-
print(f"❌ 取得 7-11 即期品時發生錯誤: {e}")
|
144 |
-
|
145 |
-
# ------------------ FamilyMart ------------------
|
146 |
-
try:
|
147 |
-
nearby_stores_family = get_family_nearby_stores(lat, lon)
|
148 |
-
for store in nearby_stores_family:
|
149 |
-
dist_m = store.get("distance", 999999)
|
150 |
-
if dist_m <= MAX_DISTANCE:
|
151 |
-
store_name = store.get("name", "全家 未提供店名")
|
152 |
-
info_list = store.get("info", [])
|
153 |
-
has_item = False
|
154 |
-
for big_cat in info_list:
|
155 |
-
big_cat_name = big_cat.get("name", "")
|
156 |
-
for subcat in big_cat.get("categories", []):
|
157 |
-
subcat_name = subcat.get("name", "")
|
158 |
-
for product in subcat.get("products", []):
|
159 |
-
product_name = product.get("name", "")
|
160 |
-
qty = product.get("qty", 0)
|
161 |
-
if qty > 0:
|
162 |
-
has_item = True
|
163 |
-
row = [
|
164 |
-
f"全家 {store_name}",
|
165 |
-
f"{dist_m:.1f} m",
|
166 |
-
f"{big_cat_name} - {subcat_name} - {product_name}",
|
167 |
-
str(qty),
|
168 |
-
dist_m # 供排序用
|
169 |
-
]
|
170 |
-
result_rows.append(row)
|
171 |
-
if not has_item:
|
172 |
-
row = [
|
173 |
-
f"全家 {store_name}",
|
174 |
-
f"{dist_m:.1f} m",
|
175 |
-
"即期品 0 項",
|
176 |
-
"0",
|
177 |
-
dist_m # 供排序用
|
178 |
-
]
|
179 |
-
result_rows.append(row)
|
180 |
-
except Exception as e:
|
181 |
-
print(f"❌ 取得全家 即期品時發生錯誤: {e}")
|
182 |
-
|
183 |
-
if not result_rows:
|
184 |
-
return [["❌ 附近 3 公里內沒有即期食品", "", "", "", ""]]
|
185 |
-
|
186 |
-
# ============= 在這裡進行排序 =============
|
187 |
-
# result_rows 的結構是 [門市, 距離(字串), 商品, 數量, float_distance]
|
188 |
-
# 我們要依照最後一欄 float_distance 做由小到大排序
|
189 |
-
result_rows.sort(key=lambda x: x[4])
|
190 |
-
|
191 |
-
# 排序完之後,再把最後一欄刪掉 (不顯示給使用者)
|
192 |
-
for row in result_rows:
|
193 |
-
row.pop() # 移除 index=4 (float_distance)
|
194 |
-
|
195 |
-
return result_rows
|
196 |
-
|
197 |
-
# -----------------------------------------------------------
|
198 |
-
# Gradio 介面
|
199 |
-
# -----------------------------------------------------------
|
200 |
-
import gradio as gr
|
201 |
-
|
202 |
-
with gr.Blocks() as demo:
|
203 |
-
gr.Markdown("## 便利商店「即期食品」搜尋示範")
|
204 |
-
gr.Markdown("""
|
205 |
-
1. 按下「使用目前位置」或自行輸入緯度/經度
|
206 |
-
2. 點選「搜尋」查詢 3 公里內 7-11 / 全家的即期品
|
207 |
-
3. 若要執行,需要有效的 mid_v (7-11 愛食記憶官網)
|
208 |
-
4. 在 Logs 查看詳細錯誤或除錯資訊
|
209 |
-
""")
|
210 |
-
address = gr.Textbox(label="輸入地址(可留空)")
|
211 |
-
lat = gr.Number(label="GPS 緯度", value=0, elem_id="lat")
|
212 |
-
lon = gr.Number(label="GPS 經度", value=0, elem_id="lon")
|
213 |
-
|
214 |
-
with gr.Row():
|
215 |
-
gps_button = gr.Button("📍 ❶ 使用目前位置-先按這個 並等待3秒 ", elem_id="gps-btn")
|
216 |
-
search_button = gr.Button("🔍 ❷ 搜尋 ")
|
217 |
-
|
218 |
-
output_table = gr.Dataframe(
|
219 |
-
headers=["門市", "距離 (m)", "商品/即期食品", "數量"],
|
220 |
-
interactive=False
|
221 |
-
)
|
222 |
-
|
223 |
-
search_button.click(fn=find_nearest_store, inputs=[address, lat, lon], outputs=output_table)
|
224 |
-
|
225 |
-
gps_button.click(
|
226 |
-
None,
|
227 |
-
None,
|
228 |
-
[lat, lon],
|
229 |
-
js="""
|
230 |
-
() => {
|
231 |
-
return new Promise((resolve) => {
|
232 |
-
if (!navigator.geolocation) {
|
233 |
-
alert("您的瀏覽器不支援地理位置功能");
|
234 |
-
resolve([0, 0]);
|
235 |
-
return;
|
236 |
-
}
|
237 |
-
navigator.geolocation.getCurrentPosition(
|
238 |
-
(position) => {
|
239 |
-
resolve([position.coords.latitude, position.coords.longitude]);
|
240 |
-
},
|
241 |
-
(error) => {
|
242 |
-
alert("無法取得位置:" + error.message);
|
243 |
-
resolve([0, 0]);
|
244 |
-
}
|
245 |
-
);
|
246 |
-
});
|
247 |
-
}
|
248 |
-
"""
|
249 |
-
)
|
250 |
-
|
251 |
-
def main():
|
252 |
-
demo.launch(server_name="0.0.0.0", server_port=7860, debug=True)
|
253 |
-
|
254 |
-
if __name__ == "__main__":
|
255 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|