From 1412b0ed0bd302d7e5793b594621a6695fb1154e Mon Sep 17 00:00:00 2001 From: DXC Date: Tue, 15 Sep 2026 17:05:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=88=AC=E8=99=AB=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E7=94=A8=E8=BF=90=E8=A1=8C=E6=97=B6=E5=88=BB=E4=BC=AA=E9=80=A0?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 决定(离线时冻结上一个有效时间)。 --- 2_1banben/services/crawler_106.py | 101 ++++++++++++++++++++++-------- 2_1banben/services/crawler_82.py | 8 ++- 2 files changed, 81 insertions(+), 28 deletions(-) diff --git a/2_1banben/services/crawler_106.py b/2_1banben/services/crawler_106.py index 451ed0e..8fb1428 100644 --- a/2_1banben/services/crawler_106.py +++ b/2_1banben/services/crawler_106.py @@ -1,7 +1,9 @@ # services/crawler_106.py import os +import re import requests import logging +import pytz from datetime import datetime from config import Config @@ -29,11 +31,18 @@ def get_106_dynamic_token(port): 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 - today = datetime.now() - scored_items = [] + + tz = pytz.timezone('Asia/Shanghai') + now = datetime.now(tz) + valid_items = [] for item in items: name_val = item.get('name', '') @@ -43,23 +52,30 @@ def find_closest_item(items, is_date_level=True): try: if is_date_level: - # 解析文件夹日期格式: YYYY_MM_DD - current_date = datetime.strptime(target_str, "%Y_%m_%d") + # 目录名只有日期没有时刻,按当天 00:00 参与比较。 + # 注意不能用 23:59:59:那样"今天"的目录会大于当前时刻而被 + # 当成未来剔除,导致永远取到昨天的数据。 + item_dt = tz.localize( + datetime.strptime(f"{target_str} 00:00:00", "%Y_%m_%d %H:%M:%S") + ) else: - # 解析文件修改时间 + # 文件修改时间:带 'Z' 表示 UTC;不带时区的按东八区处理, + # 否则会与 now 出现 naive/aware 比较异常而被静默丢弃。 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()) - scored_items.append((diff, item, target_str)) - except: + # 【关键修复】剔除未来时间,只保留已经发生过的 + if item_dt <= now: + valid_items.append(((now - item_dt).total_seconds(), item, target_str)) + except Exception: continue - if not scored_items: return None - # 按时间差排序,取最小的 - scored_items.sort(key=lambda x: x[0]) - return scored_items[0] + if not valid_items: return None + # 按时间差升序排序(取最近的过去) + valid_items.sort(key=lambda x: x[0]) + return valid_items[0] def run_106_logic(): @@ -89,15 +105,16 @@ def run_106_logic(): if not (is_tower_underscore or is_tower_i): continue # --- 构建基础数据包 --- - # 默认使用标准当前时间作为兜底,防止后续步骤失败时时间为空 - current_standard_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - + # target_time 默认 None:只有真正从文件路径解析出记录时间才会被覆盖。 + # 离线/异常/token 失败等分支一律留空,交给入库层"冻结"上一个有效时间, + # 绝不用 datetime.now() 冒充数据时间。 + # status 默认 '正常':成功路径不会回头重设它,改这里等于改所有健康设备的显示。 data_packet = { 'source': '106网站', 'name': name, 'status': '正常', 'value': '', - 'target_time': current_standard_time, + 'target_time': None, 'raw_json': {}, 'temp_file': None, 'num_files': 0 @@ -133,16 +150,13 @@ def run_106_logic(): continue # ============================================================================== - # ✅ [核心修复] 时间格式标准化 - # 原逻辑: data_packet['target_time'] = best_date[2] (得到 "2026_02_08") - # 新逻辑: 将 "2026_02_08" 转换为 "2026-02-08 HH:MM:SS" + # ⚠️ 这里刻意不再给 target_time 赋值。 + # 旧逻辑是 formatted_date_part + datetime.now() 的时分秒,写进库的 + # 实际是"爬虫运行时刻",导致 calculate_offset 恒为"当天"。 + # 真实时间要等确定最终文件后,从文件路径里解析(见下方 [核心修复])。 # ============================================================================== raw_folder_name = best_date[2] # 例如 "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}/" @@ -165,6 +179,41 @@ def run_106_logic(): file_item = best_file[1] 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. 下载/读取内容逻辑 --- if is_tower_i: # [二进制文件] 下载逻辑 diff --git a/2_1banben/services/crawler_82.py b/2_1banben/services/crawler_82.py index 8c9d1b9..de4e1ba 100644 --- a/2_1banben/services/crawler_82.py +++ b/2_1banben/services/crawler_82.py @@ -28,7 +28,9 @@ def run_82_logic(): 'name': str(sid), 'status': '正常', 'value': '', - 'target_time': datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + # 同 106:默认留空,只有真正拿到数据时间才覆盖, + # 不用 datetime.now() 冒充数据时间 + 'target_time': None, 'raw_json': {}, 'temp_file': None } @@ -42,7 +44,9 @@ def run_82_logic(): if data: 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['value'] = f"Data Points: {len(d_list)}" data_packet['raw_json'] = data # 🔥 存完整JSON