From 231f52d3eececa28701a9eabf13d3875e66919e8 Mon Sep 17 00:00:00 2001 From: HuangHai <10402852@qq.com> Date: Mon, 12 Jan 2026 21:49:19 +0800 Subject: [PATCH] 'commit' --- Apps/AiTeJiYiChong/Crawler.py | 40 ++++++++++++- Apps/AiTeJiYiChong/ReadImageKit.py | 5 ++ Apps/XinDianTu/Crawler.py | 54 ++++++++++++++++-- Apps/XinDianTu/ReadImageKit.py | 6 ++ .../__pycache__/Crawler.cpython-310.pyc | Bin 9743 -> 10490 bytes .../__pycache__/ReadImageKit.cpython-310.pyc | Bin 32109 -> 32175 bytes 6 files changed, 97 insertions(+), 8 deletions(-) diff --git a/Apps/AiTeJiYiChong/Crawler.py b/Apps/AiTeJiYiChong/Crawler.py index 5c6d17e..25d4abd 100644 --- a/Apps/AiTeJiYiChong/Crawler.py +++ b/Apps/AiTeJiYiChong/Crawler.py @@ -60,6 +60,7 @@ async def get_station_list(d, service, max_scrolls=MAX_SCROLLS): # 用于追踪后台分析任务 background_tasks = [] last_list_md5 = None + no_new_data_count = 0 for i in range(max_scrolls + 1): logger.info(f"正在处理第 {i + 1} 页...") @@ -112,6 +113,8 @@ async def get_station_list(d, service, max_scrolls=MAX_SCROLLS): # 等待二级页面加载 await asyncio.sleep(3) + should_back_to_list = True + # 截取二级页面图 detail_uuid = f"detail_{station_name}_{image_uuid}" detail_path = take_screenshot(d, detail_uuid, save_dir=TEMP_IMAGE_DIR) @@ -178,6 +181,29 @@ async def get_station_list(d, service, max_scrolls=MAX_SCROLLS): # 立即返回 d.press("back") await asyncio.sleep(0.5) + + # 检查返回后的页面状态 + check_back_path = take_screenshot(d, f"check_back_{detail_uuid}", save_dir=TEMP_IMAGE_DIR) + + check_list_md5 = Kit.get_image_content_md5( + check_back_path, + top_ratio=SAFE_EXCLUDE_RATIO, + bottom_ratio=BOTTOM_SAFE_EXCLUDE_RATIO + ) + check_detail_md5 = Kit.get_image_content_md5(check_back_path) + + if os.path.exists(check_back_path): os.remove(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]}),默认执行返回。") + should_back_to_list = True + else: logger.warning(f"点击 '{station_name}' 的 '分时价格' 按钮后页面无明显变化,跳过三级页面处理。") if os.path.exists(after_click_path): os.remove(after_click_path) @@ -185,8 +211,9 @@ async def get_station_list(d, service, max_scrolls=MAX_SCROLLS): logger.warning(f"场站 {station_name} 未找到分时价格入口。") # 从二级页面返回列表页 - d.press("back") - await asyncio.sleep(0.8) + if should_back_to_list: + d.press("back") + await asyncio.sleep(0.8) # 记录 Redis full_name_key = f"crawled:aite:{Kit.clean_station_name(station_name)}" @@ -199,6 +226,15 @@ async def get_station_list(d, service, max_scrolls=MAX_SCROLLS): logger.error(f"后台任务执行异常: {t.exception()}") background_tasks.remove(t) + if stations and new_stations_processed == 0: + no_new_data_count += 1 + logger.info(f"本页所有场站均已处理过,连续 {no_new_data_count} 页无新数据。") + if no_new_data_count >= 3: + logger.info("连续 3 页无新数据,判定为已到底或重复循环,提前结束。") + break + else: + no_new_data_count = 0 + # 5. 列表页翻页滑动 logger.info("执行翻页滑动...") start_x, start_y = w // 2, int(h * 0.8) diff --git a/Apps/AiTeJiYiChong/ReadImageKit.py b/Apps/AiTeJiYiChong/ReadImageKit.py index 2c5c735..f650e9e 100644 --- a/Apps/AiTeJiYiChong/ReadImageKit.py +++ b/Apps/AiTeJiYiChong/ReadImageKit.py @@ -217,6 +217,7 @@ class ReadImageKit: try: response = await cls._client.chat.completions.create( model=VL_MODEL_NAME, + timeout=30, messages=[ { "role": "user", @@ -275,6 +276,7 @@ class ReadImageKit: try: response = await cls._client.chat.completions.create( model=VL_MODEL_NAME, + timeout=30, messages=[ { "role": "user", @@ -342,6 +344,7 @@ class ReadImageKit: try: resp = await ReadImageKit._client.chat.completions.create( model=VL_MODEL_NAME, + timeout=30, messages=[ { "role": "user", @@ -470,6 +473,7 @@ class ReadImageKit: try: response = await cls._client.chat.completions.create( model=VL_MODEL_NAME, + timeout=30, messages=[ { "role": "user", @@ -524,6 +528,7 @@ class ReadImageKit: try: resp = await cls._client.chat.completions.create( model=VL_MODEL_NAME, + timeout=30, messages=[ { "role": "user", diff --git a/Apps/XinDianTu/Crawler.py b/Apps/XinDianTu/Crawler.py index e02d1b9..8e3e38a 100644 --- a/Apps/XinDianTu/Crawler.py +++ b/Apps/XinDianTu/Crawler.py @@ -110,13 +110,13 @@ async def is_already_crawled(redis_kit, station_name): return False -async def analyze_prices_background(service, station_name, image_paths): +async def analyze_prices_background(service, station_name, image_paths, device_info=None): """ 后台异步处理价格图片分析 """ try: # 使用多图解析并去重 - all_rows = await ReadImageKit.parse_price_schedule_multi(station_name, image_paths) + all_rows = await ReadImageKit.parse_price_schedule_multi(station_name, image_paths, device_info=device_info) if all_rows: # 24小时铺平 @@ -155,6 +155,7 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS): background_tasks = [] last_list_md5 = None + no_new_data_count = 0 for i in range(max_scrolls + 1): logger.info(f"正在处理第 {i + 1} 页...") @@ -203,6 +204,7 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS): # 5. 匹配几何卡片与 VL 识别结果 (XinDianTu 的 parse_vl_image 已经按顺序返回了) # 这里的 stations 列表长度应该与 json_metadata["cards"] 对应 + new_stations_processed = 0 if json_metadata.get("cards") and stations: for idx, card in enumerate(json_metadata["cards"]): # 检查索引是否越界 (VL 模型可能返回的数组长度不一致) @@ -216,6 +218,7 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS): logger.info(f"场站 {station_name} 匹配到已处理记录,跳过。") continue + new_stations_processed += 1 click_x, click_y = card["click_point"] logger.info(f"准备处理第 {idx + 1} 个场站: {station_name}, 点击坐标: ({click_x}, {click_y})") @@ -223,6 +226,9 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS): # 等待二级页面加载 await asyncio.sleep(WAIT_DETAIL_PAGE_LOAD) + + should_back_to_list = True + # 截取二级页面图 detail_uuid = str(uuid.uuid4()) @@ -272,12 +278,38 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS): # 【异步优化】后台处理价格 logger.info(f"已启动后台分析价格页: {station_name}") - task_price = asyncio.create_task(analyze_prices_background(service, station_name, price_screenshots)) + 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 os.path.exists(check_back_path): os.remove(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]}),默认执行返回。") + should_back_to_list = True + else: logger.warning(f"点击 '{station_name}' 的 '全部时段' 按钮后页面无明显变化,跳过三级页面处理。") @@ -286,9 +318,10 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS): else: logger.info(f"场站 {station_name} 未发现 '全部时段' 按钮。") - # 从二级页面返回 - d.press("back") - await asyncio.sleep(WAIT_BACK_TO_LIST) + # 从二级页面返回 (仅当确实需要返回时) + if should_back_to_list: + d.press("back") + await asyncio.sleep(WAIT_BACK_TO_LIST) # 记录 Redis 去重 (仅按名称去重) cleaned = Kit.clean_station_name(station_name) @@ -301,6 +334,15 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS): logger.error(f"后台任务异常: {t.exception()}") background_tasks.remove(t) + if stations and new_stations_processed == 0: + no_new_data_count += 1 + logger.info(f"本页所有场站均已处理过,连续 {no_new_data_count} 页无新数据。") + if no_new_data_count >= 3: + logger.info("连续 3 页无新数据,判定为已到底或重复循环,提前结束。") + break + else: + no_new_data_count = 0 + # 6. 翻页 logger.info("执行翻页滑动...") d.swipe_ext("up", scale=SCROLL_DISTANCE_RATIO) diff --git a/Apps/XinDianTu/ReadImageKit.py b/Apps/XinDianTu/ReadImageKit.py index e59c5dc..25198b8 100644 --- a/Apps/XinDianTu/ReadImageKit.py +++ b/Apps/XinDianTu/ReadImageKit.py @@ -243,6 +243,7 @@ class ReadImageKit: resp = await ReadImageKit._client.chat.completions.create( model=VL_MODEL_NAME, + timeout=30, messages=[ { "role": "user", @@ -301,6 +302,7 @@ class ReadImageKit: resp = await ReadImageKit._client.chat.completions.create( model=VL_MODEL_NAME, + timeout=30, messages=[ { "role": "user", @@ -345,6 +347,7 @@ class ReadImageKit: resp = await ReadImageKit._client.chat.completions.create( model=VL_MODEL_NAME, + timeout=30, messages=[ { "role": "user", @@ -865,6 +868,7 @@ class ReadImageKit: resp = await ReadImageKit._client.chat.completions.create( model=VL_MODEL_NAME_AD, # 使用更强的视觉模型 + timeout=30, messages=[ { "role": "user", @@ -972,6 +976,7 @@ class ReadImageKit: try: resp = await ReadImageKit._client.chat.completions.create( model=VL_MODEL_NAME, + timeout=30, messages=[ { "role": "user", @@ -1025,6 +1030,7 @@ class ReadImageKit: try: resp = await ReadImageKit._client.chat.completions.create( model=VL_MODEL_NAME, + timeout=30, messages=[ { "role": "user", diff --git a/Apps/XinDianTu/__pycache__/Crawler.cpython-310.pyc b/Apps/XinDianTu/__pycache__/Crawler.cpython-310.pyc index eb757722460e960df81ce44c46ecc1d94de0ea1d..69b24b8a0a3b6c39782191461af4471f1f77497f 100644 GIT binary patch delta 2465 zcmZuyZERat8NTP-o8WIdPU6^cJ{&tAP12B-7SW_s!N-56d8lk);$j|dy(AI$nrtE@(R%t7!(oD-=A}+1F)wuyPg&M{ z)SdM(AN5in@xt2ATxyZQIE&Sy!19TBe>Pw=Fqo?<^&3H$l>in&(p;JOHJ5PpGv6g? z4T}IyBOqpOqmc(9iTR8s(m;YH)&jM8Bg`A94G!L_S(`VQsPHaeHj^eEIX^)|EI^t` zh_saDRj(GLEhQt&f+o@$pO=VJkrZiBMpDi(Div*pJdq7FtArR%)}Q zg*LRXMnfU(CaPfdIt%fU##ke1kJybt$FF_|Yhf+I!gly9=%)UZg;+Rp-%M*|t$@=3 zMMYV(f(nTWnpbESQ&@Bn+cYXjJt&BDPO&Jkh}k5#BWoe}paf(hq$?sp!Zxj4xW-wy zb{Y~Qrkk}vWDjeHv3JcWDwd2mO|Z5_1Y^5y_`6CS;+%BU&Pk_0>f`aX2Dci$3}wN> zI&GbRc8ocVJuJo%Yr1zpjt0=Z>;LH9#X5!V{n>#UMy*IK&f+CdVh<>a)GZdb=i;~D zL?>|$5vP_Q16nsrfRY+Z5EOwn6?q5v3g5p1DSEVCQNSSU5vBKv((43Kx8VUINb6%D z0_$PDCIEjHIXel7~s~BAqMrV;hH9UC=UgaT0iS!{j)M8pCQ9q_l%?s&^m(2 zzC}DK9Us#M>7cQfMU5d6GLealVUyp@5jF^DvbI+!y|;!s#0Egap^|9JAQ=^BX+OTNOaIM{=uCY|C`J!<=ci1$I$v0Z3TYn(-a$UdfI_mi)( zeOya&h#nwc`|_5-g90a+e4QLFospPxl0R4@Yy?DX(MH**@sP>y%da8rLGqZr{_;RV zbwM`DX@MFy#Vz~{t^}IJ5b+svi_75_gdb;aDDH_GVK-=82R?rldy^aiN-KqPSiG>j z!Y8@WZUKFGw*_=!x4QWW=B&tSRDe7M*W4*yiKlA{wq_q;PI80{XVA*-nc~>baBMRC zZK?lg@en-kt{8mot!(ZOeu3NXc#4@+ayp%(X*K!U1FO?d>BpLfoKyAFFQunvlh5hj zYd%pbIsjq&^

`Td}_4F$84veP{7{_03;Y*FWluRnIT){CM4}tn8Q{ z?W|p`zG+l1u2e7HwpK4%=P&&2SLg42ym{}_rMsUjZ{K>?_OY&=*{+=Z%fjo$1fa&M zm*2Bif3&@_@c^{vzu4fY|GD`z8q%jiM@xecQoZ~>AXS$as+X6o%eS^~{KQ&YvOfNq z4Gh_M+E3r!-ufU`Y~(4pHxRq9QGH{hT3+X=zXkT1&-nlDL%4PBnzjDc-A`_D*mE1! ztv9OYF6=Coqt>;ub^E>Tl@GzGx>~W8%iCLeb?pjg`A%p$QVas{&eGD(rL%XpwyYnT z*82JCxk~?|o3WM|)T=sS`%dS{OpYW|r%okl=H+zqM3K^bE;*ggt0a?4(X@UoOwpJg zi5zL1YRKi2x%8_^lA_SQd@)D$ROFTLREQf3G(|J{Tp>B9=BLwzLYnA*jJ)`Qnl9Xt z=kkTAplElRzrot#U}r0?MLYw$fi6*!STnOD=v=~EeK`pe|NR3LSd9}e3vm!dCC z`R})Yi~e})E8)LG33#DiaiAco#}4E`ajakk>3?WFd`eM*D1^dbcEMjz4k86Pum|ly zO*kNXWFHF1A=nAXJiMu{8!0#r5Mkt!Jve}xWQYSW1^G}1d^rllAY~x{Sg?XIh{G~o z2x9%kXj41@sl*0^;D0HK3sL|tNCIg9NWHRoHkv~I@!{;;%y;X??4SLcpSL}S&{6#x z9pmW6W(odNq7U@0*dR*l&&Hx?V>1)`Epp$0Qq>JUT+t`u-$(QMR{ZyFlN+w`;T?S? x(SvSo{yZ^)(EGa8{k;1Mx7%_0(>)(X4mqMOk8216DG$^UfT|n|_}|Ju{{!@I{Ote$ delta 1724 zcmZuyOKclO7~X$&6a4nZiQje{Kk{(Wq;1kRZGonhatIX%P%BkDqO@Vs$S&kzPfLpa zm4l2ZLWEjciA$As7YPpK(IVd{H-uDfT*(bYk;(yy3lc>L{@Ha%k??9}XJ`KZ`@ip- z+4ZC6uRj}F4F-J*{XYARSI!*$BJ_PAJVuW}YF|^UPjs2T2Y5qtp~_iYX~L}1V)G{q zzfdj4edw37Hx%yQR&4-9bc-h}k43R>;W@S9PlggErLGMA(YR5h&0@MOQQo89pa8j6w6)V7%chV$ka2Jv?GFgXW!a zFO7BKLWK$HR%fi+Q2&S!qP$PSu1|{?hIy37czjE}9yEGH+)7}N4Myag!o8ZJDNPlW z70*GX{?+S}2wg8w6G@VsXkdVDz1y16D^gb4R*XLEvohR^teVAM)NPGIrB6v&4x!qQ zI!4gLQ(NjYRA5dqqRx6+5V#LyT)`k|Vox-T>#Q;WlHtFsF%+I{cj7H3e zW6VvxL=M$vFp9C{VL@MbX6>A8WE%Wq#5qiBW3| zlh!^Sv%v;y+?L0GAC^dtY79D32Rl-Sut*{rLr#$rAE!-%+9On|6MTa2=LfcS3m)7n zNXCEb5S{$5Lt;MyIaF-Ydu9wf?hJRxAK+7tPbsU!5A7kLVmT`fSCx3Ii11qkPe3E6 zRj2r29Fd#BkBCEjdiQ7okzH5#QGV<#1--NKl`dnMJct{kIBFfX z6?p&O?QvpHN^CxJMk$|apXlvUOJ%m>2AH){TX8bBd&A;NO%^wqssk4_O^x1oO?b^c|Z~t`f-h+4T2cN(9^E<8b@%DUj z8L~U+iX# znvlH`yHDtBvs?2(1iG0AJWya7Gp}b)HX>RK5;WzbE22g~g8&U zFGBdxM0IWXjjl=eHr4(icNU;*9?4I^?RGU!PJe0!ieuSXpg#v`72DJp@glZ5A%>Xt zN9|7gZ1EfL-=@{t9ogM9OM`F1bLPi`_x+Zfux0n5d39(I?zV3Y9RT>m{BHQ1UvjCr ddFJHEon*$7_62<7j2KI;Iw|#RvcI)I{{h;1xs3n- diff --git a/Apps/XinDianTu/__pycache__/ReadImageKit.cpython-310.pyc b/Apps/XinDianTu/__pycache__/ReadImageKit.cpython-310.pyc index a7e139665c2c1813c19b260f4c18f87d13319ceb..28e8d9f0e1a8f433046d53bc012c157471679b83 100644 GIT binary patch delta 5110 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