| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- import logging
- import os
- import shutil
- import time
- from datetime import datetime
- from multiprocessing import Pool # 新增
- from pathlib import Path
- import pandas as pd
- from labeler import apply_labels
- FILE_MODIFY_MIN_SECONDS = 60
- # ROLLING_DAYS_30 = 30
- # ROLLING_DAYS_90 = 90
- # ====================== 配置项 ======================
- # base_dir = "/data/wind-turbine"
- base_dir = "/data/wind_files/headquarter/scada"
- BASE_TMP_DIR = Path(f"{base_dir}/tmp")
- BASE_LABEL_DIR = Path(f"{base_dir}/label")
- # BASE_ROLLING_DAYS_DIR = Path(f"{base_dir}/{ROLLING_DAYS}days")
- ERROR_DATA_DIR = Path(f"{base_dir}/error_data")
- LOG_DIR = Path("/data/logs/scada_data_py")
- LOG_FILE = LOG_DIR / f"process_{datetime.now().strftime('%Y%m%d')}.log"
- LOG_LEVEL = logging.INFO
- # 多进程配置(根据服务器CPU核心数调整)
- MAX_WORKERS = 8
- # ====================== 算法 ======================
- def wind_data_labeling(df: pd.DataFrame, model_name: str = '') -> pd.DataFrame:
- df = apply_labels(df, model_name)
- return df
- # ====================== 日志 ======================
- def init_logger():
- LOG_DIR.mkdir(parents=True, exist_ok=True)
- logging.basicConfig(
- level=LOG_LEVEL,
- format="%(asctime)s - %(levelname)s - %(message)s",
- handlers=[
- logging.FileHandler(LOG_FILE, encoding="utf-8"),
- logging.StreamHandler()
- ]
- )
- return logging.getLogger(__name__)
- logger = init_logger()
- # ====================== 工具方法 ======================
- def is_file_valid(file_path: Path) -> bool:
- try:
- pd.read_parquet(file_path)
- return True
- except Exception as e:
- logger.error(f"文件损坏,跳过:{file_path},异常:{str(e)}")
- return False
- def get_target_files(base_dir: Path) -> list:
- target_files = []
- now = time.time()
- for root, _, files in os.walk(base_dir):
- for file in files:
- if not file.endswith(".parquet"):
- continue
- file_path = Path(root) / file
- if now - file_path.stat().st_mtime <= FILE_MODIFY_MIN_SECONDS:
- continue
- target_files.append(file_path)
- return target_files
- from functools import wraps
- def retry_on_exception(max_retries=3, delay=1, backoff=2):
- """重试装饰器"""
- def decorator(func):
- @wraps(func)
- def wrapper(*args, **kwargs):
- retries = 0
- current_delay = delay
- while retries < max_retries:
- try:
- return func(*args, **kwargs)
- except Exception as e:
- retries += 1
- if retries == max_retries:
- raise
- logger.warning(f"操作失败,{retries}/{max_retries} 次重试,错误: {e}")
- time.sleep(current_delay)
- current_delay *= backoff
- return None
- return wrapper
- return decorator
- @retry_on_exception(max_retries=3, delay=0.5)
- def safe_read_parquet(file_path):
- """带重试机制的 Parquet 读取"""
- return pd.read_parquet(file_path)
- def merge_rolling_days_data(model: str, wind_farm_code: str, wind_turbine_code: str,
- df_oneday_label: pd.DataFrame, ROLLING_DAYS: int) -> None:
- BASE_ROLLING_DAYS_DIR = Path(f"{base_dir}/{ROLLING_DAYS}days")
- output_path = BASE_ROLLING_DAYS_DIR / model / wind_farm_code / f"{wind_turbine_code}.parquet"
- output_path.parent.mkdir(parents=True, exist_ok=True)
- df_list = []
- if output_path.exists():
- df_list.append(safe_read_parquet(output_path))
- df_list.append(df_oneday_label)
- merged_df = pd.concat(df_list, ignore_index=True)
- merged_df['localtime'] = pd.to_datetime(merged_df['localtime'], errors='coerce')
- merged_df.sort_values(by=['localtime'], inplace=True)
- # 按自然日 00:00 保留最近30天
- latest_time = merged_df['localtime'].max()
- latest_day = latest_time.floor('D')
- thirty_days_ago = latest_day - pd.Timedelta(days=ROLLING_DAYS - 1)
- merged_df = merged_df[merged_df['localtime'] > thirty_days_ago]
- merged_df.to_parquet(output_path, engine="pyarrow", index=False)
- logger.info(f"{ROLLING_DAYS}天滚动文件生成成功:{output_path}")
- # ====================== 单个文件处理逻辑(抽成独立函数)======================
- def process_single_file(tmp_file):
- # 20251020/CCWE1500-82.DF/EmlMGcty/zKCujSuK.parquet
- parts = tmp_file.parts
- data_date = parts[-4]
- model = parts[-3]
- wind_farm_code = parts[-2]
- wind_turbine_code = tmp_file.stem
- try:
- if not is_file_valid(tmp_file):
- target_path = ERROR_DATA_DIR / data_date / model / wind_farm_code / f"{wind_turbine_code}.parquet"
- target_path.parent.mkdir(parents=True, exist_ok=True)
- shutil.move(tmp_file, target_path)
- return "fail"
- # 读取 + 打标签
- df = safe_read_parquet(tmp_file)
- for col in df.columns:
- if col != "localtime":
- df[col] = pd.to_numeric(df[col], errors="coerce")
- # 补全数据,查询前一天最后一条数据,向后补全
- df_oneday_label = wind_data_labeling(df, model)
- # 保存label
- label_file = BASE_LABEL_DIR / data_date / model / wind_farm_code / f"{wind_turbine_code}.parquet"
- label_file.parent.mkdir(parents=True, exist_ok=True)
- df_oneday_label.to_parquet(label_file, engine="pyarrow", index=False)
- logger.info(f"{wind_turbine_code} 打标签完成:{label_file}")
- # 30天数据合并
- merge_rolling_days_data(model, wind_farm_code, wind_turbine_code, df_oneday_label, 30)
- # 90天数据合并
- merge_rolling_days_data(model, wind_farm_code, wind_turbine_code, df_oneday_label, 90)
- # 删除临时文件
- tmp_file.unlink()
- logger.info(f"临时文件已删除:{tmp_file}")
- return "success"
- except Exception as e:
- logger.error(f"处理失败:{tmp_file},异常:{str(e)}", exc_info=True)
- target_path = ERROR_DATA_DIR / data_date / model / wind_farm_code / f"{wind_turbine_code}.parquet"
- target_path.parent.mkdir(parents=True, exist_ok=True)
- shutil.move(tmp_file, target_path)
- return "fail"
- # ====================== 主流程(多进程)======================
- def process_wind_data():
- logger.info(f"临时目录:{BASE_TMP_DIR}")
- tmp_files = get_target_files(BASE_TMP_DIR)
- if not tmp_files:
- logger.info("无待处理文件,任务结束")
- return
- logger.info(f"待处理文件总数:{len(tmp_files)}")
- logger.info(f"启动多进程,进程数:{MAX_WORKERS}")
- # ========== 多进程核心 ==========
- with Pool(processes=MAX_WORKERS) as pool:
- results = pool.map(process_single_file, tmp_files)
- success_count = sum(1 for res in results if res == "success")
- fail_count = sum(1 for res in results if res == "fail")
- logger.info(f"任务完成 | 成功:{success_count} | 失败:{fail_count}")
- logger.info("=" * 50)
- if __name__ == "__main__":
- process_wind_data()
|