From 3e9fc3737e0c5a7f90ff57bb2d820065d44c2247 Mon Sep 17 00:00:00 2001 From: HuangHai <10402852@qq.com> Date: Thu, 15 Jan 2026 07:29:51 +0800 Subject: [PATCH] 'commit' --- Apps/TeLaiDian/ClearHistory.py | 85 +++ Apps/TeLaiDian/Crawler.py | 543 ++++++++---------- Apps/TeLaiDian/ReadImageKit.py | 112 +++- Apps/TeLaiDian/Template/qbsd.jpg | Bin 4456 -> 4201 bytes .../__pycache__/Crawler.cpython-310.pyc | Bin 16178 -> 15734 bytes .../__pycache__/ReadImageKit.cpython-310.pyc | Bin 13154 -> 14851 bytes Config/__pycache__/Config.cpython-310.pyc | Bin 1327 -> 1327 bytes T4_TeLaiDian_Simple.py | 42 -- Tools/T_TeLaiDian_ScrollTest.py | 114 ++++ debug_cv.py | 93 --- 10 files changed, 532 insertions(+), 457 deletions(-) create mode 100644 Apps/TeLaiDian/ClearHistory.py delete mode 100644 T4_TeLaiDian_Simple.py create mode 100644 Tools/T_TeLaiDian_ScrollTest.py delete mode 100644 debug_cv.py diff --git a/Apps/TeLaiDian/ClearHistory.py b/Apps/TeLaiDian/ClearHistory.py new file mode 100644 index 0000000..d4bd2a8 --- /dev/null +++ b/Apps/TeLaiDian/ClearHistory.py @@ -0,0 +1,85 @@ +import asyncio +import os +import sys +import logging + +# 确保项目根目录在 sys.path 中 +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 DbKit.Db import Db +from Util.RedisKit import RedisKit +from sqlalchemy.sql import text +from Config.Config import DB_URL + +# 配置日志 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger("ClearAiTeJiYiChong") + +async def main(): + """ + 1. 删除数据库中所有 operator='特来电' 的记录 + 2. 删除 Redis 中所有关于艾特吉易充的缓存信息 + """ + operator = '特来电' + + # 1. 数据库清理 + logger.info(f"开始清理数据库中 operator='{operator}' 的数据...") + db = Db(db_url=DB_URL) + await db.init_db() + + try: + async with db.AsyncSessionLocal() as session: + async with session.begin(): + # 先删除从表记录(通过 station_hash 关联) + # 1. t_station_status_scd + sql_status = """ + DELETE FROM t_station_status_scd + WHERE station_hash IN ( + SELECT station_hash FROM t_station_profile_scd WHERE operator = :operator + ) + """ + logger.info("正在清理 t_station_status_scd...") + result_status = await session.execute(text(sql_status), {"operator": operator}) + logger.info(f"t_station_status_scd 已删除 {result_status.rowcount} 行记录。") + + # 2. t_station_price_schedule_scd + sql_price = """ + DELETE FROM t_station_price_schedule_scd + WHERE station_hash IN ( + SELECT station_hash FROM t_station_profile_scd WHERE operator = :operator + ) + """ + logger.info("正在清理 t_station_price_schedule_scd...") + result_price = await session.execute(text(sql_price), {"operator": operator}) + logger.info(f"t_station_price_schedule_scd 已删除 {result_price.rowcount} 行记录。") + + # 3. 最后删除主表 t_station_profile_scd + sql_profile = "DELETE FROM t_station_profile_scd WHERE operator = :operator" + logger.info("正在清理 t_station_profile_scd...") + result_profile = await session.execute(text(sql_profile), {"operator": operator}) + logger.info(f"t_station_profile_scd 已删除 {result_profile.rowcount} 行记录。") + + logger.info("数据库记录清理完成。") + except Exception as e: + logger.error(f"数据库清理失败: {e}") + + # 2. Redis 清理 + logger.info("开始清理 Redis 中的缓存数据...") + redis_kit = RedisKit() + # 根据特来电爬虫的约定,Redis 去重键的模式为 crawled:tld:* + pattern = "crawled:tld:*" + try: + keys = await redis_kit.keys(pattern) + if keys: + logger.info(f"匹配到 {len(keys)} 个键,正在删除...") + await redis_kit.delete(*keys) + logger.info("Redis 缓存清理完成。") + else: + logger.info(f"未匹配到模式为 '{pattern}' 的键。") + except Exception as e: + logger.error(f"Redis 清理失败: {e}") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/Apps/TeLaiDian/Crawler.py b/Apps/TeLaiDian/Crawler.py index 2b98091..a75cab5 100644 --- a/Apps/TeLaiDian/Crawler.py +++ b/Apps/TeLaiDian/Crawler.py @@ -27,6 +27,11 @@ if project_root not in sys.path: # 初始化日志 logger = setup_logger("TeLaiDianCrawler") +PRICE_TAB_X_NORM = 220 +PRICE_TAB_Y_NORM = 130 +PRICE_ENTRY_X_NORM = 230 +PRICE_ENTRY_Y_NORM = 380 + class TeLaiDianCrawler(BaseCrawler): def __init__(self, service=None): super().__init__(service or TeLaiDianService()) @@ -85,6 +90,61 @@ class TeLaiDianCrawler(BaseCrawler): # [优化] 向下滚动以刷新/校准地理位置 # 使用更加显式的 swipe 方式:从屏幕 30% 划到 80% + popup_screen_path = take_screenshot(d, f"tld_detail_popup_{int(time.time())}.jpg") + logger.info(f"[详情页] 截图用于检测温馨提示弹窗: {popup_screen_path}") + + template_xczs = os.path.join(os.path.dirname(__file__), "Template", "xczs.jpg") + template_point = None + if os.path.exists(template_xczs): + try: + match_res = d.image.match(template_xczs) + if match_res: + if hasattr(match_res, "point") and match_res.point: + template_point = match_res.point + elif isinstance(match_res, dict): + if match_res.get("point"): + template_point = match_res["point"] + elif "x" in match_res and "y" in match_res: + template_point = (match_res["x"], match_res["y"]) + elif isinstance(match_res, (list, tuple)) and len(match_res) >= 2: + template_point = (match_res[0], match_res[1]) + if template_point: + logger.info(f"[详情页] 模板检测到疑似温馨提示弹窗位置: {template_point}") + else: + logger.info("[详情页] 模板未能确定温馨提示弹窗位置") + except Exception as e: + logger.error(f"[详情页] 模板检测温馨提示弹窗失败: {e}") + else: + logger.info(f"[详情页] 温馨提示模板文件不存在: {template_xczs}") + + vlm_popup = {"has_popup": False} + try: + vlm_popup = await self.read_image_kit.check_warm_popup_vlm(popup_screen_path) + except Exception as e: + logger.error(f"[详情页] VLM 检测温馨提示弹窗失败: {e}") + + has_vlm_popup = isinstance(vlm_popup, dict) and vlm_popup.get("has_popup") + if template_point and has_vlm_popup: + click_x = int(template_point[0]) + click_y = int(template_point[1]) + logger.info(f"[详情页] 模板与 VLM 均确认存在温馨提示弹窗,即将点击关闭按钮: ({click_x}, {click_y})") + debug_popup_path = popup_screen_path.replace(".jpg", f"_xczs_click_{click_x}_{click_y}.jpg") + try: + img_popup = read_image(popup_screen_path) + if img_popup is not None: + cv2.circle(img_popup, (click_x, click_y), 20, (0, 0, 255), -1) + save_image(debug_popup_path, img_popup) + logger.info(f"[详情页] 已生成温馨提示弹窗点击诊断图片: {debug_popup_path}") + except Exception as e: + logger.error(f"[详情页] 生成温馨提示弹窗诊断图片失败: {e}") + try: + d.click(click_x, click_y) + await asyncio.sleep(1.5) + except Exception as e: + logger.error(f"[详情页] 点击温馨提示“下次再说”失败: {e}") + else: + logger.info(f"[详情页] 温馨提示弹窗未通过双重确认,模板检测: {bool(template_point)} | VLM 检测: {vlm_popup}") + w, h = d.window_size() logger.info(f"执行显式下拉刷新操作: (x={w//2}, y1={int(h*0.3)} -> y2={int(h*0.8)})") d.swipe(w // 2, int(h * 0.3), w // 2, int(h * 0.8), duration=0.5) @@ -220,319 +280,202 @@ class TeLaiDianCrawler(BaseCrawler): except: pass - template_xczs = os.path.join(os.path.dirname(__file__), "Template", "xczs.jpg") - logger.info("[详情页] 检查是否存在温馨提示弹窗(下次再说)...") - try: - await asyncio.sleep(0.5) - if os.path.exists(template_xczs): - clicked = d.image.click(template_xczs, timeout=3.0) - if clicked: - logger.info("[详情页] 检测到温馨提示弹窗,已点击“下次再说”关闭。") - await asyncio.sleep(1.5) - else: - logger.info("[详情页] 未在当前页面匹配到温馨提示弹窗模板。") - else: - logger.info(f"[详情页] 温馨提示模板文件不存在: {template_xczs}") - except Exception as e: - logger.error(f"[详情页] 通过模板点击关闭温馨提示弹窗失败: {e}") - w, h = d.window_size() - logger.info("[详情页] 先执行一次较大的向上滑动,将详情内容上移一屏左右") - d.swipe(w * 0.5, h * 0.8, w * 0.5, h * 0.3, 0.5) - await asyncio.sleep(1.0) - entrance_point = None - entrance_source = None - max_search_rounds = 5 + logger.info("[详情页] 根据用户策略: 多次大幅向上滑动,直到页面基本不再变化") + last_md5 = None + stable_count = 0 + max_round = 30 + final_screen_path = None - for round_idx in range(max_search_rounds): - if entrance_point: - break - search_screen_path = take_screenshot(d, f"tld_detail_search_{int(time.time())}_{round_idx}.jpg") - logger.info(f"[详情页] 搜索价格入口,第 {round_idx + 1}/{max_search_rounds} 轮: {search_screen_path}") + from Apps.TeLaiDian.Kit import get_image_content_md5 - if not entrance_point: - try: - vlm_res = await self.read_image_kit.find_price_entrance_vlm(search_screen_path) - except Exception as e: - vlm_res = {} - logger.error(f"[详情页] VLM 寻找价格入口失败: {e}") - - point = vlm_res.get("point") if isinstance(vlm_res, dict) else None - if vlm_res.get("found") and point and len(point) >= 2: - vx = int(point[0] * w / 1000) - vy = int(point[1] * h / 1000) - if vy < int(h * SAFE_EXCLUDE_RATIO): - vy = int(h * SAFE_EXCLUDE_RATIO) - if vy > int(h * (1 - BOTTOM_SAFE_EXCLUDE_RATIO)): - vy = int(h * (1 - BOTTOM_SAFE_EXCLUDE_RATIO)) - entrance_point = (vx, vy) - entrance_source = entrance_source or "vlm" - logger.info(f"[详情页] VLM 成功找到价格入口: {entrance_point},原因: {vlm_res.get('reason')}") - - if entrance_point: - break - - logger.info("[详情页] 本轮未找到价格入口,向上滑动继续搜索...") - d.swipe_ext("up", scale=DETAIL_SCROLL_DISTANCE_RATIO) - await asyncio.sleep(WAIT_AFTER_SCROLL) - - if not entrance_point: - logger.error(f"[详情页] 连续 {max_search_rounds} 轮滚动后仍未找到价格入口,本次流程直接中止。") - raise RuntimeError("TeLaiDian: 价格入口多轮搜索失败") - - before_price_path = take_screenshot(d, f"tld_detail_price_before_{int(time.time())}.jpg") - before_md5 = get_image_content_md5( - before_price_path, - top_ratio=SAFE_EXCLUDE_RATIO, - bottom_ratio=BOTTOM_SAFE_EXCLUDE_RATIO, - ) - - ex, ey = int(entrance_point[0]), int(entrance_point[1]) - entered_price_page = False - - if entrance_source == "vlm": - click_x = ex - click_y = ey - logger.info(f"[详情页] 使用 VLM 当前价红色价格入口坐标直接点击: ({click_x}, {click_y})") - else: - click_x = ex - offset_y = int(h * 0.1) - click_y = min(h - 10, ey + offset_y) - logger.info(f"[详情页] 使用 {entrance_source or '入口'} 坐标: ({ex}, {ey}),调整后点击坐标: ({click_x}, {click_y}),即将进入电价页") - d.click(click_x, click_y) - await asyncio.sleep(WAIT_DETAIL_PAGE_LOAD) - - after_price_path = take_screenshot(d, f"tld_detail_price_after_{int(time.time())}.jpg") - after_md5 = get_image_content_md5( - after_price_path, - top_ratio=SAFE_EXCLUDE_RATIO, - bottom_ratio=BOTTOM_SAFE_EXCLUDE_RATIO, - ) - - if before_md5 and after_md5 and before_md5 != after_md5: - entered_price_page = True - logger.info("[电价页] 首次点击后页面内容发生变化,判定已进入电价详情页。") - else: - logger.warning("[电价页] 首次点击后页面内容无明显变化,尝试使用 VLM 兜底寻找价格入口。") - try: - vlm_res = await self.read_image_kit.find_price_entrance_vlm(before_price_path) - except Exception as e: - vlm_res = {} - logger.error(f"[电价页] VLM 寻找价格入口失败: {e}") - - point = vlm_res.get("point") if isinstance(vlm_res, dict) else None - if vlm_res.get("found") and point and len(point) >= 2: - w, h = d.window_size() - vx = int(point[0] * w / 1000) - vy = int(point[1] * h / 1000) - if vy < int(h * SAFE_EXCLUDE_RATIO): - vy = int(h * SAFE_EXCLUDE_RATIO) - if vy > int(h * (1 - BOTTOM_SAFE_EXCLUDE_RATIO)): - vy = int(h * (1 - BOTTOM_SAFE_EXCLUDE_RATIO)) - logger.info(f"[电价页] 使用 VLM 兜底点击价格入口: ({vx}, {vy}),原因: {vlm_res.get('reason')}") - d.click(vx, vy) - await asyncio.sleep(WAIT_DETAIL_PAGE_LOAD) - - vlm_after_path = take_screenshot(d, f"tld_detail_price_after_vlm_{int(time.time())}.jpg") - vlm_after_md5 = get_image_content_md5( - vlm_after_path, - top_ratio=SAFE_EXCLUDE_RATIO, - bottom_ratio=BOTTOM_SAFE_EXCLUDE_RATIO, - ) - if before_md5 and vlm_after_md5 and before_md5 != vlm_after_md5: - entered_price_page = True - logger.info("[电价页] VLM 兜底点击后页面内容发生变化,判定已进入电价详情页。") - else: - logger.error(f"[电价页] VLM 未能找到可靠的价格入口: {vlm_res}") - - if not entered_price_page: - logger.error("[电价页] 多轮尝试后页面仍未变化,疑似点击未生效或入口不可用。") - raise RuntimeError("TeLaiDian: 价格入口点击后页面未变化") - - logger.info("[电价页] 已成功进入电价详情页,等待小程序自动滚动定位完成 (4秒)...") - await asyncio.sleep(4.0) - logger.info("[电价页] 额外执行多次向下滑动,让上方时段露出") - for idx in range(3): - logger.info(f"[电价页] 第 {idx+1} 次向下滑动(d.swipe_ext(\"down\", scale=0.9))") - d.swipe_ext("down", scale=0.9) + for idx in range(max_round): + start_x = int(w * 0.9) + start_y = int(h * 0.85) + end_y = int(h * 0.25) + logger.info(f"[详情页] 第 {idx + 1} 轮大幅向上滑动: ({start_x}, {start_y}) -> ({start_x}, {end_y})") + d.swipe(start_x, start_y, start_x, end_y, 0.25) await asyncio.sleep(1.0) - # 4. 进入分时电价页面后的处理 - try: - logger.info("[电价页] 已点击价格入口,等待小程序自动滚动定位完成 (4秒)...") - await asyncio.sleep(4.0) - logger.info("[电价页] 开始执行两次大幅向下拉动,目标是回到 00:00 时段顶部") - for idx in range(2): - logger.info(f"[电价页] 第 {idx+1} 次向下滑动(d.swipe_ext(\"down\", scale=0.9))") - d.swipe_ext("down", scale=0.9) - await asyncio.sleep(1.0) - except Exception as e: - logger.error(f"[电价页] 处理分时电价页面初始状态失败: {e}") + screen_path = take_screenshot(d, f"tld_detail_scan_{int(time.time())}_{idx}.jpg") + logger.info(f"[详情页] 第 {idx + 1} 轮滑动后的截图: {screen_path}") + curr_md5 = get_image_content_md5( + screen_path, + top_ratio=SAFE_EXCLUDE_RATIO, + bottom_ratio=BOTTOM_SAFE_EXCLUDE_RATIO, + ) + if last_md5 is not None and curr_md5 == last_md5: + stable_count += 1 + logger.info(f"[详情页] 页面内容连续第 {stable_count} 次无变化,可能已到稳定区域") + else: + stable_count = 0 + last_md5 = curr_md5 + final_screen_path = screen_path - # 5. 循环滑动抓取完整分时电价 - all_prices = [] - last_price_md5 = None - price_page_count = 0 - max_price_pages = 4 - screenshot_tasks = [] - temp_screenshots = [] + if stable_count >= 2: + logger.info("[详情页] 检测到页面多次无变化,认为已到达顶部固定区域,提前结束扫描。") + break - logger.info("[电价页] 开始循环截图(UI操作优先,后台并行分析)...") - try: - while price_page_count < max_price_pages: - price_screen_path = take_screenshot(d, f"tld_detail_price_{int(time.time())}_{price_page_count}.jpg") - - curr_md5 = get_image_content_md5(price_screen_path, top_ratio=0.2, bottom_ratio=0.2) - if curr_md5 == last_price_md5: - logger.info("价格页面内容无变化,判定已触底") - if os.path.exists(price_screen_path): - os.remove(price_screen_path) - break - last_price_md5 = curr_md5 - temp_screenshots.append(price_screen_path) - - logger.info(f"已截取价格详情页第 {price_page_count + 1} 页: {price_screen_path},启动后台异步分析") - task = asyncio.create_task(self.read_image_kit.analyze_detail_price(price_screen_path)) - screenshot_tasks.append(task) - - logger.info("向上滚动列表,准备截取下一屏价格...") - d.swipe_ext("up", scale=0.8) - await asyncio.sleep(1.0) - price_page_count += 1 - - if screenshot_tasks: - logger.info(f"UI 操作已完成,等待 {len(screenshot_tasks)} 个后台分析任务结束...") - results = await asyncio.gather(*screenshot_tasks, return_exceptions=True) - - for res in results: - if isinstance(res, Exception): - logger.error(f"后台分析任务出错: {res}") - continue - if res: - for p in res: - is_duplicate = False - for existing in all_prices: - if p.get('start') == existing.get('start') and p.get('end') == existing.get('end'): - is_duplicate = True - for key in ['price', 'plus_price', 'market_price', 'elec_price', 'service_price']: - if p.get(key) is not None and (existing.get(key) is None or existing.get(key) == 0): - existing[key] = p[key] - break - if not is_duplicate: - all_prices.append(p) - except Exception as e: - logger.error(f"抓取价格详情过程中发生异常: {e}") - finally: - for task in screenshot_tasks: - if not task.done(): - task.cancel() - - for path in temp_screenshots: - if os.path.exists(path): - try: - os.remove(path) - except: - pass - - if all_prices: - station_name_clean = clean_station_name(station_name) - try: - all_prices.sort(key=lambda x: x.get('start', '00:00')) - except: - pass - logger.info(f"✅ 场站 {station_name_clean} 共提取到 {len(all_prices)} 条价格信息,准备保存...") - await self.service.save_station_data(station_name_clean, address, all_prices) + if not final_screen_path: + final_screen_path = take_screenshot(d, f"tld_detail_scan_final_{int(time.time())}.jpg") + logger.info(f"[详情页] 扫描流程未生成截图,使用兜底截图: {final_screen_path}") else: - logger.warning(f"❌ 未能提取到任何价格信息,请检查页面识别逻辑") - if address: + logger.info(f"[详情页] 使用最终稳定区域截图作为价格页识别输入: {final_screen_path}") + + logger.info("[详情页] 使用固定归一化坐标点击顶部“价格”标签,并点击左侧当前价数字进入全时段电价") + price_tab_screen = final_screen_path + entrance_clicked = False + + try: + tab_x = int(PRICE_TAB_X_NORM * w / 1000) + tab_y = int(PRICE_TAB_Y_NORM * h / 1000) + logger.info(f"[详情页] 固定坐标点击价格标签: 归一化({PRICE_TAB_X_NORM}, {PRICE_TAB_Y_NORM}) -> 像素({tab_x}, {tab_y})") + d.click(tab_x, tab_y) + await asyncio.sleep(1.0) + price_tab_screen = take_screenshot(d, f"tld_detail_after_price_tab_{int(time.time())}.jpg") + logger.info(f"[详情页] 点击价格标签后的界面截图已保存: {price_tab_screen}") + + entry_x = int(PRICE_ENTRY_X_NORM * w / 1000) + entry_y = int(PRICE_ENTRY_Y_NORM * h / 1000) + click_x = max(5, min(w - 5, entry_x)) + click_y = max(5, min(h - 5, entry_y)) + + debug_click_path = price_tab_screen.replace( + ".jpg", + f"_click_{tab_x}_{tab_y}_price_{click_x}_{click_y}.jpg" + ) + try: + img = read_image(price_tab_screen) + if img is not None: + cv2.circle(img, (tab_x, tab_y), 20, (0, 0, 255), -1) + cv2.circle(img, (click_x, click_y), 20, (0, 0, 255), -1) + save_image(debug_click_path, img) + logger.info(f"[详情页] 已生成价格标签与当前价入口红点标记图: {debug_click_path}") + else: + logger.warning(f"[详情页] 加载价格页截图失败,无法绘制诊断红点: {price_tab_screen}") + except Exception as e: + logger.error(f"[详情页] 生成价格入口点击诊断图片失败: {e}") + + logger.info(f"[详情页] 点击当前价入口: 像素({click_x}, {click_y}),屏幕大小: ({w}, {h})") + d.click(click_x, click_y) + entrance_clicked = True + await asyncio.sleep(WAIT_DETAIL_PAGE_LOAD) + except Exception as e: + logger.error(f"[详情页] 固定坐标点击价格标签或入口失败: {e}") + + if entrance_clicked: + entered_price_path = take_screenshot(d, f"tld_detail_price_after_enter_{int(time.time())}.jpg") + logger.info(f"[电价页] 入口点击后的电价页截图已保存: {entered_price_path}") + + await asyncio.sleep(1.0) + + logger.info("[电价页] 先多次向下滚动,使列表回到起始位置") + top_last_md5 = None + top_stable = 0 + max_top_round = 8 + price_top_screen = entered_price_path + + for idx in range(max_top_round): + price_top_screen = take_screenshot(d, f"tld_detail_price_top_{int(time.time())}_{idx}.jpg") + logger.info(f"[电价页] 向下滚动前后的截图: {price_top_screen}") + curr_md5 = get_image_content_md5(price_top_screen, top_ratio=0.2, bottom_ratio=0.2) + if top_last_md5 is not None and curr_md5 == top_last_md5: + top_stable += 1 + logger.info(f"[电价页] 页面内容连续第 {top_stable} 次无变化,可能已到顶部") + if top_stable >= 2: + break + else: + top_stable = 0 + top_last_md5 = curr_md5 + + d.swipe_ext("down", scale=0.8) + await asyncio.sleep(1.0) + + all_prices = [] + last_price_md5 = None + price_page_count = 0 + max_price_pages = 4 + screenshot_tasks = [] + temp_screenshots = [] + price_screen_path = price_top_screen + + try: + while price_page_count < max_price_pages: + price_screen_path = take_screenshot(d, f"tld_detail_price_{int(time.time())}_{price_page_count}.jpg") + + curr_md5 = get_image_content_md5(price_screen_path, top_ratio=0.2, bottom_ratio=0.2) + if curr_md5 == last_price_md5: + logger.info("价格页面内容无变化,判定已触底") + if os.path.exists(price_screen_path): + os.remove(price_screen_path) + break + last_price_md5 = curr_md5 + temp_screenshots.append(price_screen_path) + + logger.info(f"已截取价格详情页第 {price_page_count + 1} 页: {price_screen_path},启动后台异步分析") + task = asyncio.create_task(self.read_image_kit.analyze_detail_price(price_screen_path)) + screenshot_tasks.append(task) + + logger.info("向上滚动列表,准备截取下一屏价格...") + d.swipe_ext("up", scale=0.8) + await asyncio.sleep(1.0) + price_page_count += 1 + + if screenshot_tasks: + logger.info(f"UI 操作已完成,等待 {len(screenshot_tasks)} 个后台分析任务结束...") + results = await asyncio.gather(*screenshot_tasks, return_exceptions=True) + + for res in results: + if isinstance(res, Exception): + logger.error(f"后台分析任务出错: {res}") + continue + if res: + for p in res: + is_duplicate = False + for existing in all_prices: + if p.get('start') == existing.get('start') and p.get('end') == existing.get('end'): + is_duplicate = True + for key in ['price', 'plus_price', 'market_price', 'elec_price', 'service_price']: + if p.get(key) is not None and (existing.get(key) is None or existing.get(key) == 0): + existing[key] = p[key] + break + if not is_duplicate: + all_prices.append(p) + except Exception as e: + logger.error(f"抓取价格详情过程中发生异常: {e}") + finally: + for task in screenshot_tasks: + if not task.done(): + task.cancel() + + for path in temp_screenshots: + if os.path.exists(path): + try: + os.remove(path) + except: + pass + + if all_prices: station_name_clean = clean_station_name(station_name) - logger.info(f"[详情页] 虽未获取价格,但已获取地址,尝试仅保存基础信息: {station_name_clean} | {address}") try: - await self.service.save_station_profile_only(station_name_clean, address) - except Exception as e: - logger.error(f"[详情页] 仅保存基础信息失败: {e}") - if os.path.exists(price_screen_path): - os.remove(price_screen_path) - - try: - while price_page_count < max_price_pages: - price_screen_path = take_screenshot(d, f"tld_detail_price_{int(time.time())}_{price_page_count}.jpg") - - # 校验页面是否发生滚动变化 - curr_md5 = get_image_content_md5(price_screen_path, top_ratio=0.2, bottom_ratio=0.2) - if curr_md5 == last_price_md5: - logger.info("价格页面内容无变化,判定已触底") - if os.path.exists(price_screen_path): os.remove(price_screen_path) - break - last_price_md5 = curr_md5 - temp_screenshots.append(price_screen_path) - - logger.info(f"已截取价格详情页第 {price_page_count + 1} 页: {price_screen_path},启动后台异步分析") - # 使用 asyncio.create_task 立即在后台开始执行分析 - task = asyncio.create_task(self.read_image_kit.analyze_detail_price(price_screen_path)) - screenshot_tasks.append(task) - - # 向上滚动列表(手指向上划),看后面的时段 - logger.info("向上滚动列表,准备截取下一屏价格...") - d.swipe_ext("up", scale=0.8) - await asyncio.sleep(1.0) - price_page_count += 1 - - # 等待所有后台分析任务完成 - if screenshot_tasks: - logger.info(f"UI 操作已完成,等待 {len(screenshot_tasks)} 个后台分析任务结束...") - results = await asyncio.gather(*screenshot_tasks, return_exceptions=True) - - for res in results: - if isinstance(res, Exception): - logger.error(f"后台分析任务出错: {res}") - continue - if res: - # 深度去重:根据时段 (start, end) 合并 - for p in res: - is_duplicate = False - for existing in all_prices: - if p.get('start') == existing.get('start') and p.get('end') == existing.get('end'): - is_duplicate = True - # 字段补全逻辑 - for key in ['price', 'plus_price', 'market_price', 'elec_price', 'service_price']: - if p.get(key) is not None and (existing.get(key) is None or existing.get(key) == 0): - existing[key] = p[key] - break - if not is_duplicate: - all_prices.append(p) - except Exception as e: - logger.error(f"抓取价格详情过程中发生异常: {e}") - finally: - # 无论是否异常,都要确保清理未完成的任务,避免 "never awaited" 警告 - for task in screenshot_tasks: - if not task.done(): - task.cancel() - - # 清理所有临时截图 - for path in temp_screenshots: - if os.path.exists(path): + all_prices.sort(key=lambda x: x.get('start', '00:00')) + except: + pass + logger.info(f"✅ 场站 {station_name_clean} 共提取到 {len(all_prices)} 条价格信息,准备保存...") + await self.service.save_station_data(station_name_clean, address, all_prices) + else: + logger.warning(f"❌ 未能提取到任何价格信息,请检查页面识别逻辑") + if address: + station_name_clean = clean_station_name(station_name) + logger.info(f"[详情页] 虽未获取价格,但已获取地址,尝试仅保存基础信息: {station_name_clean} | {address}") try: - os.remove(path) - except: - pass + await self.service.save_station_profile_only(station_name_clean, address) + except Exception as e: + logger.error(f"[详情页] 仅保存基础信息失败: {e}") - # 6. 保存数据 - if all_prices: - station_name_clean = clean_station_name(station_name) - # 对价格按时间排序 - try: - all_prices.sort(key=lambda x: x.get('start', '00:00')) - except: - pass - logger.info(f"✅ 场站 {station_name_clean} 共提取到 {len(all_prices)} 条价格信息,准备保存...") - await self.service.save_station_data(station_name_clean, address, all_prices) - else: - logger.warning(f"❌ 未能提取到任何价格信息,请检查页面识别逻辑") - # 清理价格页临时截图(详情页首屏截图已在异步任务中清理) - if os.path.exists(price_screen_path): - os.remove(price_screen_path) + if os.path.exists(price_screen_path): + os.remove(price_screen_path) async def crawl_list(self): """ diff --git a/Apps/TeLaiDian/ReadImageKit.py b/Apps/TeLaiDian/ReadImageKit.py index fcaeb35..586723c 100644 --- a/Apps/TeLaiDian/ReadImageKit.py +++ b/Apps/TeLaiDian/ReadImageKit.py @@ -10,7 +10,7 @@ if project_root not in sys.path: sys.path.append(project_root) from Util.VLMKit import VLMKit -from Apps.TeLaiDian.Kit import draw_rectangles, detect_cards_cv, setup_logger, read_image, detect_wide_rounded_card_cv +from Apps.TeLaiDian.Kit import draw_rectangles, setup_logger, read_image from Apps.TeLaiDian.Config.Setting import SAFE_EXCLUDE_RATIO, BOTTOM_SAFE_EXCLUDE_RATIO, MIN_CARD_HEIGHT # 初始化日志 @@ -20,41 +20,71 @@ class ReadImageKit: def __init__(self): self.vlm = VLMKit() + async def find_price_tab_vlm(self, image_path): + """ + 使用 VLM 在详情页顶部标签栏中寻找“价格”标签的点击位置 + """ + prompt = """ + 分析这张特来电充电站详情页截图,找到顶部标签栏中“价格”两个字所在的点击区域中心。 + 要求: + 1. 仅在页面最上方的标签栏里查找,该标签栏通常包含“价格 / 终端 / 电站 / 评论 / 周边”等文字。 + 2. 不要选择下面“价格信息”模块中的数字(例如 1.0689 元/度)或其它文本。 + 3. 不要选择最顶端系统状态栏或返回按钮等区域。 + + 输出格式为 JSON: + { + "found": true/false, + "reason": "为什么认为这个位置是顶部“价格”标签", + "point": [x, y] // 归一化坐标,范围 [0-1000] + } + """ + try: + res_text = await self.vlm.analyze_image(image_path, prompt) + json_str = self.vlm.extract_json(res_text) + data = json.loads(json_str) + + if data.get("found") and data.get("point"): + p = data["point"] + img = read_image(image_path) + if img is not None: + h, w = img.shape[:2] + actual_p = [int(p[0] * w / 1000), int(p[1] * h / 1000)] + bbox = [actual_p[0]-60, actual_p[1]-30, actual_p[0]+60, actual_p[1]+30] + draw_rectangles(image_path, bboxes=[bbox], click_points=[actual_p]) + logger.info(f"已生成价格标签诊断图片: {image_path.replace('.jpg', '_tab_vl.jpg')}") + + return data + except Exception as e: + logger.error(f"VLM 寻找价格标签失败: {e}") + return {"found": False} + async def find_price_entrance_vlm(self, image_path): """ 使用 VLM 在详情页寻找价格入口(如:1.1556元/度 的卡片或价格信息按钮) """ prompt = """ 分析这张特来电充电站详情页截图,找到进入“分时电价详情”的点击入口。 - 入口优先级和特征如下: - 1. 首选:页面中有文字“当前价”,其正下方一行通常是红色的电价数字, - 例如 “1.1317 元/度”、“1.1556 元/度”等,请优先选择这行红色价格数字所在区域。 - 2. 如果页面同时存在“停车参考价”“停车费参考价”等字样,请不要选择这些区域, - 只选择与充电“当前价”直接对应的红色价格数字。 - 3. 如果页面没有“当前价”字样,则可以退而求其次,选择明显用于展示 - 充电价格的卡片或按钮,例如写有“价格信息”“电价详情”的区域。 + 入口规则: + 1. 只选择“价格信息”模块中“当前价”下方的红色电价数字(例如 1.0689 元/度、1.3435 元/度)。 + 2. 排除底部悬浮条或底部操作区中的红色价格(靠近“扫码充电”“立即充电”等按钮的区域)。 + 3. 排除“停车参考价”“停车费参考价”等与停车相关的区域。 + 4. 禁止选择页面顶部的标签栏,例如“价格 / 终端 / 电站 / 评论 / 周边”这一行中的任何文字或区域。 + 5. 如果页面没有“当前价”,才选择用于展示充电价格的按钮,如“价格信息”“电价详情”。 + + 位置约束(尽量满足): + - Y 位置位于价格信息模块区域内:明显在顶部标签栏下方、在底部悬浮条上方。 + - X 位置应位于左侧价格列区域(当前价所在列),避免会员价右侧列。 请判断符合上述规则的价格入口是否存在,并给出其中心坐标。 输出格式为 JSON: { "found": true/false, - "reason": "为什么认为这是入口(说明是否基于当前价红色价格)", - "point": [x, y], // 归一化坐标 [0-1000],例如 [500, 600] 代表屏幕中心偏下 + "reason": "为什么认为这是入口(说明是否基于当前价红色价格,并确认未选顶部标签栏或底部悬浮条)", + "point": [x, y], "type": "price_card" / "button" } """ try: - cv_bboxes = detect_wide_rounded_card_cv(image_path, min_width_ratio=0.8, min_y_ratio=0.5) - if cv_bboxes: - img = read_image(image_path) - if img is not None: - h, w = img.shape[:2] - bx1, by1, bx2, by2 = cv_bboxes[0] - cx = (bx1 + bx2) // 2 - cy = (by1 + by2) // 2 - draw_rectangles(image_path, bboxes=[cv_bboxes[0]], click_points=[[cx, cy]]) - return {"found": True, "reason": "cv", "point": [int(cx * 1000 / w), int(cy * 1000 / h)], "type": "price_card"} - res_text = await self.vlm.analyze_image(image_path, prompt) json_str = self.vlm.extract_json(res_text) data = json.loads(json_str) @@ -68,7 +98,7 @@ class ReadImageKit: h, w = img.shape[:2] actual_p = [int(p[0] * w / 1000), int(p[1] * h / 1000)] # 生成虚拟 bbox - bbox = [actual_p[0]-50, actual_p[1]-30, actual_p[0]+50, actual_p[1]+30] + bbox = [actual_p[0]-60, actual_p[1]-40, actual_p[0]+60, actual_p[1]+40] draw_rectangles(image_path, bboxes=[bbox], click_points=[actual_p]) logger.info(f"已生成价格入口诊断图片: {image_path.replace('.jpg', '_vl.jpg')}") @@ -119,6 +149,44 @@ class ReadImageKit: logger.error(f"VLM 寻找关闭按钮失败: {e}") return {"has_ad": False} + async def check_warm_popup_vlm(self, image_path): + """ + 检测详情页是否存在“温馨提示/下次再说”弹窗 + """ + prompt = """ + 分析这张特来电充电站详情页截图,判断是否存在带有“下次再说”或“下次现说”文案的温馨提示弹窗。 + 要求: + 1. 只关注覆盖在详情页上方的弹窗或遮罩,其上包含“下次再说”“下次现说”等文字按钮。 + 2. 不要将正常页面中的列表项、价格卡片、终端状态等区域误判为弹窗。 + 3. 如果存在该弹窗,请给出“下次再说”按钮的大致点击中心位置。 + + 输出格式为 JSON: + { + "has_popup": true/false, + "reason": "为什么认为有或没有温馨提示弹窗", + "button_point": [x, y] // 归一化坐标,范围 [0-1000],没有则为 null + } + """ + try: + res_text = await self.vlm.analyze_image(image_path, prompt) + json_str = self.vlm.extract_json(res_text) + data = json.loads(json_str) + + if data.get("has_popup") and data.get("button_point"): + p = data["button_point"] + img = read_image(image_path) + if img is not None: + h, w = img.shape[:2] + actual_p = [int(p[0] * w / 1000), int(p[1] * h / 1000)] + bbox = [actual_p[0]-80, actual_p[1]-40, actual_p[0]+80, actual_p[1]+40] + draw_rectangles(image_path, bboxes=[bbox], click_points=[actual_p]) + logger.info(f"已生成温馨提示弹窗诊断图片: {image_path.replace('.jpg', '_warm_vl.jpg')}") + + return data + except Exception as e: + logger.error(f"VLM 检测温馨提示弹窗失败: {e}") + return {"has_popup": False} + async def check_wrong_page_vlm(self, image_path): """ 检查是否误触进入了错误的页面(如:新人福利、我的卡券、活动页等) diff --git a/Apps/TeLaiDian/Template/qbsd.jpg b/Apps/TeLaiDian/Template/qbsd.jpg index 3a5fcb4efbac1f3c2e7e20fa9ba3a64ee8c13340..3ea15a6afe9596c327be3a956afee59755e470c6 100644 GIT binary patch delta 3650 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 delta 3907 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 diff --git a/Apps/TelaiDian/__pycache__/Crawler.cpython-310.pyc b/Apps/TelaiDian/__pycache__/Crawler.cpython-310.pyc index 21a088590877270ef77c4f7f13fe39eff58939c1..62f6e2025a846fa8bfa775d4290e26dedebcec59 100644 GIT binary patch literal 15734 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%0DR5?is5jXw*ZIU8V& zYz%xBU{zqJfqY(5x?ZsOCEq9TTY^LK`GT6c1@rqsx2KZ*-XP(Nm_TN0)^Gy(S528s cbED=#%|BYoB9BD`NnP!FQxU$b7z_FT0gn7aWiGzMxzxXUcO)@8 z*A8h`A`&P)p!{5uG8j~2lw~a;P*63gt2C*qCQaJDlctIK?D&tZe@v6QYJcp#vk6h4 znk|3)-uK?U-|wA0pxobI8Ly})XW;eUZDH|W8;)1bCm$VO@zZqF9dJjh0#$_J8QvVJ zj(P%~s5jt^)&yz@Be;0W00}&?iQ%ohyvZ^ND2DCY4OW9LpsdCk(meoRX>X?*6wX5YN+NqdS zQj|@HiRw_DVKz*7`-lnhT*j9xX4Szvyo_p{#|#kQoCaq>F3j>yzT&)jH?y5lnQqId z6;!cf?8}h-mzY$Aab{C1#z?ACv5%Iil|Z>v*JwEpl=Ou`xr!9;nww@)?pbkt@n~G& zf*T`KtzL$Al$>(&ZaA?#I0>_ZMh>bm{;CJD zD~?eo?@`HU1@Gl+#@KVr1$K-OMs;DdW=5$@P7x;MA>H!L$~z1{rcrY*@+|H+&Md~pt;dkxO}RR8G^&Y*vEwnA7m%4<<1Y~ zPQ9&Pd+&bcy#A@C4~=AR-@Ko>@XeiJeejdqwTqx6cj{JlA~Q8MT*#c&Cnj>IujeN} zm>T*`{_I=%6CZC4i{XH)xg%Gn?wtYCMT=-_^iIyOmK)0G=db9)lluGbFaJ7oXxem3 zqYv!nety0fr2fuu?!8a+Yo|e4Yj8BB!PDI6&61&sk*Ul5(}uE>=ZXqvg`yw(uqczA zII0g0Le#U;l|T2}+|f(=^;`Pn2~3NTpPAl1bkOAUt=6iXiy!)-A3mcWyPmy$sgTJ` zWrlM%4q@tlH3m);t>#W2&7Hmr)z3~I)o+~4|6+1FSjcnHp`tOct&fc5hu+Q|K9w6D zD!lt^P*N;}{b@Vm(lCL%Re5+RZwEpJ0 z6(t3}y{%164ZgKaO|SW~w=YhOUD0nG({G)GF4FG}=>5l_RkVLru21LBp21gPeHi)| zUVmTjXQ?YH#)7a8m9C(~DPo*D@E}=awiH;9lVFuGS6OQ9?2Pxpu62eYV(9f?A}+=h zc?V@fJ%uR~!vx9`k{A+Ve`M@_evFbTrNhr zunzqg;O5hQ#L6v3ta!8LzrE%5R>05)Sbvz|&~CU$sN6E|_fyN)yvFRShIF4pF` z*AaPqm~A8DoV&@?O08X-9OQWF2u0#D_=|aw@Zn)3TAGVgJ;Fi+A3}}xxu?PUB$A7? zZ#~P?ewXA#ZYjbtgyjfNA@~t6Y0_eZB?y=|>1l)&2n`4;5gGyf6-EN3Rd~>Z@GQb} z2+t$@0HGNHD5Nh~E>pA7diG9E7Ghjfm{k|(Pg#t~ z@QVd>nj@kN!w`xml14G0zY`Hrp{9rcv$8)D=M-w?5(y#3r$V2*Zg~E;pXXQXu zEG|X4hNio+Lv2y6FNg_Y_xH?H1p?Hr#DfX3Pl(iO|5?4&in~Nwul;1rwkM@3yl&%oUJ_*4 zZhm>w+6~f}GNXlY4f%;NI`Bi2$d8zNKEg#id2Zc4?bX&zj+vABVXN=K zWPa7!I-@yejpa`CU>Cx}*|Z~T3@BuxG`V|#q9zC(K#UnfOKrWJ6cb}%sT{@jqJ&wF zOGtf(-`i6qqGK%9mpnNKPHlt zh1JQz=44@UvdNYJY_rJzU~lQWOIm{-wj!)WXamspwmntXjob!=S)0PEz0vAr|IKLQ zZC{dd?a(tz({2=d0bwJ;CWIiuK7=H~LnQ{~!ZQbe2R<_1Z)ad1o9ylOr|t9XFFR}f zbEpmf4PX|x!F6hqlX7uuZb#O}ixN&PbdI*B-Mx%D;Sth}|0$$+TrqMUaO_mXNMrF| wh=h+us&%(7auf@S&nl{ov@i4I;Al1bKZoNUNdN!< diff --git a/Config/__pycache__/Config.cpython-310.pyc b/Config/__pycache__/Config.cpython-310.pyc index 9888431733c29c38728c7eede340285982dcf6e6..a1690e3986877f427094fd334678acab9a4591c5 100644 GIT binary patch delta 20 acmZ3_wVsPRpO=@50SJ7QGB$FnumS)qm;=lJ delta 20 acmZ3_wVsPRpO=@50SJsQr)}g`VFds%1q5OM diff --git a/T4_TeLaiDian_Simple.py b/T4_TeLaiDian_Simple.py deleted file mode 100644 index ca8bb2b..0000000 --- a/T4_TeLaiDian_Simple.py +++ /dev/null @@ -1,42 +0,0 @@ -# coding=utf-8 -import sys -import os -import asyncio -import time -import logging -import uiautomator2 as u2 - -project_root = os.path.dirname(os.path.abspath(__file__)) -if project_root not in sys.path: - sys.path.append(project_root) - -from Apps.TeLaiDian import Kit -from Apps.TeLaiDian.ReadImageKit import ReadImageKit - -logger = Kit.setup_logger("T4_TeLaiDian_Simple", clear_old_log=True) - -async def run_simple(): - Kit.clear_temp_dir() - d = u2.connect() - w, h = d.window_size() - logger.info(f"开始简单流程,当前窗口: {w}x{h}") - logger.info("执行显式下拉刷新以校准位置") - d.swipe(w // 2, int(h * 0.3), w // 2, int(h * 0.8), duration=0.5) - await asyncio.sleep(2.5) - - screenshot_path = Kit.take_screenshot(d, f"tld_list_{int(time.time())}.jpg") - logger.info(f"列表页截图: {screenshot_path}") - - rik = ReadImageKit() - stations = await rik.analyze_station_list(screenshot_path) - logger.info(f"识别到场站数量: {len(stations)}") - for i, s in enumerate(stations[:10]): - logger.info(f"[{i+1}] {s.get('name')} | point={s.get('point')} | bbox={s.get('bbox')}") - - logger.info("简单流程结束") - -if __name__ == "__main__": - try: - asyncio.run(run_simple()) - except KeyboardInterrupt: - logger.info("用户中断") diff --git a/Tools/T_TeLaiDian_ScrollTest.py b/Tools/T_TeLaiDian_ScrollTest.py new file mode 100644 index 0000000..9b7a2d3 --- /dev/null +++ b/Tools/T_TeLaiDian_ScrollTest.py @@ -0,0 +1,114 @@ +# coding=utf-8 +import asyncio +import os +import sys +import time +import uiautomator2 as u2 +import cv2 + +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if project_root not in sys.path: + sys.path.append(project_root) + +from Apps.TeLaiDian.Kit import setup_logger, take_screenshot, read_image, save_image +from Apps.TeLaiDian.Config.Setting import SAFE_EXCLUDE_RATIO, BOTTOM_SAFE_EXCLUDE_RATIO + + +logger = setup_logger("TeLaiDianScrollTest", clear_old_log=False) + +# 根据最新红点截图重新估算的顶部“价格”标签归一化坐标(0-1000) +# 这里调成:X ≈ 22% 屏宽,Y ≈ 13% 屏高 +PRICE_TAB_X_NORM = 220 +PRICE_TAB_Y_NORM = 130 + +# 价格信息卡片中左侧“当前价”红色数字的大致归一化坐标(0-1000) +# 估算:X ≈ 23% 屏宽,Y ≈ 38% 屏高 +PRICE_ENTRY_X_NORM = 230 +PRICE_ENTRY_Y_NORM = 380 + + +async def run_scroll_test(): + d = u2.connect() + w, h = d.window_size() + + logger.info("=== 特来电详情页第2页滚动安全性测试开始 ===") + logger.info("请先手动进入某个场站详情页的第2页状态,然后执行本脚本。脚本会尽可能多次大幅向上滑动,直到页面不再变化。") + + first_screen = take_screenshot(d, f"tld_scrolltest_start_{int(time.time())}.jpg") + logger.info(f"[测试] 起始界面截图: {first_screen}") + + last_md5 = None + stable_count = 0 + max_round = 30 + + from Apps.TeLaiDian.Kit import get_image_content_md5 + + for idx in range(max_round): + # 1. 先执行一次“大力向上滑动” + start_x = int(w * 0.9) + start_y = int(h * 0.85) + end_y = int(h * 0.25) + logger.info(f"[测试] 第 {idx + 1} 轮大幅向上滑动: ({start_x}, {start_y}) -> ({start_x}, {end_y})") + d.swipe(start_x, start_y, start_x, end_y, 0.25) + await asyncio.sleep(1.0) + + # 2. 滑动完成后再截图、比较内容是否还在变化 + screen_path = take_screenshot(d, f"tld_scrolltest_{int(time.time())}_{idx}.jpg") + logger.info(f"[测试] 第 {idx + 1} 轮滑动后的截图: {screen_path}") + curr_md5 = get_image_content_md5( + screen_path, + top_ratio=SAFE_EXCLUDE_RATIO, + bottom_ratio=BOTTOM_SAFE_EXCLUDE_RATIO, + ) + if last_md5 is not None and curr_md5 == last_md5: + stable_count += 1 + logger.info(f"[测试] 页面内容连续第 {stable_count} 次无变化") + else: + stable_count = 0 + last_md5 = curr_md5 + + if stable_count >= 2: + logger.info("[测试] 检测到页面多次无变化,认为已到达顶部固定区域,提前结束测试。") + break + + final_screen = take_screenshot(d, f"tld_scrolltest_end_{int(time.time())}.jpg") + logger.info(f"[测试] 结束时界面截图: {final_screen}") + + # 使用写死的归一化坐标点击顶部“价格”标签 + tab_x = int(PRICE_TAB_X_NORM * w / 1000) + tab_y = int(PRICE_TAB_Y_NORM * h / 1000) + logger.info(f"[测试] 使用固定归一化坐标点击顶部“价格”标签: 归一化({PRICE_TAB_X_NORM}, {PRICE_TAB_Y_NORM}) -> 像素({tab_x}, {tab_y})") + try: + d.click(tab_x, tab_y) + await asyncio.sleep(1.0) + after_tab_screen = take_screenshot(d, f"tld_scrolltest_after_price_tab_{int(time.time())}.jpg") + logger.info(f"[测试] 点击顶部“价格”标签后的界面截图: {after_tab_screen}") + try: + img = read_image(after_tab_screen) + if img is not None: + cv2.circle(img, (tab_x, tab_y), 20, (0, 0, 255), -1) + + entry_x = int(PRICE_ENTRY_X_NORM * w / 1000) + entry_y = int(PRICE_ENTRY_Y_NORM * h / 1000) + cv2.circle(img, (entry_x, entry_y), 20, (0, 0, 255), -1) + + debug_path = after_tab_screen.replace(".jpg", f"_click_{tab_x}_{tab_y}_price_{entry_x}_{entry_y}.jpg") + save_image(debug_path, img) + logger.info(f"[测试] 已在截图上标记价格标签和下方每度价格的红点: {debug_path}") + else: + logger.warning(f"[测试] 加载点击后截图失败,无法绘制红点: {after_tab_screen}") + except Exception as e: + logger.error(f"[测试] 绘制价格标签或下方价格红点失败: {e}") + except Exception as e: + logger.error(f"[测试] 点击顶部“价格”标签失败: {e}") + + logger.info("=== 特来电详情页第2页滚动安全性测试结束 ===") + + +if __name__ == "__main__": + try: + asyncio.run(run_scroll_test()) + except KeyboardInterrupt: + logger.info("用户中断了滚动测试。") + except Exception as e: + logger.exception(f"滚动测试运行异常: {e}") diff --git a/debug_cv.py b/debug_cv.py deleted file mode 100644 index b7a86f3..0000000 --- a/debug_cv.py +++ /dev/null @@ -1,93 +0,0 @@ - -import sys -import os -import cv2 -import numpy as np - -sys.path.append(os.getcwd()) -from Apps.TeLaiDian.Kit import detect_cards_cv as real_detect_cards_cv -from Apps.TeLaiDian.Config.Setting import SAFE_EXCLUDE_RATIO, BOTTOM_SAFE_EXCLUDE_RATIO - -def read_image(path): - if not path or not os.path.exists(path): - return None - try: - data = np.fromfile(path, dtype=np.uint8) - if data.size == 0: - return None - img = cv2.imdecode(data, -1) - return img - except Exception as e: - print(f"Error reading image {path}: {e}") - return None - -def detect_cards_cv(image_path, top_ratio=None, bottom_ratio=None): - if top_ratio is None: - top_ratio = SAFE_EXCLUDE_RATIO - if bottom_ratio is None: - bottom_ratio = BOTTOM_SAFE_EXCLUDE_RATIO - MIN_CARD_HEIGHT = 150 # Assuming default from Setting - - img = read_image(image_path) - if img is None: - print("Image not found or invalid") - return [] - - h, w = img.shape[:2] - print(f"Image Size: {w}x{h}") - - # 转换为灰度图 - gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) - - # 限制检测范围 - top_limit = int(h * top_ratio) - bottom_limit = int(h * (1 - bottom_ratio)) - print(f"CV limits: top={top_limit}, bottom={bottom_limit}, threshold_y={int(h * 0.58)}") - - # 使用自适应阈值 - thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2) - - # 闭运算 - kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (w // 4, 3)) - closed = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel) - - # 寻找轮廓 - contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - - min_card_width = int(w * 0.8) - - for cnt in contours: - x, y, cw, ch = cv2.boundingRect(cnt) - center_y = y + ch // 2 - - rect_area = cw * ch - cnt_area = cv2.contourArea(cnt) - extent = cnt_area / rect_area if rect_area > 0 else 0 - approx = cv2.approxPolyDP(cnt, 0.02 * cv2.arcLength(cnt, True), True) - - ok_width = cw >= min_card_width - ok_height = ch > MIN_CARD_HEIGHT * 0.8 - ok_vertical = center_y >= int(h * 0.58) and y > top_limit and y + ch < bottom_limit - - # Check green ratio - hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) - roi = hsv[max(0,y):min(h,y+ch), max(0,x):min(w,x+cw)] - green_mask = cv2.inRange(roi, np.array([35, 80, 80]), np.array([85, 255, 255])) - green_ratio = float(cv2.countNonZero(green_mask)) / (roi.shape[0]*roi.shape[1]) if roi.size > 0 else 0.0 - - ok_color = green_ratio < 0.25 - - if cw > w * 0.5: # Only print large enough boxes - print(f"Box: y={y}, h={ch}, w={cw}, center_y={center_y}, extent={extent:.2f}, green={green_ratio:.2f}") - print(f" Checks: width={ok_width}, height={ok_height}, vertical={ok_vertical}, color={ok_color}") - -image_path = r"d:\dsWork\aiData\Output\tld_list_1768359492_flag.jpg" -# Try the original if flag doesn't exist or is modified -original_path = r"d:\dsWork\aiData\Output\tld_list_1768359492.jpg" - -if os.path.exists(original_path): - print(f"Testing original image: {original_path}") - detect_cards_cv(original_path) -else: - print(f"Original image not found, trying flag: {image_path}") - detect_cards_cv(image_path)