Spaces:
Sleeping
Sleeping
Commit
·
d5e7585
1
Parent(s):
3f1433a
- app.py +221 -116
- 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
|
7 |
from geopy.distance import geodesic
|
8 |
|
9 |
-
#
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
-
#
|
15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
#
|
33 |
-
|
34 |
-
|
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 |
-
#
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
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 |
-
#
|
94 |
-
|
95 |
-
|
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 |
-
|
101 |
-
|
102 |
-
|
|
|
|
|
|
|
|
|
103 |
|
104 |
-
if
|
105 |
-
return [["❌ 附近 3 公里內沒有便利商店", "", "", "", ""]]
|
106 |
|
107 |
-
#
|
108 |
output = []
|
|
|
|
|
|
|
109 |
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
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 |
-
#
|
|
|
|
|
136 |
with gr.Blocks() as demo:
|
137 |
-
gr.Markdown("## 便利商店門市與商品搜尋")
|
138 |
-
gr.Markdown("
|
139 |
|
140 |
-
address = gr.Textbox(label="
|
141 |
-
lat = gr.Number(label="GPS 緯度
|
142 |
-
lon = gr.Number(label="GPS 經度
|
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]);
|
165 |
return;
|
166 |
}
|
167 |
-
|
168 |
navigator.geolocation.getCurrentPosition(
|
169 |
(position) => {
|
170 |
resolve([position.coords.latitude, position.coords.longitude]);
|
171 |
},
|
172 |
(error) => {
|
173 |
-
alert("
|
174 |
-
resolve([0, 0]);
|
175 |
}
|
176 |
);
|
177 |
});
|
@@ -179,5 +272,17 @@ with gr.Blocks() as demo:
|
|
179 |
"""
|
180 |
)
|
181 |
|
182 |
-
|
183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|