Files
ZDXX/2_1banben/services/crawler_106.py
DXC 1412b0ed0b 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 决定(离线时冻结上一个有效时间)。
2026-09-15 17:05:31 +08:00

256 lines
11 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.

# services/crawler_106.py
import os
import re
import requests
import logging
import pytz
from datetime import datetime
from config import Config
CONFIG = Config.CRAWLER_CONFIG["106"]
def get_temp_dir():
"""获取临时文件存储目录"""
base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
temp_dir = os.path.join(base_dir, 'instance', 'temp')
if not os.path.exists(temp_dir):
os.makedirs(temp_dir)
return temp_dir
def get_106_dynamic_token(port):
"""获取动态登录 Token"""
try:
login_url = f"http://106.75.72.40:{port}/api/login"
resp = requests.post(login_url, json=CONFIG["login_payload"], timeout=10)
return resp.text.strip().replace('"', '') if resp.status_code == 200 else None
except:
return None
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
tz = pytz.timezone('Asia/Shanghai')
now = datetime.now(tz)
valid_items = []
for item in items:
name_val = item.get('name', '')
path_val = item.get('path', '')
# 如果是日期层级,名字通常是 2026_02_08 这种格式
target_str = name_val if name_val else path_val.split('/')[-1]
try:
if is_date_level:
# 目录名只有日期没有时刻,按当天 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', '')
item_dt = datetime.fromisoformat(mod_str.replace('Z', '+00:00'))
if item_dt.tzinfo is None:
item_dt = tz.localize(item_dt)
# 【关键修复】剔除未来时间,只保留已经发生过的
if item_dt <= now:
valid_items.append(((now - item_dt).total_seconds(), item, target_str))
except Exception:
continue
if not valid_items: return None
# 按时间差升序排序(取最近的过去)
valid_items.sort(key=lambda x: x[0])
return valid_items[0]
def run_106_logic():
"""
106 爬虫主逻辑
返回 result_list, 每个元素是一个字典
"""
results = []
print(">>> [106爬虫] 启动...")
main_headers = {"Authorization": CONFIG["primary_auth"], "User-Agent": "Mozilla/5.0"}
try:
# 0. 获取代理列表 (设备列表)
resp = requests.get(CONFIG["base_url"], headers=main_headers, timeout=20)
proxies = resp.json().get('proxies', [])
for item in proxies:
name = item.get('name', '')
# 过滤规则:必须以 _data 结尾
if not name.lower().endswith('_data'): continue
name_upper = name.upper()
is_tower_underscore = "TOWER_" in name_upper
is_tower_i = "TOWER" in name_upper and not is_tower_underscore
# 过滤规则:必须包含 TOWER 相关标识
if not (is_tower_underscore or is_tower_i): continue
# --- 构建基础数据包 ---
# target_time 默认 None:只有真正从文件路径解析出记录时间才会被覆盖。
# 离线/异常/token 失败等分支一律留空,交给入库层"冻结"上一个有效时间,
# 绝不用 datetime.now() 冒充数据时间。
# status 默认 '正常':成功路径不会回头重设它,改这里等于改所有健康设备的显示。
data_packet = {
'source': '106网站',
'name': name,
'status': '正常',
'value': '',
'target_time': None,
'raw_json': {},
'temp_file': None,
'num_files': 0
}
# 检查在线状态
if str(item.get('status')).lower() != 'online':
data_packet['status'] = '离线'
data_packet['value'] = f"状态: {item.get('status')}"
results.append(data_packet)
continue
try:
# 获取端口和 Token
port = item.get('conf', {}).get('remote_port')
token = get_106_dynamic_token(port)
if not token:
data_packet['status'] = '异常'
data_packet['value'] = "Token获取失败"
results.append(data_packet)
continue
headers = {"Authorization": CONFIG["primary_auth"], "x-auth": token}
api_root = "/api/resources/Data/" if is_tower_underscore else "/api/resources/data/"
# --- 1. 获取日期文件夹列表 ---
res1 = requests.get(f"http://106.75.72.40:{port}{api_root}", headers=headers, timeout=10)
best_date = find_closest_item(res1.json().get('items', []), True)
if not best_date:
data_packet['value'] = "未找到任何日期文件夹"
results.append(data_packet)
continue
# ==============================================================================
# ⚠️ 这里刻意不再给 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"
date_path = f"{api_root}{raw_folder_name}/"
# --- 2. 请求具体日期的文件夹内容 (获取 numFiles) ---
res2 = requests.get(f"http://106.75.72.40:{port}{date_path}", headers=headers, timeout=10)
folder_data = res2.json()
file_count = folder_data.get('numFiles', 0)
data_packet['num_files'] = file_count
print(f" -> {name}: 找到日期 {formatted_date_part}, 文件数: {file_count}")
# --- 3. 找该文件夹里最新的文件 ---
best_file = find_closest_item(folder_data.get('items', []), False)
if not best_file:
data_packet['value'] = "文件夹为空"
results.append(data_packet)
continue
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:
# [二进制文件] 下载逻辑
download_url = f"http://106.75.72.40:{port}/api/raw{full_path}"
res3 = requests.get(download_url, headers=headers, timeout=20, stream=True)
if res3.status_code == 200:
safe_name = f"{name}_{datetime.now().strftime('%H%M%S')}.db"
temp_path = os.path.join(get_temp_dir(), safe_name)
with open(temp_path, 'wb') as f:
f.write(res3.content)
data_packet['temp_file'] = temp_path
data_packet['value'] = f"Binary Downloaded: {len(res3.content)} bytes"
data_packet['raw_json'] = file_item # 借用 file_item 充当 raw_json
else:
data_packet['status'] = '异常'
data_packet['value'] = f"下载失败: {res3.status_code}"
else:
# [文本文件] JSON 解析逻辑
file_api_url = f"http://106.75.72.40:{port}/api/resources{full_path}"
res3 = requests.get(file_api_url, headers=headers, timeout=20)
try:
json_content = res3.json()
data_packet['raw_json'] = json_content
# 尝试提取 content 内容,如果没有则截取部分 JSON 字符串
data_packet['value'] = json_content.get('content', str(json_content)[:100])
except:
data_packet['value'] = "JSON解析失败"
results.append(data_packet)
except Exception as e:
data_packet['status'] = '异常'
data_packet['value'] = str(e)[:100]
results.append(data_packet)
except Exception as e:
logging.error(f"106 Crawler Error: {e}")
return results