Remove coordinate feature due to poor UX

- Revert to initial clean version without coordinates
- Nominatim rate limiting makes the feature unusable
- Keep core functionality: fetching CPC station data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 09:47:10 +08:00
parent 876b7483ba
commit 3ddbf259ae
3 changed files with 18 additions and 6054 deletions

View File

@@ -17,10 +17,8 @@
from __future__ import annotations
import argparse
import concurrent.futures
import csv
import json
import os
import re
import sys
import time
@@ -28,13 +26,10 @@ import urllib.parse
import urllib.request
from html import unescape
from html.parser import HTMLParser
from pathlib import Path
from typing import Dict, List, Optional
BASE_URL = "https://vipmbr.cpc.com.tw/mbwebs/service_search.aspx"
USER_AGENT = "Mozilla/5.0 (OpenClaw; cpc-direct-stations)"
CACHE_FILE = Path.home() / ".cache" / "cpc_stations_coords.json"
FUEL_DB_FILE = Path(__file__).parent / "fuel_stations.json" # 離線加油站座標庫
CITY_MAP = {
"台北市": "A ",
"新北市": "B ",
@@ -140,76 +135,6 @@ def extract_hidden_fields(html: str) -> Dict[str, str]:
return fields
def load_coords_cache() -> Dict[str, Dict[str, float]]:
"""載入座標快取。"""
if CACHE_FILE.exists():
try:
with open(CACHE_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
return {}
def save_coords_cache(cache: Dict[str, Dict[str, float]]) -> None:
"""儲存座標快取。"""
CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(CACHE_FILE, "w", encoding="utf-8") as f:
json.dump(cache, f, ensure_ascii=False, indent=2)
def load_fuel_db() -> Dict[str, Dict[str, float]]:
"""載入離線加油站座標資料庫來源OSM"""
if FUEL_DB_FILE.exists():
try:
with open(FUEL_DB_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
return {}
def query_fuel_db(station_name: str, city: str, district: str, fuel_db: Dict) -> Optional[Dict[str, float]]:
"""從離線資料庫查詢加油站座標。"""
if not fuel_db:
return None
# 清理站名(移除代號)
clean_name = re.sub(r'\s+[A-Z]\d+[A-Z]?\s*$', '', station_name).strip()
# 嘗試多種查詢鍵
queries = [
clean_name,
f"台灣中油 {clean_name}",
f"中油 {clean_name}",
f"{clean_name}",
f"台灣中油 {clean_name}",
f"中油 {clean_name}",
]
if city:
queries.extend([
f"{city}{clean_name}",
f"{city}{clean_name}",
f"{clean_name} ({city})",
])
for q in queries:
if q in fuel_db:
return fuel_db[q]
# 模糊匹配
for key, value in fuel_db.items():
if clean_name in key or key in clean_name:
return value
return None
def make_cache_key(station: Dict[str, str]) -> str:
"""產生快取鍵值:使用站名+縣市+完整地址。"""
return f"{station['站名']}|{station['縣市']}|{station['鄉鎮區']}|{station['地址']}"
def geocode(address: str, station_name: str, city: str, district: str) -> Optional[Dict[str, float]]:
"""使用 Nominatim (OpenStreetMap) API 將地址轉換為座標。
@@ -238,7 +163,7 @@ def geocode(address: str, station_name: str, city: str, district: str) -> Option
}
url = f"https://nominatim.openstreetmap.org/search?{urllib.parse.urlencode(params)}"
req = urllib.request.Request(url, headers={"User-Agent": user_agent})
with urllib.request.urlopen(req, timeout=15) as response:
with urllib.request.urlopen(req, timeout=10) as response:
data = json.loads(response.read().decode("utf-8"))
if data:
return {"lat": float(data[0]["lat"]), "lon": float(data[0]["lon"])}
@@ -247,67 +172,23 @@ def geocode(address: str, station_name: str, city: str, district: str) -> Option
return None
def add_coordinates(stations: List[Dict[str, str]], delay: float = 1.0, workers: int = 2) -> List[Dict[str, str]]:
"""為站點資料加上座標資訊(快取 -> OSM資料庫 -> Nominatim
Args:
stations: 站點列表
delay: 每個查詢間隔秒數Nominatim 限制約 1 秒)
workers: 並發查詢執行緒數(預設 2避免 Nominatim 限制)
"""
cache = load_coords_cache()
fuel_db = load_fuel_db()
if fuel_db:
print(f"載入離線資料庫: {len(fuel_db)}", file=sys.stderr)
missing_coords = []
# 先從快取讀取,再試離線資料庫
def add_coordinates(stations: List[Dict[str, str]], delay: float = 1.0) -> List[Dict[str, str]]:
"""為站點資料加上座標資訊"""
for station in stations:
key = make_cache_key(station)
if key in cache:
station["緯度"] = str(cache[key]["lat"])
station["經度"] = str(cache[key]["lon"])
full_address = f"{station['縣市']}{station['鄉鎮區']}{station['地址']}"
coords = geocode(
address=full_address,
station_name=station["站名"],
city=station["縣市"],
district=station["鄉鎮區"],
)
if coords:
station["緯度"] = str(coords["lat"])
station["經度"] = str(coords["lon"])
else:
# 嘗試離線資料庫
coords = query_fuel_db(station["站名"], station["縣市"], station["鄉鎮區"], fuel_db)
if coords:
station["緯度"] = str(coords["lat"])
station["經度"] = str(coords["lon"])
# 寫入快取
cache[key] = coords
else:
station["緯度"] = ""
station["經度"] = ""
missing_coords.append(station)
# 並發查詢缺少的座標(使用 Nominatim
if missing_coords:
print(f"快取命中: {len(stations) - len(missing_coords)}/{len(stations)},需 API 查詢: {len(missing_coords)}", file=sys.stderr)
def query_one(station: Dict[str, str]) -> tuple[Dict[str, str], Optional[Dict[str, float]]]:
full_address = f"{station['縣市']}{station['鄉鎮區']}{station['地址']}"
coords = geocode(
address=full_address,
station_name=station["站名"],
city=station["縣市"],
district=station["鄉鎮區"],
)
time.sleep(delay) # 避免 Nominatim 請求過快
return (station, coords)
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
futures = {executor.submit(query_one, s): s for s in missing_coords}
for future in concurrent.futures.as_completed(futures):
station, coords = future.result()
if coords:
station["緯度"] = str(coords["lat"])
station["經度"] = str(coords["lon"])
key = make_cache_key(station)
cache[key] = coords
save_coords_cache(cache)
station["緯度"] = ""
station["經度"] = ""
time.sleep(delay) # 避免 Nominatim 請求過快
return stations
@@ -436,8 +317,7 @@ def main() -> None:
parser.add_argument("--format", choices=["text", "json", "csv"], default="text")
parser.add_argument("--out", help="輸出到檔案路徑")
parser.add_argument("--coords", action="store_true", help="加上座標資訊(使用 Nominatim OSM API")
parser.add_argument("--delay", type=float, default=1.0, help="座標查詢間隔秒數(預設 1.0")
parser.add_argument("--workers", type=int, default=2, help="並發查詢執行緒數(預設 2避免 Nominatim 限制)")
parser.add_argument("--delay", type=float, default=1.0, help="座標查詢間隔秒數(預設 1.0Nominatim 限制")
args = parser.parse_args()
html = build_query_html(city=args.city, keyword=args.keyword)
@@ -445,7 +325,7 @@ def main() -> None:
stations = filter_stations(stations, args.keyword)
if args.coords:
stations = add_coordinates(stations, delay=args.delay, workers=args.workers)
stations = add_coordinates(stations, delay=args.delay)
if args.format == "json":
output_json(stations, args.out)