This commit is contained in:
HuangHai
2026-01-12 08:26:13 +08:00
parent 4a45b27638
commit 7b4548c12c
13 changed files with 471 additions and 449 deletions

View File

@@ -16,18 +16,21 @@ if project_root not in sys.path:
import uiautomator2 as u2
from Core.BaseCrawler import BaseCrawler
from Util import Kit
from Util.Kit import take_screenshot
from Apps.XinDianTu import Kit
from Apps.XinDianTu.Kit import take_screenshot
from Util.ObsUtil import ObsUploader
from Util.RedisKit import RedisKit
from Util.PaddleOCRKit import get_ocr_kit
import cv2
from Apps.XinDianTu.Service import XinDianTuService
from Config.Config import (
OBS_TMP_PREFIX, CDN_DOMAIN, SCROLL_DISTANCE_RATIO,
OBS_TMP_PREFIX, CDN_DOMAIN, TEMP_IMAGE_DIR
)
from Apps.XinDianTu.Config.Setting import (
SCROLL_DISTANCE_RATIO,
MAX_SCROLLS, REDIS_STATION_EXPIRE,
WAIT_DETAIL_PAGE_LOAD, WAIT_BACK_TO_LIST, WAIT_AFTER_SCROLL,
MAX_CRAWL_DISTANCE, TEMP_IMAGE_DIR
MAX_CRAWL_DISTANCE
)
# --- 用户配置区域 ---
@@ -57,14 +60,15 @@ logger = logging.getLogger("StationList")
# 强制设置所有相关模块的日志级别为 INFO,防止被第三方库干扰
logging.getLogger("PaddleOCRKit").setLevel(logging.INFO)
logging.getLogger("Util.Kit").setLevel(logging.INFO)
logging.getLogger("OpenXinDianTu").setLevel(logging.INFO)
logging.getLogger("FullProcess").setLevel(logging.INFO)
class XinDianTuCrawler(BaseCrawler):
"""
新电途小程序爬虫实现
"""
def __init__(self, service=None):
super().__init__(service)
# 初始化配置参数
@@ -90,6 +94,7 @@ class XinDianTuCrawler(BaseCrawler):
async def crawl_detail(self, station_info):
pass
async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
"""
获取场站列表并处理翻页
@@ -140,7 +145,8 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
logger.info(f"截图实际尺寸: {img_w}x{img_h}")
# 如果截图尺寸与窗口尺寸不符,更新 device_info 以适配坐标换算
if img_w != w or img_h != h:
logger.warning(f"检测到截图尺寸 ({img_w}x{img_h}) 与窗口尺寸 ({w}x{h}) 不一致,将以截图尺寸为准进行坐标换算。")
logger.warning(
f"检测到截图尺寸 ({img_w}x{img_h}) 与窗口尺寸 ({w}x{h}) 不一致,将以截图尺寸为准进行坐标换算。")
device_info['width'] = img_w
device_info['height'] = img_h
except Exception as e:
@@ -158,7 +164,8 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
vl_img_path = screenshot_path.replace(".jpg", "_vl.jpg")
if not os.path.exists(json_path) or not os.path.exists(vl_img_path):
logger.error(f"❌ Step [2/6] 失败: 未生成有效的 JSON 或 VL 图片 (JSON: {os.path.exists(json_path)}, VL: {os.path.exists(vl_img_path)})")
logger.error(
f"❌ Step [2/6] 失败: 未生成有效的 JSON 或 VL 图片 (JSON: {os.path.exists(json_path)}, VL: {os.path.exists(vl_img_path)})")
logger.warning("可能未识别到任何卡片,跳过当前页")
continue
@@ -201,13 +208,13 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
cropped_card = original_img[y1:y2, x1:x2]
# 识别
logger.info(f"正在识别第 {idx+1}/{num_cards} 个卡片 (区域: {rect})...")
logger.info(f"正在识别第 {idx + 1}/{num_cards} 个卡片 (区域: {rect})...")
t_card_start = time.time()
parsed_data = ocr_kit.recognize(cropped_card)
t_card_end = time.time()
# Log detailed OCR result
logger.info(f"卡片 {idx+1} OCR 识别耗时: {t_card_end - t_card_start:.2f}s")
logger.info(f"卡片 {idx + 1} OCR 识别耗时: {t_card_end - t_card_start:.2f}s")
if parsed_data and parsed_data.get("station_name"):
# 格式化数据以匹配原有结构
@@ -232,7 +239,8 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
"tags": parsed_data.get("tags", []),
"parking_info": parsed_data.get("parking", "")
}
logger.info(f"✅ 成功解析场站: {station_info['station_name']} | 距离: {station_info['distance']}")
logger.info(
f"✅ 成功解析场站: {station_info['station_name']} | 距离: {station_info['distance']}")
# 立即记录场站基础状态信息 (电桩、价格等)
try:
@@ -242,12 +250,13 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
stations.append(station_info)
else:
logger.warning(f"⚠️ 卡片 {idx+1} 未能识别出有效场站名称")
logger.warning(f"⚠️ 卡片 {idx + 1} 未能识别出有效场站名称")
except Exception as e:
logger.error(f"❌ Local OCR processing failed: {e}", exc_info=True)
logger.info(f"Step [5/6] 本地 OCR 解析完成 (总耗时: {time.time() - t_ocr_total:.2f}s) | 成功识别: {len(stations)}/{len(json_metadata.get('cards', []))}")
logger.info(
f"Step [5/6] 本地 OCR 解析完成 (总耗时: {time.time() - t_ocr_total:.2f}s) | 成功识别: {len(stations)}/{len(json_metadata.get('cards', []))}")
if not stations:
logger.warning("当前页面未检测到任何有效的场站信息")
@@ -304,7 +313,8 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
dist_log_part = f" (解析: {dist_km:.2f}km / 限: {MAX_CRAWL_DISTANCE}km)"
if dist_km >= MAX_CRAWL_DISTANCE:
logger.info(f"当前场站 {name} 距离 {dist_km}km (原始: {distance_str}) 已达到或超过限制 {MAX_CRAWL_DISTANCE}km。")
logger.info(
f"当前场站 {name} 距离 {dist_km}km (原始: {distance_str}) 已达到或超过限制 {MAX_CRAWL_DISTANCE}km。")
max_distance_reached = True
except Exception as e:
logger.warning(f"解析距离出错: {distance_str}, {e}")
@@ -312,7 +322,8 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
pro_info = f" | PRO会员价: {pro_price}" if pro_price else ""
# 确保距离始终显示,如果为空则显示 "未知"
display_distance = distance_str if distance_str else "未知"
logger.info(f"正在处理第 {s_idx+1} 个场站: {name} | 价格: {price}{pro_info} | 枪: {piles_str} | 距离: {display_distance}{dist_log_part}")
logger.info(
f"正在处理第 {s_idx + 1} 个场站: {name} | 价格: {price}{pro_info} | 枪: {piles_str} | 距离: {display_distance}{dist_log_part}")
if max_distance_reached:
break
@@ -348,11 +359,13 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
if up_success:
detail_url = f"https://{CDN_DOMAIN}/{detail_object_key}"
# 3. 解析并保存地址 (异步后台处理)
# 定义异步任务函数
async def _process_address_task(task_name, task_url, task_dev_info, task_redis_key):
t_addr_task = time.time()
try:
addr_res = await service.process_station_address(task_name, task_url, device_info=task_dev_info)
addr_res = await service.process_station_address(task_name, task_url,
device_info=task_dev_info)
logger.info(f"Step [解析并保存地址-后台] 耗时: {time.time() - t_addr_task:.4f}s")
if addr_res.get("address"):
logger.info(f"成功记录地址: {addr_res.get('address')}")
@@ -387,7 +400,8 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
# 尝试识别,将调试图片保存到 Images 目录
t_find = time.time()
pos = Kit.find_expand_button_position(screenshot_path, debug_dir=save_dir, debug_filename_prefix=name)
pos = Kit.find_expand_button_position(screenshot_path, debug_dir=save_dir,
debug_filename_prefix=name)
logger.info(f"Step [识别展开按钮] 耗时: {time.time() - t_find:.4f}s")
# 清理截图
@@ -435,7 +449,8 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
async def _process_price_task(task_name, task_urls, task_dev_info):
t_price_task = time.time()
try:
await service.process_price_schedule(task_name, task_urls, device_info=task_dev_info)
await service.process_price_schedule(task_name, task_urls,
device_info=task_dev_info)
logger.info(f"Step [解析电费时段数据-后台] 耗时: {time.time() - t_price_task:.4f}s")
logger.info(f"电费时段数据解析完成: {task_name}")
except Exception as e:
@@ -491,6 +506,7 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
logger.info(f"任务结束,共采集到 {len(all_stations)} 个场站。停止原因: {stop_reason}")
return all_stations
def clean_images_dir():
"""清理 Images 目录下除保留文件外的所有文件"""
base_dir = os.path.dirname(os.path.abspath(__file__))
@@ -517,6 +533,7 @@ def clean_images_dir():
logger.info(f"清理完成,共删除 {deleted_count} 个文件。")
async def clean_redis_data(redis_kit):
"""清除 Redis 中的场站处理记录"""
if TEST_CLEAR_REDIS:
@@ -528,6 +545,7 @@ async def clean_redis_data(redis_kit):
else:
logger.info("未发现旧的场站处理记录")
async def main(service=None, do_cleanup=True):
# 连接设备
d = u2.connect()
@@ -572,6 +590,7 @@ async def main(service=None, do_cleanup=True):
if should_close_service:
await service.close_db()
if __name__ == "__main__":
try:
asyncio.run(main())

View File

@@ -3,11 +3,13 @@ import os
import cv2
import numpy as np
import time
from Config.Config import BOTTOM_SAFE_EXCLUDE_RATIO, TEMP_IMAGE_DIR
from Apps.XinDianTu.Config.Setting import BOTTOM_SAFE_EXCLUDE_RATIO
from Config.Config import TEMP_IMAGE_DIR
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
def read_image(path):
"""读取图片,支持中文路径"""
try:
@@ -16,6 +18,7 @@ def read_image(path):
logger.info(f"Error reading image {path}: {e}")
return None
def save_image(path, img):
"""保存图片,支持中文路径"""
try:
@@ -28,6 +31,7 @@ def save_image(path, img):
logger.error(f"Error saving image {path}: {e}")
return False
# 截图
def take_screenshot(d, image_uuid, save_dir=TEMP_IMAGE_DIR):
path = f"{save_dir}/{image_uuid}.jpg"
@@ -35,6 +39,7 @@ def take_screenshot(d, image_uuid, save_dir=TEMP_IMAGE_DIR):
d.screenshot(path)
return path
def click_image_template(d, template_path, timeout=5.0, threshold=0.8):
"""
使用 OpenCV 模板匹配查找并点击图片
@@ -112,6 +117,7 @@ def click_image_template(d, template_path, timeout=5.0, threshold=0.8):
logger.info(f"Image not found after {timeout}s (Best confidence: {best_val_overall:.2f})")
return False
def detect_black_agree_button(image_path, debug_dir=None):
"""
通过计算机图形学检测黑色的"同意"按钮 (Image 1 场景)
@@ -186,7 +192,7 @@ def detect_black_agree_button(image_path, debug_dir=None):
if debug_dir:
os.makedirs(debug_dir, exist_ok=True)
debug_img = img.copy()
cv2.rectangle(debug_img, (x, y), (x+cw, y+ch), (0, 0, 255), 2)
cv2.rectangle(debug_img, (x, y), (x + cw, y + ch), (0, 0, 255), 2)
cv2.circle(debug_img, (center_x, center_y), 5, (0, 255, 0), -1)
save_image(os.path.join(debug_dir, "debug_agree_btn.jpg"), debug_img)
@@ -194,6 +200,7 @@ def detect_black_agree_button(image_path, debug_dir=None):
return None
def detect_ad_close_x(image_path, template_path, debug_dir=None, threshold=0.7):
"""
通过模板匹配检测"关闭(X)"按钮 (Image 2 场景)
@@ -264,7 +271,8 @@ def detect_ad_close_x(image_path, template_path, debug_dir=None, threshold=0.7):
rel_x = center_x / target_w
rel_y = center_y / target_h
logger.info(f"Ad Close Button Match: Confidence={max_val:.4f}, Scale={best_scale:.2f}, Pos=({center_x}, {center_y}), Rel=({rel_x:.2f}, {rel_y:.2f})")
logger.info(
f"Ad Close Button Match: Confidence={max_val:.4f}, Scale={best_scale:.2f}, Pos=({center_x}, {center_y}), Rel=({rel_x:.2f}, {rel_y:.2f})")
is_valid_pos = True
@@ -279,7 +287,8 @@ def detect_ad_close_x(image_path, template_path, debug_dir=None, threshold=0.7):
# 底部中间: Y > 0.6
if is_valid_pos and max_val < 0.98: # 如果置信度非常高(0.98+),可能是极其标准的X,暂时放过(除非在禁区)
if not ((rel_x > 0.7 and rel_y < 0.5) or (rel_y > 0.6)):
logger.warning(f"Ignored match at ({center_x}, {center_y}) - not in typical Close Button regions (Top-Right or Bottom).")
logger.warning(
f"Ignored match at ({center_x}, {center_y}) - not in typical Close Button regions (Top-Right or Bottom).")
is_valid_pos = False
if max_val >= threshold:
@@ -287,12 +296,14 @@ def detect_ad_close_x(image_path, template_path, debug_dir=None, threshold=0.7):
# [Safety Check] 底部安全区排除
# 如果检测到的关闭按钮位于屏幕底部 BOTTOM_SAFE_EXCLUDE_RATIO 区域内,认为是误判(如误触底部功能按钮)
if center_y > (target_h * (1 - BOTTOM_SAFE_EXCLUDE_RATIO)):
logger.warning(f"Ignored Ad Close Button at ({center_x}, {center_y}) - in Bottom Safety Zone ({int(BOTTOM_SAFE_EXCLUDE_RATIO*100)}%).")
logger.warning(
f"Ignored Ad Close Button at ({center_x}, {center_y}) - in Bottom Safety Zone ({int(BOTTOM_SAFE_EXCLUDE_RATIO * 100)}%).")
# 也可以保存一下调试图
if debug_dir:
os.makedirs(debug_dir, exist_ok=True)
debug_img = target.copy()
cv2.rectangle(debug_img, top_left, (top_left[0] + best_w, top_left[1] + best_h), (0, 0, 128), 2) # Dark Red for Safety Ignored
cv2.rectangle(debug_img, top_left, (top_left[0] + best_w, top_left[1] + best_h), (0, 0, 128),
2) # Dark Red for Safety Ignored
cv2.putText(debug_img, f"SAFETY IGNORED",
(top_left[0], top_left[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 128), 1)
save_image(os.path.join(debug_dir, "debug_ad_close_safety_ignored.jpg"), debug_img)
@@ -315,7 +326,8 @@ def detect_ad_close_x(image_path, template_path, debug_dir=None, threshold=0.7):
if debug_dir:
os.makedirs(debug_dir, exist_ok=True)
debug_img = target.copy()
cv2.rectangle(debug_img, top_left, (top_left[0] + best_w, top_left[1] + best_h), (0, 165, 255), 2) # Orange for ignored
cv2.rectangle(debug_img, top_left, (top_left[0] + best_w, top_left[1] + best_h), (0, 165, 255),
2) # Orange for ignored
cv2.putText(debug_img, f"IGNORED Pos",
(top_left[0], top_left[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 1)
save_image(os.path.join(debug_dir, "debug_ad_close_ignored.jpg"), debug_img)
@@ -332,6 +344,7 @@ def detect_ad_close_x(image_path, template_path, debug_dir=None, threshold=0.7):
return None
def detect_any_ad_close(image_path, template_dir, debug_dir=None):
"""
遍历模板目录下的所有 ad_close*.jpg 进行匹配
@@ -348,6 +361,7 @@ def detect_any_ad_close(image_path, template_dir, debug_dir=None):
return pos
return None
def detect_bottom_close_circle(image_path, debug_dir=None):
"""
通过几何特征检测底部的圆形关闭按钮 (常见于插屏广告)
@@ -379,8 +393,8 @@ def detect_bottom_close_circle(image_path, debug_dir=None):
# 2. 霍夫圆变换检测圆形
# dp=1.2 (累加器分辨率), minDist=w/5 (圆心最小距离), param1=100 (Canny高阈值), param2=30 (圆心累加阈值), minR=w*0.04, maxR=w*0.1
# 缩小最大半径范围,防止识别到过大的按钮(如扫码充电按钮)
circles = cv2.HoughCircles(gray_blurred, cv2.HOUGH_GRADIENT, dp=1.2, minDist=w/5,
param1=100, param2=30, minRadius=int(w*0.04), maxRadius=int(w*0.09))
circles = cv2.HoughCircles(gray_blurred, cv2.HOUGH_GRADIENT, dp=1.2, minDist=w / 5,
param1=100, param2=30, minRadius=int(w * 0.04), maxRadius=int(w * 0.09))
if circles is not None:
circles = np.round(circles[0, :]).astype("int")
@@ -394,7 +408,7 @@ def detect_bottom_close_circle(image_path, debug_dir=None):
global_cx = cx
# 过滤1: 必须在水平中心附近 (容差 15%,收紧限制)
if abs(global_cx - w//2) > (w * 0.15):
if abs(global_cx - w // 2) > (w * 0.15):
continue
# 过滤2: 垂直方向限制 (必须在屏幕 80% - 95% 之间)
@@ -403,7 +417,7 @@ def detect_bottom_close_circle(image_path, debug_dir=None):
if rel_y < 0.80 or rel_y > 0.95:
continue
dist = abs(global_cx - w//2)
dist = abs(global_cx - w // 2)
if dist < min_dist_to_center:
min_dist_to_center = dist
best_circle = (global_cx, global_cy, r)
@@ -416,7 +430,8 @@ def detect_bottom_close_circle(image_path, debug_dir=None):
# [Safety Check] 底部安全区排除
# 如果检测到的圆形按钮位于屏幕底部 BOTTOM_SAFE_EXCLUDE_RATIO 区域内,认为是误判(如误触底部扫码充电等)
if cy > (h * (1 - BOTTOM_SAFE_EXCLUDE_RATIO)):
logger.warning(f"Ignored Bottom Circle at ({cx}, {cy}) - in Bottom Safety Zone ({int(BOTTOM_SAFE_EXCLUDE_RATIO*100)}%).")
logger.warning(
f"Ignored Bottom Circle at ({cx}, {cy}) - in Bottom Safety Zone ({int(BOTTOM_SAFE_EXCLUDE_RATIO * 100)}%).")
return None
logger.info(f"Found Bottom Circle Button via Hough: ({cx}, {cy}), r={r}")
@@ -432,6 +447,7 @@ def detect_bottom_close_circle(image_path, debug_dir=None):
return None
def find_expand_button_position(image_path, debug_dir=None, debug_filename_prefix=None):
"""
通过几何特征识别"全部时段"按钮的位置
@@ -572,7 +588,7 @@ def get_row_stats(gray):
# 中央区域 (用于检测内容)
center_x = w // 2
strip_w = 100
center_strip = gray[:, center_x - 50 : center_x + 50]
center_strip = gray[:, center_x - 50: center_x + 50]
# 边缘区域 (用于检测背景/边距)
# 假设边距至少有 10px
@@ -584,6 +600,7 @@ def get_row_stats(gray):
return row_means, row_stds, edge_means
def crop_cards_from_image(img_path, output_dir=None, save_debug=True):
"""
从图片中裁剪场站卡片
@@ -718,7 +735,7 @@ def crop_cards_from_image(img_path, output_dir=None, save_debug=True):
if segments:
current_group = [segments[0]]
for i in range(1, len(segments)):
prev_end = segments[i-1][1]
prev_end = segments[i - 1][1]
curr_start = segments[i][0]
gap = curr_start - prev_end
@@ -765,7 +782,7 @@ def crop_cards_from_image(img_path, output_dir=None, save_debug=True):
# 取中间一段行来分析
mid_y = (y1 + y2) // 2
sample_h = min(10, y2 - y1)
sample_rows = gray[mid_y - sample_h//2 : mid_y + sample_h//2, :]
sample_rows = gray[mid_y - sample_h // 2: mid_y + sample_h // 2, :]
col_means = np.mean(sample_rows, axis=0)
# 从左向右找第一个非灰像素
@@ -785,7 +802,7 @@ def crop_cards_from_image(img_path, output_dir=None, save_debug=True):
if x1 >= x2 or (x2 - x1) < w * 0.5:
# 如果这个 segment 无法确定宽度,可能不是有效卡片,暂不参与宽度投票
# 但为了不漏掉,暂时先记录,用默认值填充
logger.warning(f" Warning: Segment {y1}-{y2} has weird width {x2-x1}.")
logger.warning(f" Warning: Segment {y1}-{y2} has weird width {x2 - x1}.")
pass
else:
candidate_x1.append(x1)
@@ -831,7 +848,8 @@ def crop_cards_from_image(img_path, output_dir=None, save_debug=True):
for (y1, y2), h in zip(temp_valid_segments, heights):
if h < threshold_h:
logger.info(f" Filtering out segment Y={y1}-{y2} (H={h}) because it's too short (Threshold={threshold_h:.1f}).")
logger.info(
f" Filtering out segment Y={y1}-{y2} (H={h}) because it's too short (Threshold={threshold_h:.1f}).")
else:
final_cards.append((y1, y2, final_x1, final_x2))
logger.info(f" Card: Y={y1}-{y2}, X={final_x1}-{final_x2}, H={h}")

View File

@@ -1,14 +1,15 @@
# coding=utf-8
import asyncio
import logging
import os
import time
import uuid
import os
import uiautomator2 as u2
from Util.Kit import take_screenshot, detect_black_agree_button, click_image_template, detect_ad_close_x, detect_any_ad_close, detect_bottom_close_circle
from Util.ObsUtil import ObsUploader
from Util.XinDianTuReadImageKit import XinDianTuReadImageKit
from Config.Config import OBS_TMP_PREFIX, CDN_DOMAIN, TEMP_IMAGE_DIR
from Apps.XinDianTu.Kit import take_screenshot, detect_black_agree_button, detect_any_ad_close, \
detect_bottom_close_circle
from Config.Config import TEMP_IMAGE_DIR
# pip install adbutils
# 配置日志输出,方便调试和监控

View File

@@ -12,8 +12,6 @@ if project_root not in sys.path:
from Apps.XinDianTu.Service import XinDianTuService
from Apps.XinDianTu import Opener
from Apps.XinDianTu import Crawler
from Apps.XinDianTu import Coord as Y4_Coord
from Apps.XinDianTu import GetCookie as T2_GetAmapCookie
# 配置日志
logging.basicConfig(
@@ -60,35 +58,6 @@ async def main():
logger.error(f"❌ 步骤 2 执行异常: {e}")
return
# 步骤 3: 补全坐标 (调用 Y4_Coord)
logger.info(">>> 步骤 3: 启动高德地图坐标回填任务...")
try:
# 首次尝试,开启 Cookie 检查
await Y4_Coord.main(check_cookie_first=True)
except Y4_Coord.CookieInvalidError:
logger.warning("⚠️ 检测到高德地图 Cookie 失效或缺失。")
logger.info(">>> 正在启动 Cookie 获取工具 (T2)...")
logger.info("请在弹出的浏览器窗口中完成登录,然后在控制台按回车确认。")
# 调用同步的 T2 工具
# 由于 T2 包含 input() 阻塞,且这是客户端脚本,直接调用即可
try:
# 传入 force=True 跳过“文件已存在”的询问,因为我们确定它无效
# run_in_executor 用于避免完全阻塞事件循环(虽然在这里影响不大)
loop = asyncio.get_event_loop()
success = await loop.run_in_executor(None, T2_GetAmapCookie.run_login_tool, True)
if success:
logger.info("✅ Cookie 更新成功,正在重新启动坐标回填任务...")
# 再次尝试执行,这次可以跳过检查(刚获取完)或者继续检查
await Y4_Coord.main(check_cookie_first=False)
else:
logger.error("❌ Cookie 获取失败或用户取消,跳过坐标回填步骤。")
except Exception as e:
logger.error(f"❌ 调用 T2 工具失败: {e}")
except Exception as e:
logger.error(f"❌ 步骤 3 执行异常: {e}")
finally:
# 确保数据库连接关闭
if service:

View File

@@ -1,19 +1,17 @@
import hashlib
import logging
import os
import re
import sys
import uuid
import logging
import json
import re
from datetime import datetime, timedelta
from datetime import datetime
# Ensure sys path includes root for imports if not already
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if project_root not in sys.path:
sys.path.append(project_root)
from sqlalchemy import text
from Util.XinDianTuReadImageKit import XinDianTuReadImageKit
from Apps.XinDianTu.XinDianTuReadImageKit import XinDianTuReadImageKit
from DbKit.Db import Db
from Config.Config import DB_URL
from Model.StationProfile import StationProfile

View File

@@ -9,13 +9,16 @@ import logging
import base64
from openai import OpenAI, BadRequestError
from Config.Config import (
ALY_LLM_API_KEY, VL_MODEL_NAME, VL_MODEL_NAME_AD,
ALY_LLM_API_KEY, VL_MODEL_NAME, VL_MODEL_NAME_AD
)
from Apps.XinDianTu.Config.Setting import (
SAFE_EXCLUDE_RATIO, FALLBACK_WIDTH, FALLBACK_HEIGHT,
BOTTOM_SAFE_EXCLUDE_RATIO
)
logger = logging.getLogger(__name__)
class XinDianTuReadImageKit:
_client = OpenAI(
api_key=ALY_LLM_API_KEY,
@@ -152,16 +155,20 @@ class XinDianTuReadImageKit:
return min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)
# 优先使用场站名称区域进行点击 (最安全)
coords = get_pixel_coords(text_bn, display_width, display_height) or get_pixel_coords(text_bounds, display_width, display_height)
coords = get_pixel_coords(text_bn, display_width, display_height) or get_pixel_coords(text_bounds,
display_width,
display_height)
if coords:
x1, y1, x2, y2 = coords
uia_x = int(x1 + (x2 - x1) / 2)
uia_y = int(y1 + (y2 - y1) / 2)
logger.info(f"坐标计算: 使用文字区域 -> ({uia_x}, {uia_y}) | 区域: {coords} | 屏幕: {display_width}x{display_height}")
logger.info(
f"坐标计算: 使用文字区域 -> ({uia_x}, {uia_y}) | 区域: {coords} | 屏幕: {display_width}x{display_height}")
# 备选:使用整个卡片区域
if uia_x is None:
coords = get_pixel_coords(bn, display_width, display_height) or get_pixel_coords(bounds, display_width, display_height)
coords = get_pixel_coords(bn, display_width, display_height) or get_pixel_coords(bounds, display_width,
display_height)
if coords:
x1, y1, x2, y2 = coords
w, h = x2 - x1, y2 - y1
@@ -172,18 +179,21 @@ class XinDianTuReadImageKit:
# 默认中心点,但稍微偏上一点,避开底部可能的按钮
uia_x = int(x1 + w / 2)
uia_y = int(y1 + h * 0.4)
logger.info(f"坐标计算: 使用卡片区域 -> ({uia_x}, {uia_y}) | 区域: {coords} | 屏幕: {display_width}x{display_height}")
logger.info(
f"坐标计算: 使用卡片区域 -> ({uia_x}, {uia_y}) | 区域: {coords} | 屏幕: {display_width}x{display_height}")
# --- 安全过滤:过滤掉屏幕顶部的点击坐标 (通常是广告或菜单) ---
if uia_y is not None:
# 如果点击点在屏幕顶部 SAFE_EXCLUDE_RATIO 范围内,极大概率是误触广告位,将其排除
if uia_y < (display_height * SAFE_EXCLUDE_RATIO):
logger.warning(f"安全排除: 坐标 ({uia_x}, {uia_y}) 位于屏幕顶部 {int(SAFE_EXCLUDE_RATIO*100)}% 区域,疑似广告或菜单,已忽略。")
logger.warning(
f"安全排除: 坐标 ({uia_x}, {uia_y}) 位于屏幕顶部 {int(SAFE_EXCLUDE_RATIO * 100)}% 区域,疑似广告或菜单,已忽略。")
return None
# 如果点击点在屏幕底部 BOTTOM_SAFE_EXCLUDE_RATIO 范围内,极大概率是误触底部功能区(如扫码充电),将其排除
if uia_y > (display_height * (1 - BOTTOM_SAFE_EXCLUDE_RATIO)):
logger.warning(f"安全排除: 坐标 ({uia_x}, {uia_y}) 位于屏幕底部 {int(BOTTOM_SAFE_EXCLUDE_RATIO*100)}% 区域,疑似底部功能区,已忽略。")
logger.warning(
f"安全排除: 坐标 ({uia_x}, {uia_y}) 位于屏幕底部 {int(BOTTOM_SAFE_EXCLUDE_RATIO * 100)}% 区域,疑似底部功能区,已忽略。")
return None
if uia_x is not None and uia_y is not None:
@@ -293,7 +303,9 @@ class XinDianTuReadImageKit:
try:
data = json.loads(raw)
if isinstance(data, dict) and (data.get("station_name_bounds") or data.get("station_name_bounds_norm") or data.get("bounds") or data.get("bounds_norm")):
if isinstance(data, dict) and (
data.get("station_name_bounds") or data.get("station_name_bounds_norm") or data.get(
"bounds") or data.get("bounds_norm")):
data = XinDianTuReadImageKit._add_click_point(data, device_info, anchor="station_text")
return {
"uia_center_x": data.get("uia_center_x"),
@@ -339,7 +351,8 @@ class XinDianTuReadImageKit:
data = json.loads(raw)
if isinstance(data, list):
# 过滤掉 None (即被 _add_click_point 排除掉的顶部项)
data = [XinDianTuReadImageKit._add_click_point(x, device_info, anchor="station_text") for x in data if x.get("b_use", 1) == 1]
data = [XinDianTuReadImageKit._add_click_point(x, device_info, anchor="station_text") for x in data if
x.get("b_use", 1) == 1]
data = [x for x in data if x is not None]
elif isinstance(data, dict):
if data.get("b_use", 1) == 1:
@@ -389,7 +402,8 @@ class XinDianTuReadImageKit:
if tasks:
results = await asyncio.gather(*tasks, return_exceptions=True)
for i, r in zip(idxs, results):
if isinstance(r, dict) and r.get("uia_center_x") is not None and r.get("uia_center_y") is not None:
if isinstance(r, dict) and r.get("uia_center_x") is not None and r.get(
"uia_center_y") is not None:
data[i]["uia_center_x"] = r.get("uia_center_x")
data[i]["uia_center_y"] = r.get("uia_center_y")
@@ -491,12 +505,14 @@ class XinDianTuReadImageKit:
# 顶部安全排除
if cy < (img_h * SAFE_EXCLUDE_RATIO):
logger.warning(f"VL安全排除: 坐标 ({cx}, {cy}) 位于屏幕顶部 {int(SAFE_EXCLUDE_RATIO*100)}% 区域,已忽略。")
logger.warning(
f"VL安全排除: 坐标 ({cx}, {cy}) 位于屏幕顶部 {int(SAFE_EXCLUDE_RATIO * 100)}% 区域,已忽略。")
continue
# 底部安全排除
if cy > (img_h * (1 - BOTTOM_SAFE_EXCLUDE_RATIO)):
logger.warning(f"VL安全排除: 坐标 ({cx}, {cy}) 位于屏幕底部 {int(BOTTOM_SAFE_EXCLUDE_RATIO*100)}% 区域,疑似底部功能区,已忽略。")
logger.warning(
f"VL安全排除: 坐标 ({cx}, {cy}) 位于屏幕底部 {int(BOTTOM_SAFE_EXCLUDE_RATIO * 100)}% 区域,疑似底部功能区,已忽略。")
continue
item["uia_center_x"] = cx
@@ -694,7 +710,7 @@ class XinDianTuReadImageKit:
# 计算左右边距的平均亮度,辅助判断(仅记录日志,不强行过滤)
margin_w = max(5, int(width * 0.05))
l_margin = img_data[y1:y2, 0:margin_w]
r_margin = img_data[y1:y2, width-margin_w:width]
r_margin = img_data[y1:y2, width - margin_w:width]
l_mean = np.mean(l_margin)
r_mean = np.mean(r_margin)
@@ -727,7 +743,7 @@ class XinDianTuReadImageKit:
# 如果裁剪后宽度太小,可能不是有效卡片
if (x2 - x1) < width * 0.5:
logger.info(f" [-] 忽略区域 {i}: 裁剪后宽度过小 ({x2-x1})")
logger.info(f" [-] 忽略区域 {i}: 裁剪后宽度过小 ({x2 - x1})")
continue
# 保存
@@ -743,7 +759,8 @@ class XinDianTuReadImageKit:
"center_y": center_y,
"y_range": (y1, y2)
})
logger.info(f" [+] 发现卡片 {i}: Y({y1}-{y2}), H={h}, Crop X({x1}-{x2}), 边缘(L={l_mean:.1f}, R={r_mean:.1f}), 已保存")
logger.info(
f" [+] 发现卡片 {i}: Y({y1}-{y2}), H={h}, Crop X({x1}-{x2}), 边缘(L={l_mean:.1f}, R={r_mean:.1f}), 已保存")
logger.info(f"分析完成:识别到 {len(valid_segments)} 个区域")
return valid_segments
@@ -1284,7 +1301,7 @@ async def test3(station_name: str):
if __name__ == "__main__":
#asyncio.run(test1())
# asyncio.run(test1())
station_name = '吉林省看守所充电站'
#asyncio.run(test2(station_name))
# asyncio.run(test2(station_name))
asyncio.run(test3(station_name))

Binary file not shown.