scada_collation.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import logging
  2. import os
  3. import shutil
  4. import time
  5. from datetime import datetime
  6. from multiprocessing import Pool # 新增
  7. from pathlib import Path
  8. import pandas as pd
  9. from labeler import apply_labels
  10. FILE_MODIFY_MIN_SECONDS = 60
  11. # ROLLING_DAYS_30 = 30
  12. # ROLLING_DAYS_90 = 90
  13. # ====================== 配置项 ======================
  14. # base_dir = "/data/wind-turbine"
  15. base_dir = "/data/wind_files/headquarter/scada"
  16. BASE_TMP_DIR = Path(f"{base_dir}/tmp")
  17. BASE_LABEL_DIR = Path(f"{base_dir}/label")
  18. # BASE_ROLLING_DAYS_DIR = Path(f"{base_dir}/{ROLLING_DAYS}days")
  19. ERROR_DATA_DIR = Path(f"{base_dir}/error_data")
  20. LOG_DIR = Path("/data/logs/scada_data_py")
  21. LOG_FILE = LOG_DIR / f"process_{datetime.now().strftime('%Y%m%d')}.log"
  22. LOG_LEVEL = logging.INFO
  23. # 多进程配置(根据服务器CPU核心数调整)
  24. MAX_WORKERS = 8
  25. # ====================== 算法 ======================
  26. def wind_data_labeling(df: pd.DataFrame, model_name: str = '') -> pd.DataFrame:
  27. df = apply_labels(df, model_name)
  28. return df
  29. # ====================== 日志 ======================
  30. def init_logger():
  31. LOG_DIR.mkdir(parents=True, exist_ok=True)
  32. logging.basicConfig(
  33. level=LOG_LEVEL,
  34. format="%(asctime)s - %(levelname)s - %(message)s",
  35. handlers=[
  36. logging.FileHandler(LOG_FILE, encoding="utf-8"),
  37. logging.StreamHandler()
  38. ]
  39. )
  40. return logging.getLogger(__name__)
  41. logger = init_logger()
  42. # ====================== 工具方法 ======================
  43. def is_file_valid(file_path: Path) -> bool:
  44. try:
  45. pd.read_parquet(file_path)
  46. return True
  47. except Exception as e:
  48. logger.error(f"文件损坏,跳过:{file_path},异常:{str(e)}")
  49. return False
  50. def get_target_files(base_dir: Path) -> list:
  51. target_files = []
  52. now = time.time()
  53. for root, _, files in os.walk(base_dir):
  54. for file in files:
  55. if not file.endswith(".parquet"):
  56. continue
  57. file_path = Path(root) / file
  58. if now - file_path.stat().st_mtime <= FILE_MODIFY_MIN_SECONDS:
  59. continue
  60. target_files.append(file_path)
  61. return target_files
  62. from functools import wraps
  63. def retry_on_exception(max_retries=3, delay=1, backoff=2):
  64. """重试装饰器"""
  65. def decorator(func):
  66. @wraps(func)
  67. def wrapper(*args, **kwargs):
  68. retries = 0
  69. current_delay = delay
  70. while retries < max_retries:
  71. try:
  72. return func(*args, **kwargs)
  73. except Exception as e:
  74. retries += 1
  75. if retries == max_retries:
  76. raise
  77. logger.warning(f"操作失败,{retries}/{max_retries} 次重试,错误: {e}")
  78. time.sleep(current_delay)
  79. current_delay *= backoff
  80. return None
  81. return wrapper
  82. return decorator
  83. @retry_on_exception(max_retries=3, delay=0.5)
  84. def safe_read_parquet(file_path):
  85. """带重试机制的 Parquet 读取"""
  86. return pd.read_parquet(file_path)
  87. def merge_rolling_days_data(model: str, wind_farm_code: str, wind_turbine_code: str,
  88. df_oneday_label: pd.DataFrame, ROLLING_DAYS: int) -> None:
  89. BASE_ROLLING_DAYS_DIR = Path(f"{base_dir}/{ROLLING_DAYS}days")
  90. output_path = BASE_ROLLING_DAYS_DIR / model / wind_farm_code / f"{wind_turbine_code}.parquet"
  91. output_path.parent.mkdir(parents=True, exist_ok=True)
  92. df_list = []
  93. if output_path.exists():
  94. df_list.append(safe_read_parquet(output_path))
  95. df_list.append(df_oneday_label)
  96. merged_df = pd.concat(df_list, ignore_index=True)
  97. merged_df['localtime'] = pd.to_datetime(merged_df['localtime'], errors='coerce')
  98. merged_df.sort_values(by=['localtime'], inplace=True)
  99. # 按自然日 00:00 保留最近30天
  100. latest_time = merged_df['localtime'].max()
  101. latest_day = latest_time.floor('D')
  102. thirty_days_ago = latest_day - pd.Timedelta(days=ROLLING_DAYS - 1)
  103. merged_df = merged_df[merged_df['localtime'] > thirty_days_ago]
  104. merged_df.to_parquet(output_path, engine="pyarrow", index=False)
  105. logger.info(f"{ROLLING_DAYS}天滚动文件生成成功:{output_path}")
  106. # ====================== 单个文件处理逻辑(抽成独立函数)======================
  107. def process_single_file(tmp_file):
  108. # 20251020/CCWE1500-82.DF/EmlMGcty/zKCujSuK.parquet
  109. parts = tmp_file.parts
  110. data_date = parts[-4]
  111. model = parts[-3]
  112. wind_farm_code = parts[-2]
  113. wind_turbine_code = tmp_file.stem
  114. try:
  115. if not is_file_valid(tmp_file):
  116. target_path = ERROR_DATA_DIR / data_date / model / wind_farm_code / f"{wind_turbine_code}.parquet"
  117. target_path.parent.mkdir(parents=True, exist_ok=True)
  118. shutil.move(tmp_file, target_path)
  119. return "fail"
  120. # 读取 + 打标签
  121. df = safe_read_parquet(tmp_file)
  122. for col in df.columns:
  123. if col != "localtime":
  124. df[col] = pd.to_numeric(df[col], errors="coerce")
  125. # 补全数据,查询前一天最后一条数据,向后补全
  126. df_oneday_label = wind_data_labeling(df, model)
  127. # 保存label
  128. label_file = BASE_LABEL_DIR / data_date / model / wind_farm_code / f"{wind_turbine_code}.parquet"
  129. label_file.parent.mkdir(parents=True, exist_ok=True)
  130. df_oneday_label.to_parquet(label_file, engine="pyarrow", index=False)
  131. logger.info(f"{wind_turbine_code} 打标签完成:{label_file}")
  132. # 30天数据合并
  133. merge_rolling_days_data(model, wind_farm_code, wind_turbine_code, df_oneday_label, 30)
  134. # 90天数据合并
  135. merge_rolling_days_data(model, wind_farm_code, wind_turbine_code, df_oneday_label, 90)
  136. # 删除临时文件
  137. tmp_file.unlink()
  138. logger.info(f"临时文件已删除:{tmp_file}")
  139. return "success"
  140. except Exception as e:
  141. logger.error(f"处理失败:{tmp_file},异常:{str(e)}", exc_info=True)
  142. target_path = ERROR_DATA_DIR / data_date / model / wind_farm_code / f"{wind_turbine_code}.parquet"
  143. target_path.parent.mkdir(parents=True, exist_ok=True)
  144. shutil.move(tmp_file, target_path)
  145. return "fail"
  146. # ====================== 主流程(多进程)======================
  147. def process_wind_data():
  148. logger.info(f"临时目录:{BASE_TMP_DIR}")
  149. tmp_files = get_target_files(BASE_TMP_DIR)
  150. if not tmp_files:
  151. logger.info("无待处理文件,任务结束")
  152. return
  153. logger.info(f"待处理文件总数:{len(tmp_files)}")
  154. logger.info(f"启动多进程,进程数:{MAX_WORKERS}")
  155. # ========== 多进程核心 ==========
  156. with Pool(processes=MAX_WORKERS) as pool:
  157. results = pool.map(process_single_file, tmp_files)
  158. success_count = sum(1 for res in results if res == "success")
  159. fail_count = sum(1 for res in results if res == "fail")
  160. logger.info(f"任务完成 | 成功:{success_count} | 失败:{fail_count}")
  161. logger.info("=" * 50)
  162. if __name__ == "__main__":
  163. process_wind_data()