'commit'
This commit is contained in:
@@ -30,7 +30,9 @@ from Apps.XinDianTu.Config.Setting import (
|
||||
SCROLL_DISTANCE_RATIO,
|
||||
MAX_SCROLLS, REDIS_STATION_EXPIRE,
|
||||
WAIT_DETAIL_PAGE_LOAD, WAIT_BACK_TO_LIST, WAIT_AFTER_SCROLL,
|
||||
MAX_CRAWL_DISTANCE
|
||||
MAX_CRAWL_DISTANCE,
|
||||
SAFE_EXCLUDE_RATIO,
|
||||
BOTTOM_SAFE_EXCLUDE_RATIO
|
||||
)
|
||||
|
||||
# --- 用户配置区域 ---
|
||||
@@ -152,6 +154,7 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
logger.info(f"开始爬取列表,设备: {device_info.get('productName')} | 分辨率: {w}x{h}")
|
||||
|
||||
background_tasks = []
|
||||
last_list_md5 = None
|
||||
|
||||
for i in range(max_scrolls + 1):
|
||||
logger.info(f"正在处理第 {i + 1} 页...")
|
||||
@@ -159,6 +162,19 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
# 1. 拍摄截图
|
||||
image_uuid = str(uuid.uuid4())
|
||||
screenshot_path = take_screenshot(d, image_uuid, save_dir=TEMP_IMAGE_DIR)
|
||||
|
||||
# 检查是否已经滚动到底部 (排除状态栏后,内容与上次一致)
|
||||
current_md5 = Kit.get_image_content_md5(
|
||||
screenshot_path,
|
||||
top_ratio=SAFE_EXCLUDE_RATIO,
|
||||
bottom_ratio=BOTTOM_SAFE_EXCLUDE_RATIO
|
||||
)
|
||||
if last_list_md5 and current_md5 == last_list_md5:
|
||||
logger.info("检测到列表核心内容无变化,已到达底部或加载完成,提前结束。")
|
||||
if os.path.exists(screenshot_path): os.remove(screenshot_path)
|
||||
break
|
||||
last_list_md5 = current_md5
|
||||
|
||||
logger.info(f"列表页截图已完成: {screenshot_path}")
|
||||
|
||||
# 2. 执行本地图形学分析,生成 _vl.jpg 和 .json
|
||||
@@ -224,30 +240,51 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
|
||||
# --- 抓取价格时段信息 ---
|
||||
# 识别 "全部时段" 按钮
|
||||
pos = Kit.find_expand_button_position(detail_path, debug_dir=TEMP_IMAGE_DIR, debug_filename_prefix=station_name)
|
||||
if pos:
|
||||
# 优先使用 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
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
# 【异步优化】后台处理价格
|
||||
logger.info(f"已启动后台分析价格页: {station_name}")
|
||||
task_price = asyncio.create_task(analyze_prices_background(service, station_name, price_screenshots))
|
||||
background_tasks.append(task_price)
|
||||
# 检查页面是否真的变化了
|
||||
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)
|
||||
|
||||
# 【异步优化】后台处理价格
|
||||
logger.info(f"已启动后台分析价格页: {station_name}")
|
||||
task_price = asyncio.create_task(analyze_prices_background(service, station_name, price_screenshots))
|
||||
background_tasks.append(task_price)
|
||||
|
||||
# 从三级页面返回
|
||||
d.press("back")
|
||||
await asyncio.sleep(0.5)
|
||||
# 从三级页面返回二级页面
|
||||
d.press("back")
|
||||
await asyncio.sleep(0.5)
|
||||
else:
|
||||
logger.warning(f"点击 '{station_name}' 的 '全部时段' 按钮后页面无明显变化,跳过三级页面处理。")
|
||||
|
||||
# 清理检查用的截图
|
||||
if os.path.exists(after_click_path): os.remove(after_click_path)
|
||||
else:
|
||||
logger.info(f"场站 {station_name} 未发现 '全部时段' 按钮。")
|
||||
|
||||
# 从二级页面返回
|
||||
d.press("back")
|
||||
|
||||
@@ -24,6 +24,28 @@ def read_image(path):
|
||||
return None
|
||||
|
||||
|
||||
def get_image_content_md5(file_path, top_ratio=0.1, bottom_ratio=0.1):
|
||||
"""
|
||||
计算图片核心内容的 MD5 值(排除状态栏和导航栏)
|
||||
"""
|
||||
img = read_image(file_path)
|
||||
if img is None:
|
||||
return None
|
||||
|
||||
h, w = img.shape[:2]
|
||||
top = int(h * top_ratio)
|
||||
bottom = int(h * (1 - bottom_ratio))
|
||||
|
||||
# 裁剪中间部分
|
||||
content = img[top:bottom, :]
|
||||
|
||||
# 将图片数据转换为字节流计算 MD5
|
||||
success, encoded_img = cv2.imencode(".jpg", content)
|
||||
if success:
|
||||
return hashlib.md5(encoded_img.tobytes()).hexdigest()
|
||||
return hashlib.md5(content.tobytes()).hexdigest()
|
||||
|
||||
|
||||
def save_image(path, img):
|
||||
"""保存图片,支持中文路径及鲁棒性检查"""
|
||||
try:
|
||||
|
||||
@@ -156,34 +156,14 @@ async def open_mini_program():
|
||||
tab_chat.click()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 1. 点击搜索按钮(放大镜图标)
|
||||
# 仅保留坐标点击逻辑
|
||||
# window_size = d.window_size()
|
||||
# w, h = window_size[0], window_size[1]
|
||||
#
|
||||
# # 精确相对坐标:x=84%, y=8% (基于截图推算)
|
||||
# click_x = int(w * 0.84)
|
||||
# click_y = int(h * 0.08)
|
||||
# logger.info(f"使用精确坐标点击搜索按钮: ({click_x}, {click_y})")
|
||||
# d.click(click_x, click_y)
|
||||
|
||||
# 点击搜索按钮(放大镜图标)
|
||||
logger.info("尝试查找并点击 '搜索按钮(放大镜图标)'...")
|
||||
search_template = os.path.join(BASE_DIR, "Templates", "SearchButton.jpg")
|
||||
if not os.path.exists(search_template):
|
||||
search_template = os.path.join(BASE_DIR, "SearchButton.jpg")
|
||||
|
||||
if click_image_template(d, search_template):
|
||||
logger.info("点击了搜索按钮(放大镜图标)")
|
||||
else:
|
||||
logger.warning("未找到搜索按钮(放大镜图标),尝试兜底坐标点击...")
|
||||
# 兜底点击:精确相对坐标:x=84%, y=8% (基于截图推算)
|
||||
window_size = d.window_size()
|
||||
w, h = window_size[0], window_size[1]
|
||||
click_x = int(w * 0.84)
|
||||
click_y = int(h * 0.08)
|
||||
d.click(click_x, click_y)
|
||||
logger.info(f"使用精确坐标点击搜索按钮: ({click_x}, {click_y})")
|
||||
# 1. 直接使用坐标点击搜索按钮(放大镜图标)
|
||||
logger.info("直接使用百分比坐标点击 '搜索按钮' (84%, 8%)...")
|
||||
window_size = d.window_size()
|
||||
w, h = window_size[0], window_size[1]
|
||||
click_x = int(w * 0.84)
|
||||
click_y = int(h * 0.08)
|
||||
d.click(click_x, click_y)
|
||||
logger.info(f"使用精确坐标点击搜索按钮: ({click_x}, {click_y})")
|
||||
|
||||
# 点击后给予一定的加载时间
|
||||
await asyncio.sleep(2)
|
||||
@@ -227,18 +207,11 @@ async def open_mini_program():
|
||||
# logger.info(f"尝试坐标点击第一项: ({res_x}, {res_y})")
|
||||
# d.click(res_x, res_y)
|
||||
|
||||
# 点击符合新电途图标的小程序
|
||||
logger.info("尝试查找并点击 '新电途小程序'...")
|
||||
xdt_template = os.path.join(BASE_DIR, "Templates", "xdt.jpg")
|
||||
if click_image_template(d, xdt_template):
|
||||
logger.info("点击了新电途的小程序")
|
||||
else:
|
||||
logger.warning("未找到新电途小程序图标,尝试坐标点击第一项...")
|
||||
window_size = d.window_size()
|
||||
w, h = window_size[0], window_size[1]
|
||||
res_x, res_y = int(w * 0.5), int(h * 0.18)
|
||||
d.click(res_x, res_y)
|
||||
logger.info(f"坐标点击搜索结果第一项: ({res_x}, {res_y})")
|
||||
# 3. 直接使用坐标点击搜索结果第一项 (50%, 18%)
|
||||
logger.info("直接使用百分比坐标点击 '搜索结果第一项' (50%, 18%)...")
|
||||
res_x, res_y = int(w * 0.5), int(h * 0.18)
|
||||
d.click(res_x, res_y)
|
||||
logger.info(f"坐标点击搜索结果第一项: ({res_x}, {res_y})")
|
||||
|
||||
await asyncio.sleep(10)
|
||||
# 进入小程序后,检查并处理广告
|
||||
|
||||
@@ -18,6 +18,17 @@ from Apps.XinDianTu.Config.Setting import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 场站过滤黑名单
|
||||
STATION_EXCLUDED_TITLES = {
|
||||
"畅充会员", "组团充电", "微信社群", "申卡有礼", "夜间免停",
|
||||
"洗手间", "不限车长", "不限车高", "组团", "综合排序", "距离优先",
|
||||
"价格最低", "空闲较多", "筛选", "地图", "列表"
|
||||
}
|
||||
|
||||
STATION_BLACKLIST_KEYWORDS = [
|
||||
"广告", "优惠券", "点击进入", "立即参与", "咨询电话", "活动详情"
|
||||
]
|
||||
|
||||
|
||||
class ReadImageKit:
|
||||
_client = AsyncOpenAI(
|
||||
@@ -36,18 +47,18 @@ class ReadImageKit:
|
||||
"仅输出JSON数组(不含任何说明文字),按从左到右、从上到下的顺序识别图片中的充电站区域。识别规则如下:\n"
|
||||
"1. 必须是卡片形式的充电站信息区域。\n"
|
||||
"2. 每一个卡片必须同时具备以下所有要素,否则严禁识别:\n"
|
||||
" - 场站名称 (station_name);\n"
|
||||
" - **场站名称 (station_name)**: 场站名称通常位于卡片的第一行(左上角),字体最大且加粗。严禁提取'距离优先'、'空闲较多'、'价格最低'等筛选标签。严禁将卡片中下部的描述信息(如'导航到...'、'停车说明...'、'距您...'等)识别为名称。\n"
|
||||
" - 距离信息 (distance, 例如 '5.3km' 或 '90m'),位于卡片右上角;\n"
|
||||
" - 金额/电费 (price,例如 '0.8490');\n"
|
||||
" - 充电枪信息 (piles,包含'超'、'快'或'慢'的类型、总枪数和空闲枪数,例如 '快 闲4/4')。\n"
|
||||
"3. 如果缺少上述任何一项要素(例如只有名称和距离,但没有电费或枪数信息),说明它不是真正的场站卡片(可能是广告或功能入口),请直接跳过。\n"
|
||||
" - **充电枪信息 (piles)**: 包含'超'、'快'或'慢'的类型、总枪数和空闲枪数(例如 '快 闲4/4')。如果图中只显示数字(如 '34/42')而没有明确标明类型,请统一识别为 `type: \"慢\"`。\n"
|
||||
"3. 如果缺少上述任何一项要素(例如只有名称 and 距离,但没有电费或枪数信息),说明它不是真正的场站卡片(可能是广告或功能入口),请直接跳过。\n"
|
||||
"\n"
|
||||
"JSON对象字段要求:\n"
|
||||
"1. b_use: 状态标识(1或0)。如果场站名称为灰色或带有“暂停使用”等标签,则为0,否则为1。\n"
|
||||
"2. station_name: 场站名称;\n"
|
||||
"3. price: 一度电的价格(数字);\n"
|
||||
"4. pro_price: Pro会员价格(数字),无则为null;\n"
|
||||
"5. piles: 充电枪列表 [{type: '快', free: 4, total: 4}];\n"
|
||||
"5. piles: 充电枪列表 [{type: '快'|'慢'|'超', free: 4, total: 4}];\n"
|
||||
"6. parking: 停车费用描述(通常在'P'图标后,例如 '收费停车:以场站实际收费规则为准' 或 '限时免费停车...')。\n"
|
||||
"7. distance: 距离信息字符串(例如 '5.3km' 或 '90m')。\n"
|
||||
"8. bounds: {x1,y1,x2,y2} 区域像素坐标(0-1000);\n"
|
||||
@@ -454,7 +465,7 @@ class ReadImageKit:
|
||||
"1. station_name: 场站名称;\n"
|
||||
"2. price: 价格(数字);\n"
|
||||
"3. pro_price: Pro会员价(数字,无则null);\n"
|
||||
"4. piles: 充电枪描述字符串(例如 '快 闲4/4');\n"
|
||||
"4. piles: 充电枪列表 [{type: '快'|'慢'|'超', free: 4, total: 4}]。如果图中只显示数字(如 '34/42')而没有明确标明类型,请统一识别为 `type: \"慢\"`;\n"
|
||||
"5. tags: 标签列表(如 ['限时免费']);\n"
|
||||
"6. parking: 停车费用描述(通常在'P'图标后,例如 '收费停车:以场站实际收费规则为准' 或 '限时免费停车...')。\n"
|
||||
"7. distance: 距离信息字符串(例如 '5.3km')。\n"
|
||||
@@ -583,7 +594,6 @@ class ReadImageKit:
|
||||
|
||||
return final_stations
|
||||
|
||||
@staticmethod
|
||||
@staticmethod
|
||||
def get_card_segments(image_path, output_dir="./Debug/Patches"):
|
||||
"""
|
||||
@@ -785,7 +795,18 @@ class ReadImageKit:
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "请识别图片中的充电站信息,并以 JSON 格式输出:{\"station_name\": \"...\", \"price\": \"...\", \"piles\": \"空闲数/总数\"}。只输出 JSON,不要有其他文字。"
|
||||
"text": (
|
||||
"你是一个专业的充电桩信息提取助手。请识别图片中充电站卡片的核心信息。\n"
|
||||
"注意以下识别规则:\n"
|
||||
"1. **场站名称 (station_name)**: 场站名称通常位于卡片的第一行,字体较大且加粗。严禁提取'距离优先'、'空闲较多'、'价格最低'等筛选标签。严禁将卡片中间的长段描述信息识别为名称。\n"
|
||||
"2. **价格 (price)**: 提取数字部分,通常以'元/度'结尾。\n"
|
||||
"3. **桩数 (piles)**: 识别所有类型的充电桩。请提取'快'、'慢'或'超'字样及其对应的'空闲/总数'(如 '快 4/4')。\n"
|
||||
" - 如果图中只显示数字(如 '34/42')而没有明确标明类型,请统一识别为 `type: \"慢\"`。\n"
|
||||
" - 必须以列表形式输出所有识别到的类型。\n"
|
||||
"请以 JSON 格式输出:\n"
|
||||
"{\"station_name\": \"场站名称\", \"price\": \"价格\", \"piles\": [{\"type\": \"快\", \"free\": 4, \"total\": 4}]}\n"
|
||||
"只输出 JSON,不要有任何解释文字。"
|
||||
)
|
||||
},
|
||||
{"type": "image_url", "image_url": {"url": patch_url}}
|
||||
]
|
||||
|
||||
@@ -36,6 +36,25 @@ class XinDianTuService:
|
||||
def get_hash(self, s: str) -> str:
|
||||
return hashlib.md5(s.encode('utf-8')).hexdigest()
|
||||
|
||||
def _to_float(self, value) -> float:
|
||||
"""
|
||||
从字符串中提取浮点数,过滤掉非数字字符
|
||||
"""
|
||||
if value is None:
|
||||
return 0.0
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
|
||||
import re
|
||||
try:
|
||||
# 提取第一个匹配的数字部分(包含小数点)
|
||||
match = re.search(r"[-+]?\d*\.?\d+", str(value))
|
||||
if match:
|
||||
return float(match.group())
|
||||
return 0.0
|
||||
except:
|
||||
return 0.0
|
||||
|
||||
async def init_db(self):
|
||||
await self.db.init_db()
|
||||
|
||||
@@ -129,12 +148,7 @@ class XinDianTuService:
|
||||
})
|
||||
|
||||
price_str = station.get("price", "")
|
||||
current_price = None
|
||||
try:
|
||||
price_match = re.search(r"\d+\.\d+", str(price_str))
|
||||
if price_match:
|
||||
current_price = float(price_match.group())
|
||||
except: pass
|
||||
current_price = self._to_float(price_str)
|
||||
|
||||
await self.station_status_model.save(
|
||||
session=session,
|
||||
@@ -193,32 +207,47 @@ class XinDianTuService:
|
||||
|
||||
# 2. 保存 Status (解析价格和电桩)
|
||||
status_id = self.generate_id()
|
||||
piles_str = station.get("piles", "0/0")
|
||||
|
||||
# 处理 piles 字段 (由 VL 返回的列表结构)
|
||||
piles_data = station.get("piles")
|
||||
total, free = 0, 0
|
||||
standardized_piles = []
|
||||
|
||||
if "/" in piles_str:
|
||||
if isinstance(piles_data, list):
|
||||
for idx, p in enumerate(piles_data):
|
||||
try:
|
||||
t = int(p.get("total", 0))
|
||||
f = int(p.get("free", 0))
|
||||
total += t
|
||||
free += f
|
||||
|
||||
standardized_piles.append({
|
||||
"pile_no": f"G{idx+1}",
|
||||
"type": p.get("type", "未知"),
|
||||
"power": "",
|
||||
"status_text": f"空闲{f}/总{t}",
|
||||
"remark": "列表页VL"
|
||||
})
|
||||
except:
|
||||
pass
|
||||
elif isinstance(piles_data, str) and "/" in piles_data:
|
||||
# 兼容旧格式或意外返回的字符串格式
|
||||
try:
|
||||
parts = piles_str.split("/")
|
||||
free = int(parts[0])
|
||||
total = int(parts[1])
|
||||
parts = piles_data.split("/")
|
||||
free = int(re.search(r"\d+", parts[0]).group())
|
||||
total = int(re.search(r"\d+", parts[1]).group())
|
||||
standardized_piles.append({
|
||||
"pile_no": "G1",
|
||||
"type": "未知",
|
||||
"power": "",
|
||||
"status_text": f"空闲{free}/总{total}",
|
||||
"remark": "列表页VL"
|
||||
"remark": "列表页VL(兼容)"
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
price_str = station.get("price", "")
|
||||
current_price = None
|
||||
try:
|
||||
price_match = re.search(r"\d+\.\d+", str(price_str))
|
||||
if price_match:
|
||||
current_price = float(price_match.group())
|
||||
except: pass
|
||||
current_price = self._to_float(price_str)
|
||||
|
||||
await self.station_status_model.save(
|
||||
session=session,
|
||||
@@ -299,20 +328,10 @@ class XinDianTuService:
|
||||
continue
|
||||
|
||||
price = station.get("price")
|
||||
current_price = None
|
||||
if price is not None and price != "":
|
||||
try:
|
||||
current_price = float(price)
|
||||
except (ValueError, TypeError):
|
||||
current_price = None
|
||||
current_price = self._to_float(price) if price is not None and price != "" else None
|
||||
|
||||
pro_price_val = station.get("pro_price")
|
||||
pro_price = None
|
||||
if pro_price_val is not None and pro_price_val != "":
|
||||
try:
|
||||
pro_price = float(pro_price_val)
|
||||
except (ValueError, TypeError):
|
||||
pro_price = None
|
||||
pro_price = self._to_float(pro_price_val) if pro_price_val is not None and pro_price_val != "" else None
|
||||
|
||||
await self.station_status_model.save(
|
||||
session=session,
|
||||
@@ -414,44 +433,64 @@ class XinDianTuService:
|
||||
# 2. 保存 Status
|
||||
status_id = self.generate_id()
|
||||
|
||||
# 解析电桩数量 (支持多组,如 "快:2/10 慢:1/5")
|
||||
piles_str = station_info.get("piles", "")
|
||||
# 解析电桩数量 (支持由 VL 返回的列表或字符串)
|
||||
piles_data = station_info.get("piles", [])
|
||||
total, free = 0, 0
|
||||
standardized_piles = []
|
||||
|
||||
# 尝试匹配 "快:2/10" 或 "2/10" 这种格式
|
||||
# 模式 1: 有类型,如 "快:2/10"
|
||||
matches_with_type = re.findall(r"(\w+):(\d+)/(\d+)", piles_str)
|
||||
if matches_with_type:
|
||||
for idx, (p_type, f, t) in enumerate(matches_with_type):
|
||||
f_int, t_int = int(f), int(t)
|
||||
free += f_int
|
||||
total += t_int
|
||||
standardized_piles.append({
|
||||
"pile_no": f"G{idx+1}",
|
||||
"type": p_type,
|
||||
"power": "",
|
||||
"status_text": f"空闲{f_int}/总{t_int}",
|
||||
"remark": "列表页VL"
|
||||
})
|
||||
else:
|
||||
# 模式 2: 无类型,仅 "2/10"
|
||||
matches_simple = re.findall(r"(\d+)/(\d+)", piles_str)
|
||||
for idx, (f, t) in enumerate(matches_simple):
|
||||
f_int, t_int = int(f), int(t)
|
||||
free += f_int
|
||||
total += t_int
|
||||
standardized_piles.append({
|
||||
"pile_no": f"G{idx+1}",
|
||||
"type": "未知",
|
||||
"power": "",
|
||||
"status_text": f"空闲{f_int}/总{t_int}",
|
||||
"remark": "列表页VL"
|
||||
})
|
||||
if isinstance(piles_data, list):
|
||||
# 模式 1: VL 返回的结构化列表
|
||||
for idx, p in enumerate(piles_data):
|
||||
if isinstance(p, dict):
|
||||
try:
|
||||
t = int(p.get("total", 0))
|
||||
f = int(p.get("free", 0))
|
||||
total += t
|
||||
free += f
|
||||
standardized_piles.append({
|
||||
"pile_no": f"G{idx+1}",
|
||||
"type": p.get("type", "未知"),
|
||||
"power": "",
|
||||
"status_text": f"空闲{f}/总{t}",
|
||||
"remark": "列表页VL"
|
||||
})
|
||||
except:
|
||||
pass
|
||||
elif isinstance(piles_data, str):
|
||||
# 模式 2: 字符串格式 (兼容)
|
||||
piles_str = piles_data
|
||||
# 尝试匹配 "快:2/10" 或 "2/10" 这种格式
|
||||
matches_with_type = re.findall(r"(\w+):(\d+)/(\d+)", piles_str)
|
||||
if matches_with_type:
|
||||
for idx, (p_type, f, t) in enumerate(matches_with_type):
|
||||
f_int, t_int = int(f), int(t)
|
||||
free += f_int
|
||||
total += t_int
|
||||
standardized_piles.append({
|
||||
"pile_no": f"G{idx+1}",
|
||||
"type": p_type,
|
||||
"power": "",
|
||||
"status_text": f"空闲{f_int}/总{t_int}",
|
||||
"remark": "列表页VL(兼容)"
|
||||
})
|
||||
else:
|
||||
# 模式 3: 无类型,仅 "2/10"
|
||||
matches_simple = re.findall(r"(\d+)/(\d+)", piles_str)
|
||||
for idx, (f, t) in enumerate(matches_simple):
|
||||
f_int, t_int = int(f), int(t)
|
||||
free += f_int
|
||||
total += t_int
|
||||
standardized_piles.append({
|
||||
"pile_no": f"G{idx+1}",
|
||||
"type": "未知",
|
||||
"power": "",
|
||||
"status_text": f"空闲{f_int}/总{t_int}",
|
||||
"remark": "列表页VL(兼容)"
|
||||
})
|
||||
|
||||
if not standardized_piles:
|
||||
if not standardized_piles and isinstance(piles_data, str):
|
||||
# 兜底匹配所有数字
|
||||
nums = re.findall(r"(\d+)", piles_str)
|
||||
nums = re.findall(r"(\d+)", piles_data)
|
||||
if len(nums) >= 2:
|
||||
free = int(nums[0])
|
||||
total = int(nums[1])
|
||||
@@ -463,17 +502,9 @@ class XinDianTuService:
|
||||
"remark": "列表页VL兜底"
|
||||
})
|
||||
|
||||
# 解析价格
|
||||
# 解析价格
|
||||
price_val = station_info.get("price")
|
||||
current_price = None
|
||||
if price_val:
|
||||
try:
|
||||
# 提取数字部分
|
||||
price_match = re.search(r"(\d+(\.\d+)?)", str(price_val))
|
||||
if price_match:
|
||||
current_price = float(price_match.group(1))
|
||||
except:
|
||||
pass
|
||||
current_price = self._to_float(price_val)
|
||||
|
||||
await self.station_status_model.save(
|
||||
session=session,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user