上游 82 网站返回的 date 数组是 UTC,直接入库会让这批数字命名的设备时间比 实际早 8 小时。 实证(本地库): 这批设备的 check_time 是 2026-02-06 17:11:10(那轮采集完成时刻), 而上游原值落在 09:07:02 ~ 09:10:30,+8h 后正好是 17:07 ~ 17:10 —— 即采集 完成前一分钟。若原值已是北京时间,则意味着这批站点在采集前 8 小时就集体 停止上报、且整齐划一,不合常理。 - 解析后 +timedelta(hours=8)。中国无夏令时,固定偏移与 pytz 转换等价。 - 清洗 T / Z / 毫秒后缀,兼容 ISO 格式。 - 解析失败原样兜底 —— 有值但格式不认识,原样透传给入库层的 staleness_of 再解析,与之前“拿不到时间必须为 None”的约定不冲突。 - 跨日正确:16:00 UTC 顺延至次日 00:00。 注意:本次只改后端源码,需重新打包 exe 才会在服务器生效。
86 lines
3.6 KiB
Python
86 lines
3.6 KiB
Python
# services/crawler_82.py
|
||
import requests
|
||
import json
|
||
import logging
|
||
from lxml import etree
|
||
from config import Config
|
||
from datetime import datetime, timedelta
|
||
|
||
CONFIG = Config.CRAWLER_CONFIG["82"]
|
||
|
||
|
||
def run_82_logic():
|
||
"""返回 result_list"""
|
||
results = []
|
||
print(">>> [82爬虫] 启动...")
|
||
session = requests.Session()
|
||
|
||
try:
|
||
session.post(f"{CONFIG['base_url']}/login.php", data=CONFIG["login"], timeout=10)
|
||
resp = session.post(f"{CONFIG['base_url']}/GetStationList.php", timeout=10)
|
||
html = etree.HTML(resp.content)
|
||
if html is None: return []
|
||
|
||
stations = html.xpath('//option/@value')
|
||
for sid in [s for s in stations if s]:
|
||
data_packet = {
|
||
'source': '82网站',
|
||
'name': str(sid),
|
||
'status': '正常',
|
||
'value': '',
|
||
# 同 106:默认留空,只有真正拿到数据时间才覆盖,
|
||
# 不用 datetime.now() 冒充数据时间
|
||
'target_time': None,
|
||
'raw_json': {},
|
||
'temp_file': None
|
||
}
|
||
try:
|
||
r = session.post(f"{CONFIG['base_url']}/getLastWeatherData.php", data=str(sid),
|
||
headers={'Content-Type': 'text/plain'}, timeout=10)
|
||
try:
|
||
data = r.json()
|
||
except:
|
||
data = None
|
||
|
||
if data:
|
||
d_list = data.get('date', [])
|
||
|
||
# 上游返回的是 UTC,需 +8 小时转北京时间。
|
||
# 实证:库里 check_time 为 2026-02-06 17:11:10(那轮采集完成时刻),
|
||
# 而同一批设备的上游原值落在 09:07~09:10,+8h 后正好是 17:07~17:10,
|
||
# 即采集完成前一分钟 —— 若原值已是北京时间,则意味着这批站点在采集
|
||
# 前 8 小时就集体停止上报,不合常理。
|
||
# 中国无夏令时,固定 +8h 与 pytz 转换等价。
|
||
# 拿不到时间时必须是 None,不能是 "N/A" —— 字符串在入库层是真值,
|
||
# 会把设备主表已经冻结的有效 latest_time 覆盖掉,令 offset 倒退成"从未同步"
|
||
latest = None
|
||
if d_list:
|
||
raw_time_str = str(d_list[-1])
|
||
try:
|
||
# 兼容可能的 T 或 Z 等 ISO 格式,先做一下基础清洗
|
||
clean_str = raw_time_str.replace('T', ' ').replace('Z', '').split('.')[0]
|
||
dt = datetime.strptime(clean_str, "%Y-%m-%d %H:%M:%S")
|
||
# 加上 8 小时转换为北京时间
|
||
dt = dt + timedelta(hours=8)
|
||
latest = dt.strftime("%Y-%m-%d %H:%M:%S")
|
||
except Exception:
|
||
# 如果解析失败,原样返回兜底
|
||
latest = raw_time_str
|
||
|
||
data_packet['target_time'] = latest
|
||
data_packet['value'] = f"Data Points: {len(d_list)}"
|
||
data_packet['raw_json'] = data # 🔥 存完整JSON
|
||
else:
|
||
data_packet['status'] = '异常'
|
||
data_packet['value'] = "返回空数据"
|
||
|
||
except Exception as e:
|
||
data_packet['status'] = '异常'
|
||
data_packet['value'] = "单个采集失败"
|
||
|
||
results.append(data_packet)
|
||
|
||
except Exception as e:
|
||
logging.error(f"82 Crawler Error: {e}")
|
||
|
||
return results |