'commit'
This commit is contained in:
@@ -279,8 +279,7 @@ async def get_station_list(d, service, uploader, max_stations_count=MAX_STATIONS
|
||||
loop.run_in_executor(None, uploader.upload_file, detail_object_key, detail_path)
|
||||
|
||||
# --- 抓取价格时段信息 ---
|
||||
# 【优化】梯级识别策略:优先模板匹配 (qbsd.jpg),失败则降级为 VL 识别
|
||||
template_qbsd = os.path.join(os.path.dirname(__file__), "Template", "qbsd.jpg")
|
||||
# 【优化】使用 OCR 识别“全部时段”按钮,替代之前的模板匹配 (qbsd.jpg)
|
||||
|
||||
# 记录点击前的页面特征,用于验证是否成功进入三级页面
|
||||
before_click_md5 = Kit.get_image_content_md5(detail_path)
|
||||
@@ -289,28 +288,20 @@ async def get_station_list(d, service, uploader, max_stations_count=MAX_STATIONS
|
||||
click_pos = None
|
||||
|
||||
try:
|
||||
# 1. 尝试模板匹配
|
||||
match_res = d.image.match(template_qbsd)
|
||||
if match_res:
|
||||
# 兼容 uiautomator2 不同版本的返回结果 (Match对象或dict)
|
||||
if hasattr(match_res, 'point') and match_res.point:
|
||||
click_pos = match_res.point
|
||||
elif isinstance(match_res, dict):
|
||||
if 'point' in match_res and match_res['point']:
|
||||
click_pos = match_res['point']
|
||||
elif 'x' in match_res and 'y' in match_res:
|
||||
click_pos = (match_res['x'], match_res['y'])
|
||||
elif isinstance(match_res, (list, tuple)) and len(match_res) >= 2:
|
||||
click_pos = (match_res[0], match_res[1])
|
||||
|
||||
if click_pos:
|
||||
logger.info(f"通过 qbsd.jpg 成功找到!坐标: {click_pos}")
|
||||
else:
|
||||
logger.warning(f"模板匹配成功但无法解析坐标: {match_res}")
|
||||
# 1. 尝试 OCR 识别
|
||||
logger.info("正在使用 OCR 识别 '全部时段' 按钮...")
|
||||
ocr_res = await ReadImageKit.find_price_entrance_ocr(detail_path)
|
||||
|
||||
# 如果模板匹配未成功解析坐标,或者匹配直接失败,则降级到 VL
|
||||
if ocr_res.get("found"):
|
||||
p = ocr_res["point"]
|
||||
# 归一化坐标转像素坐标
|
||||
w, h = d.window_size()
|
||||
click_pos = (int(p[0] * w / 1000), int(p[1] * h / 1000))
|
||||
logger.info(f"OCR 成功找到 '全部时段' 按钮: {click_pos}")
|
||||
|
||||
# 2. 如果 OCR 未找到,则降级到 VL
|
||||
if not click_pos:
|
||||
logger.info("模板匹配未找到或解析失败,降级使用 VL 识别...")
|
||||
logger.info("OCR 未找到,降级使用 VL 识别...")
|
||||
res = await ReadImageKit.find_all_time_button_coordinate(detail_path, device_info=device_info)
|
||||
if res.get("uia_center_x"):
|
||||
click_pos = (res.get("uia_center_x"), res.get("uia_center_y"))
|
||||
@@ -327,13 +318,50 @@ async def get_station_list(d, service, uploader, max_stations_count=MAX_STATIONS
|
||||
if before_click_md5 != after_click_md5:
|
||||
entered_price_page = True
|
||||
price_screenshots = []
|
||||
for p_idx in range(1, 4):
|
||||
|
||||
# 1. 向下滚动到底 (根据用户反馈:只有不断向下滚动,才能看到00点的)
|
||||
logger.info("正在向下滚动价格列表到底部...")
|
||||
max_scroll_down = 6
|
||||
for i in range(max_scroll_down):
|
||||
before_scroll_path = take_screenshot(d, f"scroll_dn_{i}", save_dir=TEMP_IMAGE_DIR)
|
||||
before_scroll_md5 = Kit.get_image_content_md5(before_scroll_path)
|
||||
|
||||
d.swipe_ext("up", scale=0.8)
|
||||
await asyncio.sleep(1.2)
|
||||
|
||||
after_scroll_path = take_screenshot(d, f"scroll_dn_after_{i}", save_dir=TEMP_IMAGE_DIR)
|
||||
after_scroll_md5 = Kit.get_image_content_md5(after_scroll_path)
|
||||
|
||||
# 清理临时截图
|
||||
if os.path.exists(before_scroll_path): os.remove(before_scroll_path)
|
||||
if os.path.exists(after_scroll_path): os.remove(after_scroll_path)
|
||||
|
||||
if before_scroll_md5 == after_scroll_md5:
|
||||
logger.info(f"价格列表已到达底部 (滚动次数: {i})")
|
||||
break
|
||||
|
||||
# 2. 向上滚动并逐页截图 (从底向上抓取)
|
||||
logger.info("正在向上滚动价格列表并逐页截图...")
|
||||
max_scroll_up = 8
|
||||
for p_idx in range(1, max_scroll_up + 1):
|
||||
# 截图当前页
|
||||
p_uuid = f"{hashlib.md5(station_name.encode('utf-8')).hexdigest()}_p_{p_idx}"
|
||||
p_path = take_screenshot(d, p_uuid, save_dir=TEMP_IMAGE_DIR)
|
||||
price_screenshots.append(p_path)
|
||||
if p_idx < 3:
|
||||
d.swipe_ext("up", scale=0.7)
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
# 检查是否还能向上滚动
|
||||
before_up_md5 = Kit.get_image_content_md5(p_path)
|
||||
d.swipe_ext("down", scale=0.85) # 向上滚动 = 手势向下
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
# 检查是否还有新内容
|
||||
check_up_path = take_screenshot(d, f"check_up_{p_idx}", save_dir=TEMP_IMAGE_DIR)
|
||||
after_up_md5 = Kit.get_image_content_md5(check_up_path)
|
||||
if os.path.exists(check_up_path): os.remove(check_up_path)
|
||||
|
||||
if before_up_md5 == after_up_md5:
|
||||
logger.info(f"价格列表已到达顶部 (共抓取页数: {p_idx})")
|
||||
break
|
||||
|
||||
# 【异步优化】后台处理价格
|
||||
logger.info(f"已启动后台分析价格页: {station_name}")
|
||||
@@ -367,12 +395,13 @@ async def get_station_list(d, service, uploader, max_stations_count=MAX_STATIONS
|
||||
logger.warning(f"返回后状态未知 (ListMD5:{check_list_md5[:8]} vs {current_md5[:8]}, DetailMD5:{check_detail_md5[:8]} vs {before_click_md5[:8]})。")
|
||||
logger.info("再次尝试识别按钮以确认当前页面状态...")
|
||||
|
||||
# 优先尝试模板匹配确认
|
||||
if d.image.match(template_qbsd):
|
||||
logger.info("模板匹配确认当前仍处于详情页,执行返回。")
|
||||
# 优先尝试 OCR 确认
|
||||
ocr_check = await ReadImageKit.find_price_entrance_ocr(check_back_path)
|
||||
if ocr_check.get("found"):
|
||||
logger.info("OCR 确认当前仍处于详情页,执行返回。")
|
||||
should_back_to_list = True
|
||||
else:
|
||||
# 模板匹配未发现,则用 VL 兜底确认
|
||||
# OCR 未发现,则用 VL 兜底确认
|
||||
check_res = await ReadImageKit.find_all_time_button_coordinate(check_back_path, device_info=device_info)
|
||||
if check_res and check_res.get("uia_center_x"):
|
||||
logger.info("VL 确认当前仍处于详情页,执行返回。")
|
||||
|
||||
@@ -137,6 +137,43 @@ def is_background_dimmed(image_path, threshold=80):
|
||||
return avg_brightness < threshold
|
||||
|
||||
|
||||
from Util.EasyOcrKit import get_easyocr_reader
|
||||
|
||||
# 预加载 EasyOCR Reader (单例模式)
|
||||
def get_ocr_reader():
|
||||
return get_easyocr_reader(gpu=True)
|
||||
|
||||
|
||||
def detect_price_info_container_cv(image_path):
|
||||
"""
|
||||
使用 OCR 精准定位详情页中的价格入口文本(“全部时段”)。
|
||||
返回: [x1, y1, x2, y2] 归一化坐标 (0-1000),如果未找到则返回 None
|
||||
"""
|
||||
img = read_image(image_path)
|
||||
if img is None:
|
||||
return None
|
||||
h, w = img.shape[:2]
|
||||
|
||||
keywords = ['全部时段']
|
||||
|
||||
try:
|
||||
reader = get_ocr_reader()
|
||||
# 获取所有识别结果
|
||||
results = reader.read_text(img)
|
||||
|
||||
for (quad, text, prob) in results:
|
||||
# 检查是否包含关键字
|
||||
if any(kw in text for kw in keywords) and prob >= 0.5:
|
||||
# 使用封装后的方法计算归一化矩形
|
||||
res = reader.get_normalized_rect(quad, w, h)
|
||||
logger.info(f"[OCR识别] 找到文本: '{text}', 置信度: {prob:.4f}, 归一化坐标: {res}")
|
||||
return res
|
||||
except Exception as e:
|
||||
logger.error(f"OCR 识别发生异常: {e}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def detect_rabbit_ad_close(image_path, debug_dir=None):
|
||||
"""
|
||||
通过图形学算法检测“新电兔AI”广告的关闭按钮
|
||||
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
import base64
|
||||
from openai import AsyncOpenAI, BadRequestError
|
||||
from Apps.XinDianTu.Kit import (
|
||||
read_image, save_image, detect_rabbit_ad_close
|
||||
read_image, save_image, detect_rabbit_ad_close, detect_price_info_container_cv
|
||||
)
|
||||
from Config.Config import (
|
||||
ALY_LLM_API_KEY, VL_MODEL_NAME, VL_MODEL_NAME_AD, TEMP_IMAGE_DIR
|
||||
@@ -77,6 +77,28 @@ class ReadImageKit:
|
||||
"严格返回纯JSON格式。"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def find_price_entrance_ocr(cls, image_path):
|
||||
"""
|
||||
使用 OCR 在详情页寻找价格入口 (全部时段 >)
|
||||
"""
|
||||
container_norm = detect_price_info_container_cv(image_path)
|
||||
if container_norm:
|
||||
# 找到矩形框 [x1, y1, x2, y2]
|
||||
x1, y1, x2, y2 = container_norm
|
||||
# 计算中心点
|
||||
center_x = (x1 + x2) // 2
|
||||
center_y = (y1 + y2) // 2
|
||||
|
||||
return {
|
||||
"found": True,
|
||||
"reason": "OCR 精准定位到价格入口关键字 '全部时段'",
|
||||
"point": [center_x, center_y]
|
||||
}
|
||||
|
||||
logger.warning(f"⚠️ [OCR识别失败] 在图片中未发现价格入口关键字 (全部时段): {image_path}")
|
||||
return {"found": False}
|
||||
|
||||
@classmethod
|
||||
async def detect_ad_popup(cls, image_path: str, device_info=None):
|
||||
"""
|
||||
|
||||
0
Apps/XinDianTu/Test/testOcrX.py
Normal file
0
Apps/XinDianTu/Test/testOcrX.py
Normal file
Binary file not shown.
Reference in New Issue
Block a user