'commit'
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
|
|
||||||
# 采集配置
|
# 采集配置
|
||||||
SCROLL_DISTANCE_RATIO = 0.5
|
SCROLL_DISTANCE_RATIO = 0.3
|
||||||
MAX_STATIONS_COUNT = 3
|
MAX_STATIONS_COUNT = 3
|
||||||
FIRST_RUN_ONLY_ONE_STATION = False
|
FIRST_RUN_ONLY_ONE_STATION = False
|
||||||
|
# 场站去重过期时间(秒),在此时间内重复出现的场站不会再次点击进入详情页
|
||||||
|
REDIS_STATION_EXPIRE = 120
|
||||||
|
|
||||||
# 调试绘图配置
|
# 调试绘图配置
|
||||||
DRAW_DEBUG_BOXES = True
|
DRAW_DEBUG_BOXES = True
|
||||||
@@ -16,7 +18,7 @@ WAIT_BACK_TO_LIST = 1.5
|
|||||||
WAIT_AFTER_SCROLL = 5.0
|
WAIT_AFTER_SCROLL = 5.0
|
||||||
|
|
||||||
# 坐标计算与安全防护
|
# 坐标计算与安全防护
|
||||||
SAFE_EXCLUDE_RATIO = 0.58
|
SAFE_EXCLUDE_RATIO = 0.4
|
||||||
BOTTOM_SAFE_EXCLUDE_RATIO = 0.12
|
BOTTOM_SAFE_EXCLUDE_RATIO = 0.12
|
||||||
MIN_CARD_HEIGHT = 250
|
MIN_CARD_HEIGHT = 250
|
||||||
DETAIL_SCROLL_DISTANCE_RATIO = 0.9
|
DETAIL_SCROLL_DISTANCE_RATIO = 0.9
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ from Apps.TeLaiDian.Service import TeLaiDianService
|
|||||||
from Apps.TeLaiDian.Config.Setting import (
|
from Apps.TeLaiDian.Config.Setting import (
|
||||||
SCROLL_DISTANCE_RATIO, WAIT_AFTER_SCROLL, MAX_STATIONS_COUNT,
|
SCROLL_DISTANCE_RATIO, WAIT_AFTER_SCROLL, MAX_STATIONS_COUNT,
|
||||||
SAFE_EXCLUDE_RATIO, BOTTOM_SAFE_EXCLUDE_RATIO, WAIT_DETAIL_PAGE_LOAD,
|
SAFE_EXCLUDE_RATIO, BOTTOM_SAFE_EXCLUDE_RATIO, WAIT_DETAIL_PAGE_LOAD,
|
||||||
WAIT_BACK_TO_LIST, DETAIL_SCROLL_DISTANCE_RATIO, FIRST_RUN_ONLY_ONE_STATION
|
WAIT_BACK_TO_LIST, DETAIL_SCROLL_DISTANCE_RATIO, FIRST_RUN_ONLY_ONE_STATION,
|
||||||
|
REDIS_STATION_EXPIRE
|
||||||
)
|
)
|
||||||
from Core.BaseCrawler import BaseCrawler
|
from Core.BaseCrawler import BaseCrawler
|
||||||
|
from Util.RedisKit import RedisKit
|
||||||
import uiautomator2 as u2
|
import uiautomator2 as u2
|
||||||
|
|
||||||
# 项目根目录处理
|
# 项目根目录处理
|
||||||
@@ -36,6 +38,7 @@ class TeLaiDianCrawler(BaseCrawler):
|
|||||||
def __init__(self, service=None):
|
def __init__(self, service=None):
|
||||||
super().__init__(service or TeLaiDianService())
|
super().__init__(service or TeLaiDianService())
|
||||||
self.read_image_kit = ReadImageKit()
|
self.read_image_kit = ReadImageKit()
|
||||||
|
self.redis_kit = RedisKit()
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""
|
"""
|
||||||
@@ -166,7 +169,6 @@ class TeLaiDianCrawler(BaseCrawler):
|
|||||||
|
|
||||||
processed_count = 0
|
processed_count = 0
|
||||||
last_md5 = None
|
last_md5 = None
|
||||||
processed_station_names = set() # 用于列表级去重
|
|
||||||
|
|
||||||
while processed_count < MAX_STATIONS_COUNT:
|
while processed_count < MAX_STATIONS_COUNT:
|
||||||
# 1. 截图并分析
|
# 1. 截图并分析
|
||||||
@@ -205,8 +207,11 @@ class TeLaiDianCrawler(BaseCrawler):
|
|||||||
if not name or not point:
|
if not name or not point:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if name in processed_station_names:
|
# [优化] 使用 Redis 进行跨运行去重
|
||||||
logger.info(f"跳过已处理场站: {name}")
|
cleaned_name = clean_station_name(name)
|
||||||
|
redis_key = f"crawled:tld:{cleaned_name}"
|
||||||
|
if await self.redis_kit.get_data(redis_key):
|
||||||
|
logger.info(f"跳过已处理场站 (Redis): {name}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.info(f"处理场站: {name} (坐标: {point})")
|
logger.info(f"处理场站: {name} (坐标: {point})")
|
||||||
@@ -234,7 +239,9 @@ class TeLaiDianCrawler(BaseCrawler):
|
|||||||
|
|
||||||
# 爬取详情
|
# 爬取详情
|
||||||
await self.crawl_detail_logic(d, station)
|
await self.crawl_detail_logic(d, station)
|
||||||
processed_station_names.add(name)
|
|
||||||
|
# 标记为已处理
|
||||||
|
await self.redis_kit.set_data(redis_key, "1", ex=REDIS_STATION_EXPIRE)
|
||||||
|
|
||||||
d.press("back")
|
d.press("back")
|
||||||
await asyncio.sleep(WAIT_BACK_TO_LIST)
|
await asyncio.sleep(WAIT_BACK_TO_LIST)
|
||||||
@@ -270,19 +277,29 @@ class TeLaiDianCrawler(BaseCrawler):
|
|||||||
first_screen_path = take_screenshot(d, f"tld_detail_basic_{int(time.time())}.jpg")
|
first_screen_path = take_screenshot(d, f"tld_detail_basic_{int(time.time())}.jpg")
|
||||||
station_name = station_info.get("name")
|
station_name = station_info.get("name")
|
||||||
address = station_info.get("address")
|
address = station_info.get("address")
|
||||||
|
total_piles = None
|
||||||
|
free_piles = None
|
||||||
|
piles_detail = None
|
||||||
|
parking_info = None
|
||||||
|
|
||||||
logger.info(f"[详情页] 进入 crawl_detail_logic,场站: {station_name} | 地址: {address}")
|
logger.info(f"[详情页] 进入 crawl_detail_logic,场站: {station_name} | 地址: {address}")
|
||||||
logger.info(f"[详情页] 已截取首屏截图,准备识别基础信息: {first_screen_path}")
|
logger.info(f"[详情页] 已截取首屏截图,准备识别基础信息: {first_screen_path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
basic_info = await self.read_image_kit.analyze_detail_basic_info(first_screen_path)
|
basic_info = await self.read_image_kit.analyze_detail_basic_info(first_screen_path)
|
||||||
if isinstance(basic_info, dict):
|
if isinstance(basic_info, dict):
|
||||||
name2 = basic_info.get("name")
|
if basic_info.get("name"):
|
||||||
addr2 = basic_info.get("address")
|
station_name = basic_info.get("name")
|
||||||
if name2:
|
if basic_info.get("address"):
|
||||||
station_name = name2
|
address = basic_info.get("address")
|
||||||
if addr2:
|
|
||||||
address = addr2
|
# 提取电桩信息
|
||||||
logger.info(f"[详情页] 同步基础信息识别结果用于写库: {station_name} | {address}")
|
total_piles = basic_info.get("total_piles")
|
||||||
|
free_piles = basic_info.get("free_piles")
|
||||||
|
piles_detail = basic_info.get("piles_detail")
|
||||||
|
parking_info = basic_info.get("parking_info")
|
||||||
|
|
||||||
|
logger.info(f"[详情页] 基础信息识别结果: {station_name} | {address} | 桩数: {total_piles}/{free_piles} | 停车费: {parking_info}")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.error(f"[详情页] 同步分析详情页基础信息失败: {ex}")
|
logger.error(f"[详情页] 同步分析详情页基础信息失败: {ex}")
|
||||||
finally:
|
finally:
|
||||||
@@ -298,81 +315,94 @@ class TeLaiDianCrawler(BaseCrawler):
|
|||||||
w, h = d.window_size()
|
w, h = d.window_size()
|
||||||
|
|
||||||
# 1. 增加等待时间,确保页面加载完成
|
# 1. 增加等待时间,确保页面加载完成
|
||||||
logger.info(f"[详情页] 等待 {WAIT_DETAIL_PAGE_LOAD + 1}s 确保页面稳定...")
|
logger.info(f"[详情页] 等待 {WAIT_DETAIL_PAGE_LOAD}s 确保页面稳定...")
|
||||||
await asyncio.sleep(WAIT_DETAIL_PAGE_LOAD + 1)
|
await asyncio.sleep(WAIT_DETAIL_PAGE_LOAD)
|
||||||
|
|
||||||
# 2. 向上滑动大一些,确保“全部时段”露出来
|
# --- [优化] 小步快跑滚动查找逻辑 ---
|
||||||
logger.info("[详情页] 执行大范围向上滑动 (y: 90% -> 10%),确保显示全部时段")
|
# 目标:通过多次小幅度滚动 + OCR 实时探测,精准捕获“全部时段”入口,避免滑过头
|
||||||
d.swipe(w // 2, int(h * 0.9), w // 2, int(h * 0.1), duration=1.0)
|
max_scroll_attempts = 6 # 最大滚动尝试次数
|
||||||
await asyncio.sleep(2.5) # 滑动后等待稳定
|
scroll_step_ratio = 0.3 # 每次滚动的步长(屏幕高度的 30%)
|
||||||
|
found_entry = None
|
||||||
|
|
||||||
# 3. 截图并识别 (纯 OCR 流程)
|
logger.info(f"[详情页] 开始“小步快跑”滚动查找‘全部时段’入口 (最多 {max_scroll_attempts} 次)...")
|
||||||
final_screen_path = take_screenshot(d, f"tld_detail_ocr_input_{int(time.time())}.jpg")
|
|
||||||
logger.info(f"[详情页] 已截取 OCR 识别用图: {final_screen_path}")
|
for i in range(max_scroll_attempts):
|
||||||
|
# 1. 截图识别当前屏
|
||||||
|
curr_screen = take_screenshot(d, f"tld_scroll_ocr_{i}_{int(time.time())}.jpg")
|
||||||
|
|
||||||
|
# 2. 尝试 OCR 识别“全部时段”
|
||||||
|
entry_data = await self.read_image_kit.find_price_entrance_ocr(curr_screen)
|
||||||
|
|
||||||
|
if entry_data.get("found"):
|
||||||
|
logger.info(f"[详情页] 第 {i+1} 次尝试:成功探测到‘全部时段’入口!")
|
||||||
|
found_entry = {
|
||||||
|
"screen": curr_screen,
|
||||||
|
"point": entry_data["point"]
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
# 3. 如果没找到,小幅向上滚动一段距离
|
||||||
|
if i < max_scroll_attempts - 1:
|
||||||
|
logger.info(f"[详情页] 第 {i+1} 次尝试未找到,小幅向上滚动 (步长: {scroll_step_ratio*100}%)...")
|
||||||
|
d.swipe(w // 2, int(h * 0.7), w // 2, int(h * (0.7 - scroll_step_ratio)), duration=0.5)
|
||||||
|
await asyncio.sleep(1.5) # 滚动后短暂停留
|
||||||
|
|
||||||
|
# 清理过程截图
|
||||||
|
if os.path.exists(curr_screen):
|
||||||
|
try: os.remove(curr_screen)
|
||||||
|
except: pass
|
||||||
|
|
||||||
entrance_clicked = False
|
entrance_clicked = False
|
||||||
try:
|
try:
|
||||||
# 1. 使用 OCR 寻找顶部“价格”标签
|
if found_entry:
|
||||||
tab_data = await self.read_image_kit.find_price_tab_ocr(final_screen_path)
|
price_tab_screen = found_entry["screen"]
|
||||||
tab_x, tab_y = None, None
|
p = found_entry["point"]
|
||||||
|
|
||||||
if tab_data.get("found") and tab_data.get("point"):
|
# 1. 先点击顶部“价格”标签 (确保切到价格页,虽然滚动前可能已经点击,但这里做双保险)
|
||||||
p = tab_data["point"]
|
# 先 OCR 找一次标签
|
||||||
tab_x = int(p[0] * w / 1000)
|
tab_data = await self.read_image_kit.find_price_tab_ocr(price_tab_screen)
|
||||||
tab_y = int(p[1] * h / 1000)
|
tab_x, tab_y = None, None
|
||||||
logger.info(f"[详情页] OCR 成功找到价格标签: 归一化{p} -> 像素({tab_x}, {tab_y})")
|
if tab_data.get("found"):
|
||||||
else:
|
p_tab = tab_data["point"]
|
||||||
tab_x = int(PRICE_TAB_X_NORM * w / 1000)
|
tab_x, tab_y = int(p_tab[0] * w / 1000), int(p_tab[1] * h / 1000)
|
||||||
tab_y = int(PRICE_TAB_Y_NORM * h / 1000)
|
logger.info(f"[详情页] 点击顶部价格标签: ({tab_x}, {tab_y})")
|
||||||
logger.warning(f"[详情页] OCR 未找到价格标签,使用固定坐标兜底: 像素({tab_x}, {tab_y})")
|
d.click(tab_x, tab_y)
|
||||||
|
await asyncio.sleep(1.0)
|
||||||
logger.info(f"[详情页] 正在点击价格标签...")
|
|
||||||
d.click(tab_x, tab_y)
|
|
||||||
await asyncio.sleep(2.5) # 点击标签后等待
|
|
||||||
|
|
||||||
price_tab_screen = take_screenshot(d, f"tld_detail_after_price_tab_{int(time.time())}.jpg")
|
|
||||||
logger.info(f"[详情页] 点击价格标签后的界面截图已保存: {price_tab_screen}")
|
|
||||||
|
|
||||||
# 2. 寻找并点击“全部时段”入口 (纯 OCR 识别)
|
|
||||||
entry_data = await self.read_image_kit.find_price_entrance_ocr(price_tab_screen)
|
|
||||||
entry_x, entry_y = None, None
|
|
||||||
|
|
||||||
if entry_data.get("found") and entry_data.get("point"):
|
|
||||||
p = entry_data["point"]
|
|
||||||
# 安全校验:严禁点击 y > 900 的区域,那是底部浮动条
|
|
||||||
if p[1] > 900:
|
|
||||||
logger.warning(f"[详情页] OCR 返回坐标 {p} 偏低 (疑似底部浮动条),尝试使用固定坐标兜底")
|
|
||||||
entry_x = int(PRICE_ENTRY_X_NORM * w / 1000)
|
|
||||||
entry_y = int(PRICE_ENTRY_Y_NORM * h / 1000)
|
|
||||||
else:
|
else:
|
||||||
entry_x = int(p[0] * w / 1000)
|
# 按照用户要求:找不到文字时输出日志、截图并停止程序
|
||||||
entry_y = int(p[1] * h / 1000)
|
fail_screen = take_screenshot(d, f"tld_ocr_tab_fail_{int(time.time())}.jpg")
|
||||||
logger.info(f"[详情页] OCR 成功找到价格入口: 归一化{p} -> 像素({entry_x}, {entry_y})")
|
logger.error(f"❌ [OCR失败] 在页面中未找到‘价格’标签文字!")
|
||||||
else:
|
logger.error(f"❌ [OCR失败] 最终截图已保存至: {fail_screen}")
|
||||||
entry_x = int(PRICE_ENTRY_X_NORM * w / 1000)
|
logger.error("❌ [OCR失败] 程序将停止运行,请检查页面内容或识别逻辑。")
|
||||||
entry_y = int(PRICE_ENTRY_Y_NORM * h / 1000)
|
sys.exit(1)
|
||||||
logger.warning(f"[详情页] OCR 未找到价格入口,使用固定坐标兜底: 像素({entry_x}, {entry_y})")
|
|
||||||
|
|
||||||
click_x = max(5, min(w - 5, entry_x))
|
# 2. 点击“全部时段”入口
|
||||||
click_y = max(5, min(h - 5, entry_y))
|
entry_x = int(p[0] * w / 1000)
|
||||||
|
entry_y = int(p[1] * h / 1000)
|
||||||
|
|
||||||
# 绘制诊断图
|
# 安全校验
|
||||||
debug_click_path = price_tab_screen.replace(".jpg", f"_click_diag.jpg")
|
if entry_y > h * 0.9:
|
||||||
try:
|
logger.warning(f"[详情页] 入口坐标偏低 ({entry_y}),可能在底部遮罩,尝试微调。")
|
||||||
|
|
||||||
|
# 绘制最终点击诊断图
|
||||||
|
debug_click_path = price_tab_screen.replace(".jpg", "_final_click.jpg")
|
||||||
img = read_image(price_tab_screen)
|
img = read_image(price_tab_screen)
|
||||||
if img is not None:
|
if img is not None:
|
||||||
# 绘制两个点击位置
|
cv2.circle(img, (entry_x, entry_y), 25, (0, 255, 0), -1)
|
||||||
cv2.circle(img, (tab_x, tab_y), 20, (0, 0, 255), -1)
|
|
||||||
cv2.circle(img, (click_x, click_y), 20, (0, 255, 0), -1)
|
|
||||||
save_image(debug_click_path, img)
|
save_image(debug_click_path, img)
|
||||||
logger.info(f"[详情页] 已生成 OCR 点击诊断图 (红点:价格标签, 绿点:入口): {debug_click_path}")
|
logger.info(f"[详情页] 已生成最终点击诊断图: {debug_click_path}")
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[详情页] 生成诊断图片失败: {e}")
|
|
||||||
|
|
||||||
logger.info(f"[详情页] 正在点击电价入口...")
|
logger.info(f"[详情页] 正在点击电价入口: ({entry_x}, {entry_y})")
|
||||||
d.click(click_x, click_y)
|
d.click(entry_x, entry_y)
|
||||||
entrance_clicked = True
|
entrance_clicked = True
|
||||||
await asyncio.sleep(WAIT_DETAIL_PAGE_LOAD + 1)
|
await asyncio.sleep(WAIT_DETAIL_PAGE_LOAD)
|
||||||
|
else:
|
||||||
|
# 按照用户要求:找不到文字时输出日志、截图并停止程序
|
||||||
|
fail_screen = take_screenshot(d, f"tld_ocr_fail_{int(time.time())}.jpg")
|
||||||
|
logger.error(f"❌ [OCR失败] 经过 {max_scroll_attempts} 次滚动仍未在页面中找到‘全部时段’文字!")
|
||||||
|
logger.error(f"❌ [OCR失败] 最终截图已保存至: {fail_screen}")
|
||||||
|
logger.error("❌ [OCR失败] 程序将停止运行,请检查页面内容或识别逻辑。")
|
||||||
|
sys.exit(1) # 停止程序
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[详情页] 识别或点击价格入口失败: {e}")
|
logger.error(f"[详情页] 识别或点击价格入口失败: {e}")
|
||||||
|
|
||||||
@@ -475,7 +505,15 @@ class TeLaiDianCrawler(BaseCrawler):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
logger.info(f"✅ 场站 {station_name_clean} 共提取到 {len(all_prices)} 条价格信息,准备保存...")
|
logger.info(f"✅ 场站 {station_name_clean} 共提取到 {len(all_prices)} 条价格信息,准备保存...")
|
||||||
await self.service.save_station_data(station_name_clean, address, all_prices)
|
await self.service.save_station_data(
|
||||||
|
station_name_clean,
|
||||||
|
address,
|
||||||
|
all_prices,
|
||||||
|
total_piles=total_piles,
|
||||||
|
free_piles=free_piles,
|
||||||
|
piles_detail=piles_detail,
|
||||||
|
parking_info=parking_info
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"❌ 未能提取到任何价格信息,请检查页面识别逻辑")
|
logger.warning(f"❌ 未能提取到任何价格信息,请检查页面识别逻辑")
|
||||||
if address:
|
if address:
|
||||||
|
|||||||
@@ -179,37 +179,6 @@ class ReadImageKit:
|
|||||||
logger.warning(f"[OCR] 未能在顶部区域定位到‘价格’标签")
|
logger.warning(f"[OCR] 未能在顶部区域定位到‘价格’标签")
|
||||||
return {"found": False}
|
return {"found": False}
|
||||||
|
|
||||||
async def find_price_tab_ocr(self, image_path):
|
|
||||||
"""
|
|
||||||
使用 OCR 在详情页寻找顶部“价格”标签
|
|
||||||
"""
|
|
||||||
img = read_image(image_path)
|
|
||||||
if img is None:
|
|
||||||
return {"found": False}
|
|
||||||
h, w = img.shape[:2]
|
|
||||||
|
|
||||||
from Util.EasyOcrKit import get_easyocr_reader
|
|
||||||
reader = get_easyocr_reader(gpu=True)
|
|
||||||
|
|
||||||
# 顶部标签栏通常在屏幕上部 10%-30% 区域
|
|
||||||
# 我们先识别所有文字,然后过滤出“价格”
|
|
||||||
results = reader.read_text(img)
|
|
||||||
for (quad, text, prob) in results:
|
|
||||||
if "价格" in text and prob > 0.3:
|
|
||||||
# 获取中心点
|
|
||||||
rect = reader.get_normalized_rect(quad, w, h)
|
|
||||||
center_x = (rect[0] + rect[2]) // 2
|
|
||||||
center_y = (rect[1] + rect[3]) // 2
|
|
||||||
|
|
||||||
# 校验位置:必须在屏幕上半部分
|
|
||||||
if center_y < 500:
|
|
||||||
logger.info(f"[OCR] 找到顶部价格标签: '{text}', 置信度: {prob:.4f}, 坐标: [{center_x}, {center_y}]")
|
|
||||||
return {
|
|
||||||
"found": True,
|
|
||||||
"point": [center_x, center_y]
|
|
||||||
}
|
|
||||||
|
|
||||||
return {"found": False}
|
|
||||||
|
|
||||||
async def find_price_entrance_ocr(self, image_path):
|
async def find_price_entrance_ocr(self, image_path):
|
||||||
"""
|
"""
|
||||||
@@ -239,7 +208,8 @@ class ReadImageKit:
|
|||||||
"point": [center_x, center_y]
|
"point": [center_x, center_y]
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.warning(f"[OCR] 未能在页面中定位到‘全部时段’")
|
# 记录警告日志
|
||||||
|
logger.warning(f"⚠️ [OCR识别失败] 在图片中未发现‘全部时段’关键字: {image_path}")
|
||||||
return {"found": False}
|
return {"found": False}
|
||||||
|
|
||||||
async def find_close_button_vlm(self, image_path):
|
async def find_close_button_vlm(self, image_path):
|
||||||
@@ -424,18 +394,31 @@ class ReadImageKit:
|
|||||||
|
|
||||||
async def analyze_detail_basic_info(self, image_path):
|
async def analyze_detail_basic_info(self, image_path):
|
||||||
"""
|
"""
|
||||||
分析详情页首屏截图,提取场站名称和精确地址
|
分析详情页首屏截图,提取场站名称、精确地址以及电桩状态信息
|
||||||
"""
|
"""
|
||||||
prompt = """
|
prompt = """
|
||||||
分析这张充电站详情页首屏截图,提取:
|
分析这张充电站详情页首屏截图,提取:
|
||||||
1. 场站名称 (通常在页面中部,大字体)
|
1. 场站名称 (通常在页面中部,大字体)
|
||||||
2. 详细地址 (通常在名称下方或页面下半部分,伴有地址图标)
|
2. 详细地址 (通常在名称下方或页面下半部分,伴有地址图标)
|
||||||
|
3. 电桩状态信息:寻找包含“快充”、“慢充”、“空闲”、“总数”或类似描述的区域。
|
||||||
|
- 提取快充桩的总数和空闲数
|
||||||
|
- 提取慢充桩的总数和空闲数
|
||||||
|
- 计算总桩数 (total_piles) 和总空闲数 (free_piles)
|
||||||
|
4. 停车费信息:寻找“停车费”、“停车减免”、“免费停车”等相关描述。
|
||||||
|
|
||||||
输出格式为 JSON:
|
输出格式为 JSON:
|
||||||
{
|
{
|
||||||
"name": "xxx充电站",
|
"name": "xxx充电站",
|
||||||
"address": "xxx省xxx市xxx区xxx路xxx号"
|
"address": "xxx省xxx市xxx区xxx路xxx号",
|
||||||
|
"total_piles": 10,
|
||||||
|
"free_piles": 5,
|
||||||
|
"piles_detail": [
|
||||||
|
{"type": "快充", "total": 6, "free": 3},
|
||||||
|
{"type": "慢充", "total": 4, "free": 2}
|
||||||
|
],
|
||||||
|
"parking_info": "前2小时免费,之后5元/小时"
|
||||||
}
|
}
|
||||||
|
注意:如果无法确定某个数值,请设为 null。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
res_text = await self.vlm.analyze_image(image_path, prompt)
|
res_text = await self.vlm.analyze_image(image_path, prompt)
|
||||||
|
|||||||
@@ -64,19 +64,16 @@ class TeLaiDianService:
|
|||||||
logger.info(f"仅保存场站基础信息: {station_name}")
|
logger.info(f"仅保存场站基础信息: {station_name}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def save_station_data(self, station_name, address, prices):
|
async def save_station_data(self, station_name, address, prices, total_piles=None, free_piles=None, piles_detail=None, parking_info=None):
|
||||||
"""
|
"""
|
||||||
保存场站及其价格数据到数据库
|
保存场站全量数据:名称、地址、24小时价格计划、当前状态
|
||||||
"""
|
"""
|
||||||
if not prices:
|
|
||||||
return False
|
|
||||||
|
|
||||||
station_hash = self.get_hash(station_name)
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
station_hash = hashlib.md5(f"{station_name}{address}".encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
# 将价格转换为 24 小时的 schedule 格式 (0-23)
|
# 预处理价格:生成 24 小时的价格映射
|
||||||
# 每个小时存储一个包含多重价格的字典,以记录优惠价、PLUS价和挂牌价
|
|
||||||
hourly_schedule = [None] * 24
|
hourly_schedule = [None] * 24
|
||||||
|
|
||||||
for p in prices:
|
for p in prices:
|
||||||
try:
|
try:
|
||||||
start_parts = p['start'].split(':')
|
start_parts = p['start'].split(':')
|
||||||
@@ -142,9 +139,10 @@ class TeLaiDianService:
|
|||||||
session=session,
|
session=session,
|
||||||
id=status_id,
|
id=status_id,
|
||||||
station_hash=station_hash,
|
station_hash=station_hash,
|
||||||
total_piles=None, # 特来电暂时没抓取总桩数
|
total_piles=total_piles,
|
||||||
free_piles=None,
|
free_piles=free_piles,
|
||||||
piles_detail_json=None,
|
piles_detail_json=piles_detail,
|
||||||
|
parking_info=parking_info,
|
||||||
current_price=current_price_info.get('price'),
|
current_price=current_price_info.get('price'),
|
||||||
pro_price=current_price_info.get('plus_price'),
|
pro_price=current_price_info.get('plus_price'),
|
||||||
market_price=current_price_info.get('market_price'),
|
market_price=current_price_info.get('market_price'),
|
||||||
@@ -152,5 +150,5 @@ class TeLaiDianService:
|
|||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
logger.info(f"成功保存场站数据: {station_name}")
|
logger.info(f"成功保存场站数据: {station_name} (桩数: {total_piles}, 空闲: {free_piles}, 停车费: {parking_info})")
|
||||||
return True
|
return True
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user