fix: 爬虫不再用运行时刻伪造数据时间

crawler_106:
- find_closest_item 原来用 abs(now - item) 取“离现在最近”,服务端只要存在
  未来日期的目录就会被选中。改为只保留不晚于当前时间的项,再取最新那个。
- 目录名只有日期,按当天 00:00 参与比较。不能用 23:59:59,否则“今天”的目录
  会大于当前时刻而被当成未来剔除,永远只取到昨天的数据。
- 文件 modified 带 Z 表示 UTC,不带时区的按东八区补上。原来 .replace(tzinfo=None)
  会让 naive/aware 比较抛异常,被 except 静默吞掉,导致所有文件被丢弃。
- target_time 不再用「目录日期 + datetime.now()」拼接(那写进库的是爬虫跑批
  时刻,而 calculate_offset 只取日期部分,等于在骗前端)。改为从最终文件路径
  正则提取真实记录时间,正则没命中时退化到文件修改时间。
- 正则放宽:兼容 /Data 与 /data、前导斜杠、扩展名大小写。

crawler_82:
- 基础数据包的 target_time 默认值不再用 datetime.now()。
- d_list 为空时原为字符串 "N/A",它是真值,会通过入库层的 if target_date
  判断,把设备主表已冻结的有效时间覆盖掉。改为 None。

爬虫层只负责解析真实记录时间,解析不出来就留 None;如何入库交给
services/db_ingest 决定(离线时冻结上一个有效时间)。
This commit is contained in:
DXC
2026-09-15 17:05:31 +08:00
parent c745f0c070
commit 1412b0ed0b
2 changed files with 81 additions and 28 deletions

View File

@ -1,7 +1,9 @@
# services/crawler_106.py # services/crawler_106.py
import os import os
import re
import requests import requests
import logging import logging
import pytz
from datetime import datetime from datetime import datetime
from config import Config from config import Config
@ -29,11 +31,18 @@ def get_106_dynamic_token(port):
def find_closest_item(items, is_date_level=True): def find_closest_item(items, is_date_level=True):
""" """
在列表中找到与当前日期最接近的文件夹或文件 在列表中找到【不晚于当前时间】的最新的文件夹或文件
旧实现用 abs(now - item) 取"离现在最近",服务端只要存在未来日期的
目录就会被选中;这里改成只保留已经发生过的,再取其中最新的一个。
返回 (diff, item, target_str);没有合法项时返回 None。
""" """
if not items or not isinstance(items, list): return None if not items or not isinstance(items, list): return None
today = datetime.now()
scored_items = [] tz = pytz.timezone('Asia/Shanghai')
now = datetime.now(tz)
valid_items = []
for item in items: for item in items:
name_val = item.get('name', '') name_val = item.get('name', '')
@ -43,23 +52,30 @@ def find_closest_item(items, is_date_level=True):
try: try:
if is_date_level: if is_date_level:
# 解析文件夹日期格式: YYYY_MM_DD # 目录名只有日期没有时刻,按当天 00:00 参与比较。
current_date = datetime.strptime(target_str, "%Y_%m_%d") # 注意不能用 23:59:59那样"今天"的目录会大于当前时刻而被
# 当成未来剔除,导致永远取到昨天的数据。
item_dt = tz.localize(
datetime.strptime(f"{target_str} 00:00:00", "%Y_%m_%d %H:%M:%S")
)
else: else:
# 解析文件修改时间 # 文件修改时间:带 'Z' 表示 UTC不带时区的按东八区处理
# 否则会与 now 出现 naive/aware 比较异常而被静默丢弃。
mod_str = item.get('modified', '') mod_str = item.get('modified', '')
current_date = datetime.fromisoformat(mod_str.replace('Z', '+00:00')) item_dt = datetime.fromisoformat(mod_str.replace('Z', '+00:00'))
if item_dt.tzinfo is None:
item_dt = tz.localize(item_dt)
# 计算与当前时间的差距 # 【关键修复】剔除未来时间,只保留已经发生过的
diff = abs((today - current_date.replace(tzinfo=None)).total_seconds()) if item_dt <= now:
scored_items.append((diff, item, target_str)) valid_items.append(((now - item_dt).total_seconds(), item, target_str))
except: except Exception:
continue continue
if not scored_items: return None if not valid_items: return None
# 按时间差排序取最小的 # 按时间差升序排序取最近的过去)
scored_items.sort(key=lambda x: x[0]) valid_items.sort(key=lambda x: x[0])
return scored_items[0] return valid_items[0]
def run_106_logic(): def run_106_logic():
@ -89,15 +105,16 @@ def run_106_logic():
if not (is_tower_underscore or is_tower_i): continue if not (is_tower_underscore or is_tower_i): continue
# --- 构建基础数据包 --- # --- 构建基础数据包 ---
# 默认使用标准当前时间作为兜底,防止后续步骤失败时时间为空 # target_time 默认 None只有真正从文件路径解析出记录时间才会被覆盖。
current_standard_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 离线/异常/token 失败等分支一律留空,交给入库层"冻结"上一个有效时间,
# 绝不用 datetime.now() 冒充数据时间。
# status 默认 '正常':成功路径不会回头重设它,改这里等于改所有健康设备的显示。
data_packet = { data_packet = {
'source': '106网站', 'source': '106网站',
'name': name, 'name': name,
'status': '正常', 'status': '正常',
'value': '', 'value': '',
'target_time': current_standard_time, 'target_time': None,
'raw_json': {}, 'raw_json': {},
'temp_file': None, 'temp_file': None,
'num_files': 0 'num_files': 0
@ -133,16 +150,13 @@ def run_106_logic():
continue continue
# ============================================================================== # ==============================================================================
# ✅ [核心修复] 时间格式标准化 # ⚠️ 这里刻意不再给 target_time 赋值。
# 逻辑: data_packet['target_time'] = best_date[2] (得到 "2026_02_08") # 逻辑是 formatted_date_part + datetime.now() 的时分秒,写进库的
# 新逻辑: 将 "2026_02_08" 转换为 "2026-02-08 HH:MM:SS" # 实际是"爬虫运行时刻",导致 calculate_offset 恒为"当天"。
# 真实时间要等确定最终文件后,从文件路径里解析(见下方 [核心修复])。
# ============================================================================== # ==============================================================================
raw_folder_name = best_date[2] # 例如 "2026_02_08" raw_folder_name = best_date[2] # 例如 "2026_02_08"
formatted_date_part = raw_folder_name.replace('_', '-') # 变成 "2026-02-08" formatted_date_part = raw_folder_name.replace('_', '-') # 变成 "2026-02-08"
current_time_part = datetime.now().strftime("%H:%M:%S")
# 覆盖默认时间,确保数据库存入的是标准时间戳格式
data_packet['target_time'] = f"{formatted_date_part} {current_time_part}"
date_path = f"{api_root}{raw_folder_name}/" date_path = f"{api_root}{raw_folder_name}/"
@ -165,6 +179,41 @@ def run_106_logic():
file_item = best_file[1] file_item = best_file[1]
full_path = file_item.get('path') or f"{date_path}{file_item.get('name')}" full_path = file_item.get('path') or f"{date_path}{file_item.get('name')}"
# ==============================================================================
# ✅ [核心修复] 从最终确定的文件路径里正则提取真正的记录时间
# 原逻辑在 routes/api.py 的 run_monitor 里,只有手动触发才生效;
# 下沉到爬虫层后,自动/手动两条路径拿到的是同一个真实时间。
# ==============================================================================
real_time = None
# 放宽点路径分隔符、data 大小写TOWER-I 走小写分支)、
# 是否以 / 开头、扩展名大小写都做兼容
match = re.search(
r'(?:^|[/\\])data/(\d{4}_\d{2}_\d{2})/[\w\-.]+_(\d{2}_\d{2}_\d{2})\.csv',
full_path,
re.IGNORECASE
)
if match:
date_part = match.group(1).replace('_', '-')
time_part = match.group(2).replace('_', ':')
real_time = f"{date_part} {time_part}"
# 正则没命中(例如二进制 .db 文件)时退化到文件修改时间,
# 绝不拿系统当前时间去伪造记录时间
if not real_time:
mod_str = file_item.get('modified', '')
if mod_str:
try:
mod_dt = datetime.fromisoformat(mod_str.replace('Z', '+00:00'))
if mod_dt.tzinfo is None:
mod_dt = pytz.timezone('Asia/Shanghai').localize(mod_dt)
else:
mod_dt = mod_dt.astimezone(pytz.timezone('Asia/Shanghai'))
real_time = mod_dt.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
pass
data_packet['target_time'] = real_time
# --- 4. 下载/读取内容逻辑 --- # --- 4. 下载/读取内容逻辑 ---
if is_tower_i: if is_tower_i:
# [二进制文件] 下载逻辑 # [二进制文件] 下载逻辑

View File

@ -28,7 +28,9 @@ def run_82_logic():
'name': str(sid), 'name': str(sid),
'status': '正常', 'status': '正常',
'value': '', 'value': '',
'target_time': datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 同 106默认留空只有真正拿到数据时间才覆盖
# 不用 datetime.now() 冒充数据时间
'target_time': None,
'raw_json': {}, 'raw_json': {},
'temp_file': None 'temp_file': None
} }
@ -42,7 +44,9 @@ def run_82_logic():
if data: if data:
d_list = data.get('date', []) d_list = data.get('date', [])
latest = str(d_list[-1]) if d_list else "N/A" # 拿不到时间时必须是 None不能是 "N/A" —— 字符串在入库层是真值,
# 会把设备主表已经冻结的有效 latest_time 覆盖掉,令 offset 倒退成"从未同步"
latest = str(d_list[-1]) if d_list else None
data_packet['target_time'] = latest data_packet['target_time'] = latest
data_packet['value'] = f"Data Points: {len(d_list)}" data_packet['value'] = f"Data Points: {len(d_list)}"
data_packet['raw_json'] = data # 🔥 存完整JSON data_packet['raw_json'] = data # 🔥 存完整JSON