From 4ddada009b4843e88718b8d8a11038566b708905 Mon Sep 17 00:00:00 2001 From: HuangHai <10402852@qq.com> Date: Tue, 13 Jan 2026 15:02:32 +0800 Subject: [PATCH] 'commit' --- Apps/XinDianTu/Crawler.py | 145 +++++++++++++++---------------- Apps/XinDianTu/Doc/流程说明.md | 6 ++ Apps/XinDianTu/Template/qbsd.jpg | Bin 0 -> 4107 bytes 3 files changed, 77 insertions(+), 74 deletions(-) create mode 100644 Apps/XinDianTu/Template/qbsd.jpg diff --git a/Apps/XinDianTu/Crawler.py b/Apps/XinDianTu/Crawler.py index 6717d86..9d900e7 100644 --- a/Apps/XinDianTu/Crawler.py +++ b/Apps/XinDianTu/Crawler.py @@ -313,89 +313,86 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS): loop.run_in_executor(None, uploader.upload_file, detail_object_key, detail_path) # --- 抓取价格时段信息 --- - # 识别 "全部时段" 按钮 - # 优先使用 VL 模型精准识别,避免几何特征误判导致退回微信 - res = await ReadImageKit.find_all_time_button_coordinate(detail_path, device_info=device_info) - pos = (res.get("uia_center_x"), res.get("uia_center_y")) if res.get("uia_center_x") else None + # 【优化】使用模板匹配点击 "全部时段" 按钮,节省 VL Token 且更稳定 + template_qbsd = os.path.join(os.path.dirname(__file__), "Template", "qbsd.jpg") + logger.info(f"尝试通过图片识别点击 '全部时段' 按钮: {template_qbsd}") + + # 记录点击前的页面特征,用于验证是否成功进入三级页面 + before_click_md5 = Kit.get_image_content_md5(detail_path) entered_price_page = False - if pos and pos[0] is not None and pos[1] is not None: - ex, ey = pos - logger.info(f"找到 '全部时段' 按钮: ({ex}, {ey})") - - # 记录点击前的页面特征,用于验证是否成功进入三级页面 - before_click_md5 = Kit.get_image_content_md5(detail_path) - - d.click(ex, ey) - await asyncio.sleep(1.5) - - # 检查页面是否真的变化了 - after_click_path = take_screenshot(d, f"check_{detail_uuid}", save_dir=TEMP_IMAGE_DIR) - after_click_md5 = Kit.get_image_content_md5(after_click_path) - - if before_click_md5 != after_click_md5: - entered_price_page = True - price_screenshots = [] - for p_idx in range(1, 4): - 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) + try: + if d.image.click(template_qbsd, timeout=5.0): + logger.info("成功通过图片识别点击 '全部时段' 按钮") + await asyncio.sleep(1.5) - # 【异步优化】后台处理价格 - logger.info(f"已启动后台分析价格页: {station_name}") - task_price = asyncio.create_task(analyze_prices_background(service, station_name, price_screenshots, device_info=device_info)) - background_tasks.append(task_price) - - # 从三级页面返回二级页面 - d.press("back") - await asyncio.sleep(0.5) - - # 检查返回后的页面状态,防止多重返回导致退出到搜索页 - check_back_path = take_screenshot(d, f"check_back_{detail_uuid}", save_dir=TEMP_IMAGE_DIR) + # 检查页面是否真的变化了 + after_click_path = take_screenshot(d, f"check_{detail_uuid}", save_dir=TEMP_IMAGE_DIR) + after_click_md5 = Kit.get_image_content_md5(after_click_path) - # 1. 与列表页对比 (使用列表页的排除比例) - check_list_md5 = Kit.get_image_content_md5( - check_back_path, - top_ratio=SAFE_EXCLUDE_RATIO, - bottom_ratio=BOTTOM_SAFE_EXCLUDE_RATIO - ) - - # 2. 与详情页对比 (使用默认排除比例,与 before_click_md5 一致) - check_detail_md5 = Kit.get_image_content_md5(check_back_path) - - if check_list_md5 == current_md5: - logger.info("检测到已直接返回列表页,跳过后续返回操作。") - should_back_to_list = False - elif check_detail_md5 == before_click_md5: - logger.info("检测到返回详情页,需执行第二次返回。") - should_back_to_list = True - else: - logger.warning(f"返回后状态未知 (ListMD5:{check_list_md5[:8]} vs {current_md5[:8]}, DetailMD5:{check_detail_md5[:8]} vs {before_click_md5[:8]})。") - logger.info("正在使用 VL 模型进一步确认当前页面状态...") + if before_click_md5 != after_click_md5: + entered_price_page = True + price_screenshots = [] + for p_idx in range(1, 4): + 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) - # 使用 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 确认当前仍处于详情页 (发现了'全部时段'按钮),执行返回。") + # 【异步优化】后台处理价格 + logger.info(f"已启动后台分析价格页: {station_name}") + task_price = asyncio.create_task(analyze_prices_background(service, station_name, price_screenshots, device_info=device_info)) + background_tasks.append(task_price) + + # 从三级页面返回二级页面 + d.press("back") + await asyncio.sleep(0.5) + + # 检查返回后的页面状态,防止多重返回导致退出到搜索页 + check_back_path = take_screenshot(d, f"check_back_{detail_uuid}", save_dir=TEMP_IMAGE_DIR) + + # 1. 与列表页对比 (使用列表页的排除比例) + check_list_md5 = Kit.get_image_content_md5( + check_back_path, + top_ratio=SAFE_EXCLUDE_RATIO, + bottom_ratio=BOTTOM_SAFE_EXCLUDE_RATIO + ) + + # 2. 与详情页对比 (使用默认排除比例,与 before_click_md5 一致) + check_detail_md5 = Kit.get_image_content_md5(check_back_path) + + if check_list_md5 == current_md5: + logger.info("检测到已直接返回列表页,跳过后续返回操作。") + should_back_to_list = False + elif check_detail_md5 == before_click_md5: + logger.info("检测到返回详情页,需执行第二次返回。") should_back_to_list = True else: - logger.info("VL 未发现'全部时段'按钮,判定已回到列表页(或非详情页),停止回退以防误退。") - should_back_to_list = False - - # 使用完毕后再清理检查用的截图 - if os.path.exists(check_back_path): os.remove(check_back_path) + 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("图片识别确认当前仍处于详情页 (发现了'全部时段'按钮),执行返回。") + should_back_to_list = True + else: + logger.info("图片识别未发现'全部时段'按钮,判定已回到列表页(或非详情页),停止回退以防误退。") + should_back_to_list = False + + # 使用完毕后再清理检查用的截图 + if os.path.exists(check_back_path): os.remove(check_back_path) + else: + logger.warning(f"点击 '{station_name}' 的 '全部时段' 按钮后页面无明显变化,跳过三级页面处理。") + + # 清理检查用的截图 + if os.path.exists(after_click_path): os.remove(after_click_path) else: - logger.warning(f"点击 '{station_name}' 的 '全部时段' 按钮后页面无明显变化,跳过三级页面处理。") - - # 清理检查用的截图 - if os.path.exists(after_click_path): os.remove(after_click_path) - else: - logger.info(f"场站 {station_name} 未发现 '全部时段' 按钮。") + logger.info(f"场站 {station_name} 通过图片识别未发现 '全部时段' 按钮。") + except Exception as e: + logger.error(f"通过图片识别点击 '全部时段' 按钮异常: {e}") # 从二级页面返回 (仅当确实需要返回时) if should_back_to_list: diff --git a/Apps/XinDianTu/Doc/流程说明.md b/Apps/XinDianTu/Doc/流程说明.md index 58734c5..ac7c786 100644 --- a/Apps/XinDianTu/Doc/流程说明.md +++ b/Apps/XinDianTu/Doc/流程说明.md @@ -69,6 +69,12 @@ - 中文路径与空文件校验:`Kit.py` 统一图片读写与校验,避免并发读写导致崩溃。 - 异步任务清理:每页结束清理已完成任务,最终 `asyncio.gather` 等待残余任务,防止内存堆积。 +### 3.7 模板匹配与设备一致性(关键建议) +- **识别原理**:使用 `d.image.click` 等基于 OpenCV 的图形学匹配方法时,本质上是像素级的特征对齐。 +- **设备差异性**:不同手机的**屏幕分辨率**、**像素密度 (DPI)** 以及 **UI 缩放比例** 存在显著差异。同一张按钮截图(如 `qbsd.jpg`)在 A 设备上能匹配成功,但在 B 设备上可能因像素尺寸微调或渲染差异而识别失败。 +- **一机一策**:在实际生产环境中,建议遵循“一机一议”原则。即针对不同的处理机(或同款但配置不同的手机),应分别采集对应的模板图片,确保匹配精度。 +- **优化方案**:建议将模板文件按设备分辨率或型号分类存放(如 `Template/1080x2400/qbsd.jpg`),或者在无法匹配时使用视觉大模型(VL)作为兜底定位方案。 + --- ## 4. 数据持久化 diff --git a/Apps/XinDianTu/Template/qbsd.jpg b/Apps/XinDianTu/Template/qbsd.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fd41900f0a19c7d48d5972683abc4f8982999a69 GIT binary patch literal 4107 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 X+kT0T6Mr6>jP`PnkpI6+Q#AfxQ0uVb literal 0 HcmV?d00001