实现数据接入层功能(REST API + WebSocket + 批量导入)

- 实现REST API服务器,支持IoT数据、手动录入和批量导入接口
- 实现WebSocket服务器,支持实时数据推送和连接管理
- 实现批量导入模块,支持CSV、Excel、JSON多种格式
- 实现数据处理工具,包含字段映射和单位转换功能
- 实现数据模型定义和数据验证机制
- 创建主程序入口和配置文件
- 添加详细的使用文档和API说明
This commit is contained in:
2026-06-15 11:59:00 +08:00
commit 5eae031679
8 changed files with 2071 additions and 0 deletions
+409
View File
@@ -0,0 +1,409 @@
"""
数据处理工具模块
提供数据验证、转换、格式化等工具函数
"""
import json
import csv
import pandas as pd
from typing import Dict, List, Any, Optional, Union
from datetime import datetime, timedelta
import hashlib
import logging
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class DataConverter:
"""数据转换器"""
# 水利行业标准字段映射
FIELD_MAPPING = {
"流量": "LL",
"压力": "YL",
"水位": "SW",
"浊度": "ZD",
"pH值": "PH",
"温度": "WD",
"电导率": "DD",
"硬度": "YD",
# 支持常见的中文字段名
"流量计": "LL",
"压力表": "YL",
"水位计": "SW",
"浊度仪": "ZD",
"pH计": "PH",
"温度计": "WD",
"电导率仪": "DD",
"硬度计": "YD"
}
# 单位转换
UNIT_CONVERSIONS = {
# 流量单位转换 (m³/h)
"m³/h": 1.0,
"L/s": 3.6, # L/s = m³/h / 1000 * 3600
"m³/d": 1/24, # m³/d = m³/h / 24
"L/min": 1/60, # L/min = m³/h / 1000 * 60
# 压力单位转换 (MPa)
"MPa": 1.0,
"kPa": 0.001, # kPa = MPa / 1000
"bar": 0.1, # bar = MPa * 10
"kgf/cm²": 0.0980665, # kgf/cm² = MPa / 0.0980665
# 水位单位转换 (m)
"m": 1.0,
"cm": 0.01, # cm = m / 100
"mm": 0.001, # mm = m / 1000
# 浊度单位转换 (NTU)
"NTU": 1.0,
"FNU": 1.0, # FNU ≈ NTU
# pH值单位转换
"pH": 1.0,
# 温度单位转换 (°C)
"°C": 1.0,
"K": 1.0, # 相对差值
"°F": lambda x: (x - 32) / 1.8, # °F to °C
# 电导率单位转换 (μS/cm)
"μS/cm": 1.0,
"mS/cm": 1000, # mS/cm = μS/cm * 1000
"S/m": 10000 # S/m = μS/cm * 100
}
@classmethod
def normalize_field_name(cls, field_name: str) -> str:
"""标准化字段名"""
if not field_name:
return ""
field_name = field_name.strip().upper()
# 如果已经是标准格式,直接返回
if field_name in cls.FIELD_MAPPING.values():
return field_name
# 查映射表
if field_name in cls.FIELD_MAPPING:
return cls.FIELD_MAPPING[field_name]
# 英文映射
english_mapping = {
"flow": "LL",
"pressure": "YL",
"level": "SW",
"turbidity": "ZD",
"ph": "PH",
"temperature": "WD",
"conductivity": "DD",
"hardness": "YD"
}
if field_name.lower() in english_mapping:
return english_mapping[field_name.lower()]
return field_name
@classmethod
def convert_unit(cls, value: float, from_unit: str, to_unit: str) -> float:
"""单位转换"""
if from_unit == to_unit:
return value
if from_unit not in cls.UNIT_CONVERSIONS:
raise ValueError(f"不支持的单位: {from_unit}")
if to_unit not in cls.UNIT_CONVERSIONS:
raise ValueError(f"不支持的目标单位: {to_unit}")
from_conv = cls.UNIT_CONVERSIONS[from_unit]
to_conv = cls.UNIT_CONVERSIONS[to_unit]
if callable(from_conv):
value = from_conv(value)
if callable(to_conv):
return value / to_conv
else:
return value * (to_conv / from_conv)
@classmethod
def validate_sensor_data(cls, data: Dict[str, Any]) -> Dict[str, Any]:
"""验证传感器数据"""
errors = []
validated_data = {}
# 必需字段验证
required_fields = ["device_id", "data_type", "value"]
for field in required_fields:
if field not in data:
errors.append(f"缺少必需字段: {field}")
else:
validated_data[field] = data[field]
# 数据类型验证和标准化
if "data_type" in validated_data:
original_type = validated_data["data_type"]
validated_data["data_type"] = cls.normalize_field_name(original_type)
if validated_data["data_type"] != original_type:
logger.info(f"字段名标准化: {original_type} -> {validated_data['data_type']}")
# 数值验证
if "value" in validated_data:
try:
validated_data["value"] = float(validated_data["value"])
# 检查数值范围
data_type = validated_data.get("data_type", "")
if data_type == "LL" and validated_data["value"] < 0:
errors.append("流量不能为负数")
elif data_type == "YL" and validated_data["value"] < 0:
errors.append("压力不能为负数")
elif data_type == "SW" and validated_data["value"] < 0:
errors.append("水位不能为负数")
except (ValueError, TypeError):
errors.append(f"无效的数值: {validated_data['value']}")
# 地点验证
if "location" not in validated_data or not validated_data["location"]:
validated_data["location"] = "未知"
# 时间戳处理
if "timestamp" in data:
try:
if isinstance(data["timestamp"], str):
validated_data["timestamp"] = datetime.fromisoformat(data["timestamp"])
else:
validated_data["timestamp"] = data["timestamp"]
except (ValueError, TypeError):
validated_data["timestamp"] = datetime.now()
else:
validated_data["timestamp"] = datetime.now()
return {
"valid": len(errors) == 0,
"data": validated_data,
"errors": errors
}
class DataFormatter:
"""数据格式化器"""
@staticmethod
def format_sensor_data(data: Dict[str, Any], format_type: str = "json") -> str:
"""格式化传感器数据"""
if format_type == "json":
return json.dumps(data, ensure_ascii=False, indent=2)
elif format_type == "csv":
# CSV格式只包含关键字段
csv_fields = ["device_id", "data_type", "value", "location", "timestamp"]
csv_data = {k: data.get(k, "") for k in csv_fields}
import io
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=csv_fields)
writer.writeheader()
writer.writerow(csv_data)
return output.getvalue()
else:
raise ValueError(f"不支持的格式类型: {format_type}")
@staticmethod
def format_statistics(stats: Dict[str, Any], format_type: str = "text") -> str:
"""格式化统计数据"""
if format_type == "json":
return json.dumps(stats, ensure_ascii=False, indent=2)
elif format_type == "text":
lines = ["数据统计报告", "=" * 20]
lines.append(f"总记录数: {stats.get('total_records', 0)}")
if "by_type" in stats:
lines.append("\n按数据类型统计:")
for data_type, count in stats["by_type"].items():
lines.append(f" {data_type}: {count} 条")
if "by_device" in stats:
lines.append("\n按设备统计:")
for device_id, count in list(stats["by_device"].items())[:10]: # 只显示前10个
lines.append(f" {device_id}: {count} 条")
return "\n".join(lines)
else:
raise ValueError(f"不支持的格式类型: {format_type}")
class DataHasher:
"""数据哈希工具"""
@staticmethod
def calculate_data_hash(data: Dict[str, Any]) -> str:
"""计算数据哈希值"""
# 将数据转换为字符串
data_str = json.dumps(data, sort_keys=True, ensure_ascii=False)
# 计算MD5哈希
hash_md5 = hashlib.md5(data_str.encode())
return hash_md5.hexdigest()
@staticmethod
def generate_data_id(device_id: str, data_type: str, timestamp: datetime) -> str:
"""生成数据ID"""
# 使用设备ID、数据类型和时间戳生成唯一ID
time_str = timestamp.strftime("%Y%m%d_%H%M%S")
hash_input = f"{device_id}_{data_type}_{time_str}"
hash_md5 = hashlib.md5(hash_input.encode())
return f"{data_type}_{device_id}_{hash_md5.hexdigest()[:8]}"
class DataQualityChecker:
"""数据质量检查器"""
@staticmethod
def check_data_quality(records: List[Dict[str, Any]]) -> Dict[str, Any]:
"""检查数据质量"""
quality_report = {
"total_records": len(records),
"valid_records": 0,
"invalid_records": 0,
"quality_score": 0,
"issues": [],
"statistics": {}
}
if not records:
quality_report["quality_score"] = 0
return quality_report
valid_records = []
for record in records:
issues = []
# 检查必需字段
required_fields = ["device_id", "data_type", "value"]
for field in required_fields:
if field not in record or not record[field]:
issues.append(f"缺少必需字段: {field}")
# 检查数据类型
if "data_type" in record and record["data_type"]:
valid_types = ["LL", "YL", "SW", "ZD", "PH", "WD", "DD", "YD"]
if record["data_type"] not in valid_types:
issues.append(f"无效的数据类型: {record['data_type']}")
# 检查数值范围
if "value" in record and record["value"]:
try:
value = float(record["value"])
data_type = record.get("data_type", "")
if data_type == "LL" and value < 0:
issues.append("流量不能为负数")
elif data_type == "YL" and value < 0:
issues.append("压力不能为负数")
elif data_type == "SW" and value < 0:
issues.append("水位不能为负数")
# 检查异常值
if data_type == "LL" and value > 10000:
issues.append("流量值异常大")
elif data_type == "YL" and value > 10:
issues.append("压力值异常大")
except (ValueError, TypeError):
issues.append("无效的数值格式")
if not issues:
valid_records.append(record)
quality_report["valid_records"] += 1
else:
quality_report["invalid_records"] += 1
quality_report["issues"].extend(issues)
# 计算质量分数
quality_report["quality_score"] = quality_report["valid_records"] / len(records)
# 统计信息
if records:
quality_report["statistics"] = {
"completeness": quality_report["valid_records"] / len(records),
"uniqueness": len(set(r.get("device_id", "") for r in valid_records)) / len(valid_records) if valid_records else 0,
"timeliness": quality_report.calculate_timeliness(records)
}
return quality_report
@staticmethod
def calculate_timeliness(records: List[Dict[str, Any]]) -> float:
"""计算数据及时性(24小时内的数据比例)"""
if not records:
return 0
now = datetime.now()
recent_count = 0
for record in records:
timestamp = record.get("timestamp")
if timestamp:
try:
if isinstance(timestamp, str):
timestamp = datetime.fromisoformat(timestamp)
time_diff = now - timestamp
if time_diff <= timedelta(hours=24):
recent_count += 1
except:
pass
return recent_count / len(records)
class DataExporter:
"""数据导出工具"""
@staticmethod
def export_to_csv(records: List[Dict[str, Any]], file_path: str) -> bool:
"""导出为CSV文件"""
try:
if not records:
return False
# 获取所有字段
all_fields = set()
for record in records:
all_fields.update(record.keys())
# 排序字段
field_order = ["device_id", "data_type", "value", "location", "timestamp"]
for field in all_fields:
if field not in field_order:
field_order.append(field)
with open(file_path, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=field_order)
writer.writeheader()
writer.writerows(records)
return True
except Exception as e:
logger.error(f"导出CSV失败: {str(e)}")
return False
@staticmethod
def export_to_json(records: List[Dict[str, Any]], file_path: str) -> bool:
"""导出为JSON文件"""
try:
with open(file_path, 'w', encoding='utf-8') as jsonfile:
json.dump(records, jsonfile, ensure_ascii=False, indent=2, default=str)
return True
except Exception as e:
logger.error(f"导出JSON失败: {str(e)}")
return False
# 全局工具实例
data_converter = DataConverter()
data_formatter = DataFormatter()
data_hasher = DataHasher()
quality_checker = DataQualityChecker()
data_exporter = DataExporter()