Files
aiData/Apps/AiTeJiYiChong/Crawler.py
HuangHai 8606709510 'commit'
2026-01-12 15:31:25 +08:00

208 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# coding=utf-8
import asyncio
import logging
import uuid
import os
import sys
import time
from PIL import Image
# 将项目根目录添加到 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)
import uiautomator2 as u2
from Apps.AiTeJiYiChong import Kit
from Apps.AiTeJiYiChong.Kit import take_screenshot
from Apps.AiTeJiYiChong.ReadImageKit import ReadImageKit
from Util.RedisKit import RedisKit
from Apps.AiTeJiYiChong.Service import AiTeJiYiChongService
from Config.Config import TEMP_IMAGE_DIR
from Apps.AiTeJiYiChong.Config.Setting import (
SCROLL_DISTANCE_RATIO,
MAX_SCROLLS, REDIS_STATION_EXPIRE,
WAIT_AFTER_SCROLL,
MAX_CRAWL_DISTANCE
)
logger = logging.getLogger("AiTeJiYiChongCrawler")
async def get_station_list(d, service, max_scrolls=MAX_SCROLLS):
"""
获取场站列表并处理翻页
"""
redis_kit = RedisKit()
window_size = d.window_size()
w, h = window_size[0], window_size[1]
device_info = d.info
device_info['width'] = w
device_info['height'] = h
logger.info(f"开始爬取列表,设备: {device_info.get('productName')} | 分辨率: {w}x{h}")
for i in range(max_scrolls + 1):
logger.info(f"正在处理第 {i + 1} 页...")
# 1. 拍摄截图
image_uuid = str(uuid.uuid4())
screenshot_path = take_screenshot(d, image_uuid, save_dir=TEMP_IMAGE_DIR)
logger.info(f"列表页截图已完成: {screenshot_path}")
# 2. 执行图形学分析,生成 _flag.jpg, _vl.jpg 和 .json
logger.info("正在执行图形学切片分析...")
json_data = Kit.crop_cards_from_image(screenshot_path)
# 3. 调用 VL 模型识别并保存数据
stations = await service.process_station_list_vl(screenshot_path, device_info=device_info)
logger.info(f"本页识别到 {len(stations)} 个场站")
# 4. 匹配几何卡片与 VL 识别结果
if json_data and json_data.get("cards") and stations:
for card in json_data["cards"]:
card_rect = card["rect"] # [x1, y1, x2, y2]
for st in stations:
st_bounds = st.get("bounds") # [x1, y1, x2, y2] (0-1000)
if not st_bounds: continue
# 转换 VL 坐标到像素坐标
st_y1_px = st_bounds[1] * h / 1000
st_y2_px = st_bounds[3] * h / 1000
# 计算 y 轴重叠
overlap = min(card_rect[3], st_y2_px) - max(card_rect[1], st_y1_px)
if overlap > 50: # 有显著重叠
card["station_name"] = st.get("station_name")
logger.info(f"匹配成功: 几何卡片 {card_rect} -> 场站 {card['station_name']}")
break
# 5. 遍历处理本页所有场站
if json_data and json_data.get("cards"):
for card_idx, card in enumerate(json_data["cards"]):
station_name = card.get("station_name")
if not station_name:
logger.warning(f"第 {card_idx + 1} 个几何卡片未匹配到场站名称,跳过。")
continue
# 检查 Redis 去重
redis_key = f"crawled:aite:{station_name}"
if await redis_kit.get_data(redis_key):
logger.info(f"场站 {station_name} 已处理,跳过。")
continue
click_x, click_y = card["click_point"]
logger.info(f"准备处理第 {card_idx + 1} 个场站: {station_name}, 点击坐标: ({click_x}, {click_y})")
d.click(click_x, click_y)
# 等待二级页面加载
await asyncio.sleep(3)
# 截取二级页面图
detail_uuid = f"detail_{station_name}_{image_uuid}"
detail_path = take_screenshot(d, detail_uuid, save_dir=TEMP_IMAGE_DIR)
# 5. 调用详情页处理逻辑 (二级页面:提取地址)
logger.info(f"正在解析详情页基础数据: {detail_path}")
detail_data = await service.process_station_detail(detail_path, station_name=station_name)
# 6. 寻找 timePrice.jpg 图标并进入三级页面 (分时价格页)
time_price_template = os.path.join(os.path.dirname(__file__), "BiaoShi", "timePrice.jpg")
coords = Kit.find_template_coords(detail_path, time_price_template)
if coords:
cx, cy, conf = coords
logger.info(f"找到分时价格图标,进入三级页面...")
d.click(cx, cy)
await asyncio.sleep(3)
# 截取三级页面并处理滑动价格
price_detail_uuid = f"price_detail_{station_name}_{image_uuid}"
price_detail_path = take_screenshot(d, price_detail_uuid, save_dir=TEMP_IMAGE_DIR)
all_prices = []
# 初始识别
prices = await ReadImageKit.get_price_detail_from_image(price_detail_path)
if prices: all_prices.extend(prices)
last_md5 = Kit.get_file_md5(price_detail_path)
# 滑动逻辑
for scroll_idx in range(3):
logger.info(f"执行第 {scroll_idx + 1} 次滑动以抓取更多价格...")
d.swipe(w // 2, int(h * 0.7), w // 2, int(h * 0.3), duration=0.5)
await asyncio.sleep(2)
scroll_path = take_screenshot(d, f"price_scroll_{scroll_idx}_{station_name}", save_dir=TEMP_IMAGE_DIR)
current_md5 = Kit.get_file_md5(scroll_path)
if current_md5 == last_md5:
logger.info("检测到屏幕未发生变化(已滑到底部),停止滑动识别。")
break
last_md5 = current_md5
new_prices = await ReadImageKit.get_price_detail_from_image(scroll_path)
if new_prices:
for np_item in new_prices:
if not any(np_i.get("start") == np_item.get("start") for np_i in all_prices):
all_prices.append(np_item)
else: break
if all_prices:
all_prices.sort(key=lambda x: x.get("start", "00:00"))
hourly_schedule = ReadImageKit.expand_schedule_to_24h(all_prices)
await service.process_price_detail_data(station_name, hourly_schedule)
logger.info(f"场站 {station_name} 三级页面价格处理完成。")
# 从三级页面返回二级页面
logger.info("从三级页面返回二级页面...")
d.press("back")
await asyncio.sleep(1.5)
else:
logger.warning(f"场站 {station_name} 未找到分时价格入口。")
# 从二级页面返回列表页
logger.info("从二级页面返回列表页...")
d.press("back")
await asyncio.sleep(1.5)
# 记录 Redis
await redis_kit.set_data(redis_key, "1", expire=REDIS_STATION_EXPIRE)
# 5. 翻页滑动 (如果没进入二级页面)
logger.info("执行翻页滑动...")
start_x, start_y = w // 2, int(h * 0.8)
end_x, end_y = w // 2, int(h * (0.8 - SCROLL_DISTANCE_RATIO))
d.swipe(start_x, start_y, end_x, end_y, duration=0.5)
await asyncio.sleep(WAIT_AFTER_SCROLL)
logger.info("达到最大翻页次数,爬取结束。")
return True
async def main(service=None, do_cleanup=True):
"""
爬虫主入口
"""
if do_cleanup:
Kit.clear_temp_dir()
if service is None:
service = AiTeJiYiChongService()
await service.init_db()
d = u2.connect()
try:
await get_station_list(d, service)
return True
except Exception as e:
logger.error(f"爬取过程中出现异常: {e}")
return False
finally:
# 如果是内部初始化的 service,则在此关闭
pass
if __name__ == "__main__":
asyncio.run(main())