'commit'
This commit is contained in:
@@ -286,6 +286,7 @@ class TeLaiDianCrawler(BaseCrawler):
|
||||
free_piles = None
|
||||
piles_detail = None
|
||||
parking_info = None
|
||||
submitted_price_task = False
|
||||
|
||||
logger.info(f"[详情页] 进入 crawl_detail_logic,场站: {station_name} | 地址: {address}")
|
||||
logger.info(f"[详情页] 已截取首屏截图,准备识别基础信息: {first_screen_path}")
|
||||
@@ -303,6 +304,15 @@ class TeLaiDianCrawler(BaseCrawler):
|
||||
free_piles = basic_info.get("free_piles")
|
||||
piles_detail = basic_info.get("piles_detail")
|
||||
parking_info = basic_info.get("parking_info")
|
||||
|
||||
if total_piles is None and isinstance(station_info, dict):
|
||||
tp = station_info.get("total_piles")
|
||||
if tp is not None:
|
||||
total_piles = tp
|
||||
if free_piles is None and isinstance(station_info, dict):
|
||||
fp = station_info.get("free_piles")
|
||||
if fp is not None:
|
||||
free_piles = fp
|
||||
|
||||
logger.info(f"[详情页] 基础信息识别结果: {station_name} | {address} | 桩数: {total_piles}/{free_piles} | 停车费: {parking_info}")
|
||||
except Exception as ex:
|
||||
@@ -388,8 +398,7 @@ class TeLaiDianCrawler(BaseCrawler):
|
||||
fail_screen = take_screenshot(d, f"tld_ocr_fail_{int(time.time())}.jpg")
|
||||
logger.error(f"❌ [OCR失败] 经过 {max_scroll_attempts} 次滚动仍未在页面中找到价格入口文字 (全部时段/全天价格统一)!")
|
||||
logger.error(f"❌ [OCR失败] 最终截图已保存至: {fail_screen}")
|
||||
logger.error("❌ [OCR失败] 程序将停止运行,请检查页面内容或识别逻辑。")
|
||||
sys.exit(1) # 停止程序
|
||||
logger.error("❌ [OCR失败] 将不进入电价页,继续以当前信息写入基础数据。")
|
||||
except Exception as e:
|
||||
logger.error(f"[详情页] 识别或点击价格入口失败: {e}")
|
||||
|
||||
@@ -447,6 +456,8 @@ class TeLaiDianCrawler(BaseCrawler):
|
||||
task = asyncio.create_task(
|
||||
self._analyze_and_save_prices_async(
|
||||
station_name=station_name,
|
||||
address=address,
|
||||
distance=distance,
|
||||
price_screenshots=price_screenshots,
|
||||
total_piles=total_piles,
|
||||
free_piles=free_piles,
|
||||
@@ -455,7 +466,25 @@ class TeLaiDianCrawler(BaseCrawler):
|
||||
)
|
||||
)
|
||||
self.pending_price_tasks.append(task)
|
||||
submitted_price_task = True
|
||||
logger.info(f"[详情页] 已后台提交 {len(price_screenshots)} 张电价截图进行识别与保存,继续后续流程不阻塞。")
|
||||
else:
|
||||
pass
|
||||
|
||||
if not submitted_price_task:
|
||||
try:
|
||||
await self.service.save_station_profile_and_status(
|
||||
station_name=station_name,
|
||||
address=address,
|
||||
total_piles=total_piles,
|
||||
free_piles=free_piles,
|
||||
piles_detail=piles_detail,
|
||||
parking_info=parking_info,
|
||||
distance=distance
|
||||
)
|
||||
logger.info(f"[详情页] 已基于整合信息写入基础数据: {station_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"[详情页] 写入基础数据失败: {e}")
|
||||
|
||||
async def crawl_list(self):
|
||||
"""
|
||||
@@ -471,7 +500,7 @@ class TeLaiDianCrawler(BaseCrawler):
|
||||
# 逻辑已在 crawl_list_logic 中通过 crawl_detail_logic 调用
|
||||
pass
|
||||
|
||||
async def _analyze_and_save_prices_async(self, station_name, price_screenshots, total_piles=None, free_piles=None, piles_detail=None, parking_info=None):
|
||||
async def _analyze_and_save_prices_async(self, station_name, address, distance, price_screenshots, total_piles=None, free_piles=None, piles_detail=None, parking_info=None):
|
||||
all_prices = []
|
||||
sem = asyncio.Semaphore(self.vlm_concurrency)
|
||||
|
||||
@@ -509,15 +538,17 @@ class TeLaiDianCrawler(BaseCrawler):
|
||||
|
||||
unique_prices.sort(key=lambda x: x.get("start", "00:00"))
|
||||
|
||||
await self.service.process_price_detail_data(
|
||||
station_name,
|
||||
unique_prices,
|
||||
await self.service.save_station_data(
|
||||
station_name=station_name,
|
||||
address=address,
|
||||
prices=unique_prices,
|
||||
total_piles=total_piles,
|
||||
free_piles=free_piles,
|
||||
piles_detail=piles_detail,
|
||||
parking_info=parking_info
|
||||
parking_info=parking_info,
|
||||
distance=distance,
|
||||
)
|
||||
logger.info(f"[详情页] {station_name} 后台价格信息处理完成,共 {len(unique_prices)} 条时段")
|
||||
logger.info(f"[详情页] {station_name} 后台价格信息处理完成,共 {len(unique_prices)} 条时段,并已写入数据库。")
|
||||
except Exception as e:
|
||||
logger.error(f"[详情页] 后台处理价格截图失败: {e}")
|
||||
finally:
|
||||
|
||||
@@ -285,7 +285,7 @@ class ReadImageKit:
|
||||
- **场站名称 (name)**: 通常位于卡片顶部,字体最大且加粗。
|
||||
- **距离信息 (distance)**: 例如 '5.3km' 或 '90m',通常在名称附近或卡片右上角。
|
||||
- **价格 (price)**: 例如 '0.84',通常以 ¥ 开头。
|
||||
- **枪数信息**: 如 '快 闲10/12'。
|
||||
- **枪数信息**: 如 '快 闲10/12',请解析为 total_piles 与 free_piles(若不可判定则为 null)。
|
||||
|
||||
对于每张卡片,请输出:
|
||||
- name: 场站名称
|
||||
@@ -293,6 +293,8 @@ class ReadImageKit:
|
||||
- address: 场站地址(如果卡片上有显示则提取,无则为 null)
|
||||
- point: 卡片中心点击坐标 [x, y],使用归一化坐标 [0-1000]
|
||||
- bbox: 卡片外接矩形边界 [x1, y1, x2, y2],使用归一化坐标 [0-1000]
|
||||
- total_piles: 卡片可见的总桩数(整数或 null)
|
||||
- free_piles: 卡片可见的空闲桩数(整数或 null)
|
||||
|
||||
输出格式为 JSON 数组。
|
||||
注意:严禁识别广告位、筛选标签或功能入口。真正的场站卡片通常是一个横跨屏幕的大区域。
|
||||
@@ -373,7 +375,9 @@ class ReadImageKit:
|
||||
"distance": res.get("distance"),
|
||||
"address": res.get("address"),
|
||||
"point": actual_p,
|
||||
"bbox": actual_bbox
|
||||
"bbox": actual_bbox,
|
||||
"total_piles": res.get("total_piles"),
|
||||
"free_piles": res.get("free_piles")
|
||||
})
|
||||
|
||||
if final_stations:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# coding=utf-8
|
||||
import asyncio
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
@@ -11,14 +12,23 @@ if project_root not in sys.path:
|
||||
sys.path.append(project_root)
|
||||
|
||||
from DbKit.Db import Db
|
||||
from Config.Config import DB_URL, PRICE_FLATTEN_TO_24H_GLOBAL
|
||||
try:
|
||||
from Config.Config import DB_URL, PRICE_FLATTEN_TO_24H_GLOBAL
|
||||
except ModuleNotFoundError:
|
||||
import importlib.util
|
||||
_config_path_cfg = os.path.join(project_root, "Config", "Config.py")
|
||||
_spec_cfg = importlib.util.spec_from_file_location("project_config_fallback", _config_path_cfg)
|
||||
_cfg_mod = importlib.util.module_from_spec(_spec_cfg)
|
||||
assert _spec_cfg.loader is not None
|
||||
_spec_cfg.loader.exec_module(_cfg_mod)
|
||||
DB_URL = _cfg_mod.DB_URL
|
||||
PRICE_FLATTEN_TO_24H_GLOBAL = _cfg_mod.PRICE_FLATTEN_TO_24H_GLOBAL
|
||||
from Model.StationProfile import StationProfile
|
||||
from Model.StationStatus import StationStatus
|
||||
from Model.StationPriceSchedule import StationPriceSchedule
|
||||
from Apps.TeLaiDian.Kit import setup_logger
|
||||
from Apps.TeLaiDian.Config.Setting import PRICE_FLATTEN_TO_24H
|
||||
|
||||
# 初始化日志
|
||||
logger = setup_logger("TeLaiDianService")
|
||||
|
||||
class TeLaiDianService:
|
||||
@@ -347,3 +357,52 @@ class TeLaiDianService:
|
||||
await session.commit()
|
||||
|
||||
return True
|
||||
|
||||
async def save_station_profile_and_status(self, station_name, address, total_piles=None, free_piles=None, piles_detail=None, parking_info=None, distance=None):
|
||||
if not station_name:
|
||||
return False
|
||||
now = datetime.now()
|
||||
station_hash = self.get_hash(station_name, address)
|
||||
async with await self.db.get_session() as session:
|
||||
profile_id = self.generate_id()
|
||||
await self.station_profile_model.save(
|
||||
session=session,
|
||||
id=profile_id,
|
||||
station_hash=station_hash,
|
||||
operator=self.operator,
|
||||
station_name=station_name,
|
||||
address=address,
|
||||
valid_start_time=now
|
||||
)
|
||||
status_id = self.generate_id()
|
||||
await self.station_status_model.save(
|
||||
session=session,
|
||||
id=status_id,
|
||||
station_hash=station_hash,
|
||||
total_piles=total_piles,
|
||||
free_piles=free_piles,
|
||||
piles_detail_json=piles_detail,
|
||||
parking_info=parking_info,
|
||||
distance=distance,
|
||||
current_price=None,
|
||||
valid_start_time=now
|
||||
)
|
||||
await session.commit()
|
||||
return True
|
||||
|
||||
async def _test_save_profile():
|
||||
service = TeLaiDianService()
|
||||
await service.init_db()
|
||||
try:
|
||||
ok = await service.save_station_profile_only(
|
||||
"测试场站-AI调试",
|
||||
"测试地址-杭州滨江",
|
||||
"1.2km"
|
||||
)
|
||||
print("save_station_profile_only_return:", ok)
|
||||
finally:
|
||||
await service.close_db()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(_test_save_profile())
|
||||
|
||||
BIN
Apps/TeLaiDian/__pycache__/TestCV.cpython-310.pyc
Normal file
BIN
Apps/TeLaiDian/__pycache__/TestCV.cpython-310.pyc
Normal file
Binary file not shown.
BIN
Apps/TeLaiDian/__pycache__/TestDetailCV.cpython-310.pyc
Normal file
BIN
Apps/TeLaiDian/__pycache__/TestDetailCV.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -157,6 +157,20 @@ class YiLaiTeCrawler(BaseCrawler):
|
||||
if before_md5 != after_md5:
|
||||
logger.info(f"成功进入详情页: {name}")
|
||||
|
||||
# 兜底:在进入详情页后,先基于列表页信息一次性写入 Profile 与 Status
|
||||
try:
|
||||
await self.service.save_station_profile_and_status(
|
||||
station_name=name,
|
||||
address=address,
|
||||
total_piles=station.get("total_piles"),
|
||||
free_piles=station.get("free_piles"),
|
||||
piles_detail=None,
|
||||
parking_info=None,
|
||||
distance=distance
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"兜底写入场站基础信息失败: {name}, {e}")
|
||||
|
||||
# --- 新增:点击“阶段性电价”按钮以获取完整电价列表 ---
|
||||
# 使用 OCR 探测价格入口
|
||||
dqdf_pos = detect_price_info_container_cv(detail_shot)
|
||||
|
||||
@@ -154,6 +154,8 @@ class ReadImageKit:
|
||||
- "name": 场站名称 (仅提取名称文字)
|
||||
- "address": 场站地址 (通常在名称下方,请完整提取地址文字)
|
||||
- "distance": 距离 (如 "1.2km", "800m" 等,如果存在请提取)
|
||||
- "total_piles": 总桩数 (如果存在请提取)
|
||||
- "free_piles": 空闲桩数 (如果存在请提取)
|
||||
- "is_valid": true/false (是否为真实的场站卡片)
|
||||
|
||||
注意:仅提取名称、地址和距离,不要包含价格。
|
||||
@@ -190,6 +192,8 @@ class ReadImageKit:
|
||||
"name": res.get("name"),
|
||||
"address": res.get("address"),
|
||||
"distance": res.get("distance"),
|
||||
"total_piles": res.get("total_piles"),
|
||||
"free_piles": res.get("free_piles"),
|
||||
"point": [(bbox[0] + bbox[2]) // 2, (bbox[1] + bbox[3]) // 2],
|
||||
"bbox": bbox
|
||||
})
|
||||
|
||||
@@ -195,6 +195,41 @@ class YiLaiTeService:
|
||||
logger.info(f"成功保存驿来特场站数据: {station_name} | 距离: {distance} | 价格条数: {cnt}")
|
||||
return True
|
||||
|
||||
async def save_station_profile_and_status(self, station_name, address=None, total_piles=None, free_piles=None, piles_detail=None, parking_info=None, distance=None):
|
||||
"""
|
||||
一次性写入场站基础信息与状态,用于列表页兜底
|
||||
"""
|
||||
if not station_name:
|
||||
return False
|
||||
station_hash = self.get_hash(station_name)
|
||||
now = datetime.now()
|
||||
async with await self.db.get_session() as session:
|
||||
profile_id = self.generate_id()
|
||||
await self.station_profile_model.save(
|
||||
session=session,
|
||||
id=profile_id,
|
||||
station_hash=station_hash,
|
||||
operator=self.operator,
|
||||
station_name=station_name,
|
||||
address=address,
|
||||
valid_start_time=now,
|
||||
)
|
||||
status_id = self.generate_id()
|
||||
await self.station_status_model.save(
|
||||
session=session,
|
||||
id=status_id,
|
||||
station_hash=station_hash,
|
||||
total_piles=total_piles,
|
||||
free_piles=free_piles,
|
||||
piles_detail_json=piles_detail,
|
||||
parking_info=parking_info,
|
||||
distance=distance,
|
||||
current_price=None,
|
||||
valid_start_time=now,
|
||||
)
|
||||
await session.commit()
|
||||
logger.info(f"兜底写入驿来特场站基础信息与状态: {station_name} | 桩数: {free_piles}/{total_piles} | 距离: {distance}")
|
||||
return True
|
||||
async def process_station_list_vl(self, image_path, json_metadata, device_info=None, max_count=None) -> list:
|
||||
"""
|
||||
基于 VL 模式处理场站列表 (整页识别)
|
||||
|
||||
BIN
Apps/YeLiTe/Test/__pycache__/killApp.cpython-310.pyc
Normal file
BIN
Apps/YeLiTe/Test/__pycache__/killApp.cpython-310.pyc
Normal file
Binary file not shown.
73
Apps/YeLiTe/Test/killApp.py
Normal file
73
Apps/YeLiTe/Test/killApp.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
|
||||
import uiautomator2 as u2
|
||||
|
||||
|
||||
def kill_all_apps():
|
||||
try:
|
||||
d = u2.connect()
|
||||
except Exception as e:
|
||||
print(f"设备连接失败: {e}")
|
||||
return False
|
||||
|
||||
print("返回桌面...")
|
||||
try:
|
||||
d.press("home")
|
||||
except Exception as e:
|
||||
print(f"按 Home 键失败: {e}")
|
||||
return False
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
print("打开快捷设置面板...")
|
||||
opened = False
|
||||
try:
|
||||
d.open_quick_settings()
|
||||
opened = True
|
||||
except Exception:
|
||||
try:
|
||||
d.open_notification()
|
||||
opened = True
|
||||
except Exception as e:
|
||||
print(f"打开快捷设置/通知面板失败: {e}")
|
||||
|
||||
if not opened:
|
||||
return False
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
print("查找“一键清理”按钮并点击...")
|
||||
btn = None
|
||||
for _ in range(3):
|
||||
btn = d(text="一键清理")
|
||||
if btn.exists:
|
||||
break
|
||||
btn = d(resourceId="com.android.systemui:id/statebutton3")
|
||||
if btn.exists:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
|
||||
if not btn or not btn.exists:
|
||||
print("未找到“一键清理”按钮,请确认系统界面支持此功能。")
|
||||
return False
|
||||
|
||||
try:
|
||||
btn.click()
|
||||
time.sleep(0.5)
|
||||
print("已点击“一键清理”,正在结束后台应用。")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"点击“一键清理”失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
success = kill_all_apps()
|
||||
if not success:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user