Files
UAV-CO2/src/gasflux/data_processor.py
2026-02-25 11:50:30 +08:00

1009 lines
35 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.

#!/usr/bin/env python3
"""
无人机数据处理脚本
处理Excel格式的无人机测量数据,转换为GasFlux标准输入格式。
支持两种使用方式:
1. 命令行调用:python data_processor.py input.xlsx
2. 直接调用:from data_processor import process_file; df = process_file('input.xlsx')
处理步骤:
1. 读取Excel文件
2. 删除不需要的列
3. 根据文件名修正时间格式
4. 坐标转换(经纬度除以10^7)
5. 计算气压(使用qiya.py)
6. 高度调整(减去最小高度)
7. 时间戳融合
8. 字段重命名
作者:GasFlux开发团队
"""
import pandas as pd
import numpy as np
from pathlib import Path
import re
from datetime import datetime
import sys
import os
from collections import Counter
import yaml
try:
from tqdm import tqdm
HAS_TQDM = True
except ImportError:
HAS_TQDM = False
print("WARNING: 未安装tqdm库,将不显示进度条。如需进度条,请运行: pip install tqdm")
def _safe_print(msg):
"""
安全打印函数,保证任何情况下不因打印而失败
避免 Windows 控制台编码问题导致的 OSError
"""
try:
print(msg)
except Exception:
try:
# 回退方案:移除非ASCII字符后打印
safe_msg = str(msg).encode('ascii', 'ignore').decode('ascii', 'ignore')
sys.stdout.write(safe_msg + '\n')
sys.stdout.flush()
except Exception:
# 最终回退:静默失败,不影响程序运行
pass
def create_height_bins(heights, bin_size=2.0):
"""
将高度数据按指定间隔分档
Args:
heights: 高度数据Series
bin_size: 每个档位的间隔(米)
Returns:
list: [(bin_min, bin_max, bin_center, count), ...] 每个档位的信息
"""
if len(heights) == 0:
return []
height_min = heights.min()
height_max = heights.max()
# 计算需要的档位数量
range_size = height_max - height_min
if range_size == 0:
# 所有高度相同
return [(height_min, height_max, height_min, len(heights))]
num_bins = max(1, int(np.ceil(range_size / bin_size)))
bins = []
for i in range(num_bins):
bin_min = height_min + i * bin_size
bin_max = min(height_min + (i + 1) * bin_size, height_max)
bin_center = (bin_min + bin_max) / 2
# 统计这个档位有多少数据点
count = ((heights >= bin_min) & (heights < bin_max)).sum()
if i == num_bins - 1: # 最后一个档位包含上限
count = ((heights >= bin_min) & (heights <= bin_max)).sum()
if count > 0: # 只保留有数据的档位
bins.append((bin_min, bin_max, bin_center, count))
return bins
# 导入qiya模块
try:
from .qiya import get_pressure_at_location
except ImportError as e:
print(f"导入qiya模块失败: {e}")
print("请确保GasFlux包结构完整")
sys.exit(1)
def load_excel_data(file_path):
"""
读取Excel文件并进行初步处理
Args:
file_path: Excel文件路径
Returns:
pd.DataFrame: 读取的数据
"""
try:
_safe_print(f"正在读取Excel文件: {Path(file_path).name}")
# 读取Excel文件
df = pd.read_excel(file_path)
print(f" 数据读取成功: {len(df):,} 行 × {len(df.columns)} 列")
print(f" 检测到的列: {', '.join(df.columns[:5])}{'...' if len(df.columns) > 5 else ''}")
return df
except Exception as e:
_safe_print(f" 文件读取失败: {Path(file_path).name}")
print(f" 错误详情: {e}")
sys.exit(1)
def remove_columns(df, columns_to_remove):
"""
删除指定的列
Args:
df: 输入DataFrame
columns_to_remove: 要删除的列名列表
Returns:
pd.DataFrame: 删除列后的DataFrame
"""
print(f"\n 删除不需要的列: {columns_to_remove}")
# 检查要删除的列是否存在
existing_columns = [col for col in columns_to_remove if col in df.columns]
missing_columns = [col for col in columns_to_remove if col not in df.columns]
if missing_columns:
print(f"以下列不存在(跳过): {missing_columns}")
if existing_columns:
df = df.drop(columns=existing_columns)
print(f"已删除 {len(existing_columns)} 列")
return df
def extract_hour_from_filename(filename):
"""
从文件名中提取小时信息
例如: "08_34_01_间隔高度5m.xlsx" -> "08"
Args:
filename: 文件名
Returns:
str: 小时字符串(两位数)
"""
# 使用正则表达式匹配小时部分
match = re.match(r'(\d{2})_', filename)
if match:
return match.group(1)
else:
print(f"无法从文件名 '{filename}' 中提取小时信息,使用默认值 '00'")
return "00"
def fix_time_column(df, filename):
"""
根据文件名修正时间列
Args:
df: 输入DataFrame
filename: 文件名
Returns:
pd.DataFrame: 修正后的DataFrame
"""
print(" 根据文件名修正时间格式...")
# 提取小时信息
hour_prefix = extract_hour_from_filename(filename)
print(f"从文件名提取的小时: {hour_prefix}")
# 检查时间列是否存在
if '时间' not in df.columns:
print("未找到 '时间' 列")
return df
# 修正时间格式
def fix_single_time(time_str):
if pd.isna(time_str):
return time_str
time_str = str(time_str).strip()
# 如果时间格式类似 "0:34:01" 或 "00:34:01"
if re.match(r'^\d{1,2}:\d{2}:\d{2}$', time_str):
parts = time_str.split(':')
original_hour = int(parts[0])
filename_hour = int(hour_prefix)
# 小时相加
new_hour = (filename_hour + original_hour) % 24 # 防止超过24小时
# 保持分钟和秒不变
new_time = f"{new_hour:02d}:{parts[1]}:{parts[2]}"
return new_time
else:
# 其他格式,使用默认时间
return f"{hour_prefix}:00:00"
# 应用时间修正
original_times = df['时间'].head(3).tolist()
df['时间'] = df['时间'].apply(fix_single_time)
corrected_times = df['时间'].head(3).tolist()
print(f"时间修正示例:")
for orig, corr in zip(original_times, corrected_times):
print(f" {orig} → {corr}")
return df
def convert_coordinates(df):
"""
转换经纬度坐标(除以10^7)
Args:
df: 输入DataFrame
Returns:
pd.DataFrame: 转换后的DataFrame
"""
print(" 转换经纬度坐标(GPS原始数据除以10^7)...")
if 'stGPSPositionX' in df.columns:
original_lon = df['stGPSPositionX'].head(3).tolist()
df['stGPSPositionX'] = df['stGPSPositionX'] / 1e7
converted_lon = df['stGPSPositionX'].head(3).tolist()
print(" 经度转换示例:")
for i, (orig, conv) in enumerate(zip(original_lon, converted_lon)):
print(f" 第{i+1}行: {orig:.6f} → {conv:.6f}")
if 'stGPSPositionY' in df.columns:
original_lat = df['stGPSPositionY'].head(3).tolist()
df['stGPSPositionY'] = df['stGPSPositionY'] / 1e7
converted_lat = df['stGPSPositionY'].head(3).tolist()
print(" 纬度转换示例:")
for i, (orig, conv) in enumerate(zip(original_lat, converted_lat)):
print(f" 第{i+1}行: {orig:.6f} → {conv:.6f}")
return df
def calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_size=10.0):
"""
计算气压数据(高度分档优化版)
Args:
df: 输入DataFrame
max_samples: 最大采样数量(None表示计算所有行)
height_tolerance: 高度变化容差(米),如果所有高度都在此范围内,只计算一次
height_bin_size: 高度分档间隔(米),每个档位使用中间高度计算气压
Returns:
pd.DataFrame: 添加气压列的DataFrame
"""
print(" 计算气压数据(使用qiya.py大气模型)...")
# 检查必要列是否存在(支持原始列名和新列名)
# 原始数据中的列名
original_cols = ['qStrDate', 'qStrTime', 'stGPSPositionX', 'stGPSPositionY', 'fAltitudeFused']
# 坐标转换后的列名(经纬度已被除以1e7)
converted_cols = ['qStrDate', 'qStrTime', 'stGPSPositionX', 'stGPSPositionY', 'fAltitudeFused']
# 优先使用转换后的列名(如果存在),否则使用原始列名
date_col = 'qStrDate' if 'qStrDate' in df.columns else '日期'
time_col = 'qStrTime' if 'qStrTime' in df.columns else '时间'
lon_col = 'stGPSPositionX' if 'stGPSPositionX' in df.columns else '经度'
lat_col = 'stGPSPositionY' if 'stGPSPositionY' in df.columns else '纬度'
height_col = 'fAltitudeFused' if 'fAltitudeFused' in df.columns else '融合高程'
required_cols = [date_col, time_col, lon_col, lat_col, height_col]
missing_cols = [col for col in required_cols if col not in df.columns]
if missing_cols:
print(f"缺少必要列: {missing_cols}")
return df
# 检查高度变化范围
height_min = df[height_col].min()
height_max = df[height_col].max()
height_range = height_max - height_min
print(f"高度范围: {height_min:.1f} - {height_max:.1f} 米 (变化: {height_range:.1f} 米)")
# 创建高度分档
height_bins = create_height_bins(df[height_col], height_bin_size)
print(f"高度分档: {len(height_bins)} 个档位 (间隔: {height_bin_size:.1f} 米)")
for i, (bin_min, bin_max, bin_center, count) in enumerate(height_bins):
print(f" 档位{i+1}: {bin_min:.1f}-{bin_max:.1f}m (中心: {bin_center:.1f}m, 数据: {count}行)")
# 决定计算策略
if height_range <= height_tolerance:
# 高度变化小,只计算一次气压
print("高度变化小,将使用平均高度计算一次气压")
use_single_calculation = True
mean_height = df[height_col].mean()
print(f"使用平均高度: {mean_height:.1f} 米")
elif len(height_bins) == 1:
# 只有一个高度档位,使用档位中心高度
print("只有一个高度档位,使用档位中心高度")
use_single_calculation = True
mean_height = height_bins[0][2] # bin_center
print(f"使用档位中心高度: {mean_height:.1f} 米")
else:
# 高度变化大,使用分档计算
print("使用高度分档策略,减少API调用")
use_single_calculation = False
# 确定要处理的行数
if max_samples is None or max_samples >= len(df):
# 计算所有行
sample_df = df.copy()
actual_samples = len(df)
if not use_single_calculation:
print(f"将计算所有 {len(df)} 行的气压数据")
else:
# 限制采样数量
print(f"数据量较大 ({len(df)} 行),只对前 {max_samples} 行计算气压")
sample_df = df.head(max_samples).copy()
actual_samples = max_samples
pressures = []
if use_single_calculation:
# 只计算一次气压
try:
# 使用第一行的日期和时间作为代表
first_row = sample_df.iloc[0]
# 转换日期格式 - 只提取日期部分,移除任何时间信息
date_str = str(first_row[date_col])
if ' ' in date_str:
date_str = date_str.split(' ')[0] # 处理 "2026-01-15 00:00:00" 格式
elif 'T' in date_str:
date_str = date_str.split('T')[0] # 处理ISO格式
if '/' in date_str:
date_str = date_str.replace('/', '-')
date_parts = date_str.split('-')
if len(date_parts) == 3 and len(date_parts[0]) == 4:
year, month, day = date_parts
formatted_date = f"{year}-{month.zfill(2)}-{day.zfill(2)}"
else:
raise ValueError(f"日期格式异常: {date_str}")
# 使用数据的代表性时间(整点小时,众数)
time_strings = []
for time_val in sample_df[time_col]:
time_str = str(time_val).strip()
# 处理时间字符串,提取正确的部分
# 如果时间字符串包含多个时间部分(如 "00:00:00 08:37:12"),取最后一个
if ' ' in time_str:
time_parts = time_str.split()
time_str = time_parts[-1] # 取最后一个有效的时间部分
if ':' in time_str:
# 确保是有效的 HH:MM 格式,然后取整点小时
parts = time_str.split(':')
if len(parts) >= 2:
try:
hour = int(parts[0])
# 确保小时在有效范围内 (0-23)
if 0 <= hour <= 23:
time_strings.append(f"{hour:02d}:00")
except ValueError:
continue
if time_strings:
time_counts = Counter(time_strings)
formatted_time = time_counts.most_common(1)[0][0]
occurrence_count = time_counts.most_common(1)[0][1]
print(f"数据时间选择: {formatted_time} ({occurrence_count}/{len(time_strings)} 次,{occurrence_count/len(time_strings)*100:.1f}%)")
else:
formatted_time = "12:00"
print("无有效时间数据,使用默认中午12:00")
print("正在计算平均气压...")
pressure = get_pressure_at_location(
lat=sample_df[lat_col].mean(),
lon=sample_df[lon_col].mean(),
altitude=mean_height,
date=formatted_date,
time=formatted_time
)
if pressure is not None:
pressures = [pressure] * len(sample_df)
print("平均气压计算成功,将应用到所有行")
else:
print("平均气压计算失败")
pressures = [None] * len(sample_df)
except Exception as e:
print(f"平均气压计算失败: {e}")
pressures = [None] * len(sample_df)
else:
# 使用高度分档策略
print("开始分档计算气压...")
# 为每个高度档位计算气压
bin_pressures = {} # bin_center -> pressure
# 设置进度条
iterator = height_bins
if HAS_TQDM:
iterator = tqdm(iterator, total=len(height_bins), desc="计算气压档位", unit="档")
for bin_min, bin_max, bin_center, count in iterator:
try:
# 使用第一行数据作为代表来获取日期和时间
# 找到这个档位中的一行数据
bin_rows = sample_df[(sample_df[height_col] >= bin_min) &
(sample_df[height_col] <= bin_max)]
if len(bin_rows) == 0:
continue
first_row = bin_rows.iloc[0]
# 转换日期格式 - 只提取日期部分,移除任何时间信息
date_str = str(first_row[date_col])
if ' ' in date_str:
date_str = date_str.split(' ')[0] # 处理 "2026-01-15 00:00:00" 格式
elif 'T' in date_str:
date_str = date_str.split('T')[0] # 处理ISO格式
if '/' in date_str:
date_str = date_str.replace('/', '-')
date_parts = date_str.split('-')
if len(date_parts) == 3 and len(date_parts[0]) == 4:
year, month, day = date_parts
formatted_date = f"{year}-{month.zfill(2)}-{day.zfill(2)}"
else:
print(f"档位高度 {bin_center:.1f}m 日期格式异常: {date_str}")
bin_pressures[bin_center] = None
continue
# 使用该档位数据的代表性时间(整点小时)
time_strings = []
for time_val in bin_rows[time_col]:
time_str = str(time_val).strip()
# 处理时间字符串,提取正确的部分
# 如果时间字符串包含多个时间部分(如 "00:00:00 08:37:12"),取最后一个
if ' ' in time_str:
time_parts = time_str.split()
time_str = time_parts[-1] # 取最后一个有效的时间部分
if ':' in time_str:
# 确保是有效的 HH:MM 格式,然后取整点小时
parts = time_str.split(':')
if len(parts) >= 2:
try:
hour = int(parts[0])
# 确保小时在有效范围内 (0-23)
if 0 <= hour <= 23:
time_strings.append(f"{hour:02d}:00")
except ValueError:
continue
if time_strings:
# 使用最常见的时间(众数)
time_counts = Counter(time_strings)
formatted_time = time_counts.most_common(1)[0][0]
occurrence_count = time_counts.most_common(1)[0][1]
print(f" 档位时间选择: {formatted_time} ({occurrence_count}/{len(time_strings)} 次,{occurrence_count/len(time_strings)*100:.1f}%)")
else:
# 如果没有有效时间,使用中午12点
formatted_time = "12:00"
print(" 无有效时间数据,使用默认中午12:00")
# 计算这个档位的气压(使用平均位置和档位中心高度)
avg_lat = bin_rows[lat_col].mean()
avg_lon = bin_rows[lon_col].mean()
pressure = get_pressure_at_location(
lat=avg_lat,
lon=avg_lon,
altitude=bin_center,
date=formatted_date,
time=formatted_time
)
bin_pressures[bin_center] = pressure
# 更新进度条
if HAS_TQDM:
success_count = sum(1 for p in bin_pressures.values() if p is not None)
iterator.set_description(f"计算档位 (成功: {success_count}/{len(bin_pressures)})")
except Exception as e:
print(f"计算高度档位 {bin_center:.1f}m 气压失败: {e}")
bin_pressures[bin_center] = None
# 为每一行分配对应档位的气压
pressures = []
for idx, row in sample_df.iterrows():
# 找到这个高度对应的档位
height = row[height_col]
assigned_pressure = None
for bin_min, bin_max, bin_center, count in height_bins:
if bin_min <= height <= bin_max:
assigned_pressure = bin_pressures.get(bin_center)
break
pressures.append(assigned_pressure)
print(f"完成分档气压计算,共 {len(bin_pressures)} 个档位,{len([p for p in bin_pressures.values() if p is not None])} 个成功")
# 添加气压列
df['pressure'] = None # 初始化
df.loc[sample_df.index, 'pressure'] = pressures
# 对于未计算的行,使用插值或平均值填充
if max_samples is not None and len(df) > max_samples:
# 只计算了部分行,用平均值填充其余行
valid_pressures_for_mean = [p for p in pressures if p is not None]
if valid_pressures_for_mean:
mean_pressure = sum(valid_pressures_for_mean) / len(valid_pressures_for_mean)
df['pressure'] = df['pressure'].fillna(mean_pressure)
print(f"使用平均气压填充其余 {len(df) - max_samples} 行: {mean_pressure:.1f} hPa")
# 统计信息
valid_pressures = [p for p in pressures if p is not None]
if valid_pressures:
avg_pressure = sum(valid_pressures) / len(valid_pressures)
print(f"成功计算 {len(valid_pressures)}/{actual_samples} 个气压值,平均值: {avg_pressure:.1f} hPa")
else:
print("未能计算出任何气压值")
return df
def adjust_altitude(df):
"""
创建调整后的高度字段(减去最小值)
Args:
df: 输入DataFrame
Returns:
pd.DataFrame: 添加调整后高度字段的DataFrame
"""
print(" 创建调整后的高度字段...")
if 'fAltitudeFused' in df.columns:
min_altitude = df['fAltitudeFused'].min()
print(f" 最小高度值: {min_altitude:.2f} 米")
# 创建新的调整后高度字段,而不是修改原始字段
df['height_ato'] = df['fAltitudeFused'] - min_altitude
original_alt = df['fAltitudeFused'].head(3).tolist()
adjusted_alt = df['height_ato'].head(3).tolist()
print(" 高度调整示例:")
for i, (orig, adj) in enumerate(zip(original_alt, adjusted_alt)):
print(f" 第{i+1}行: {orig:.2f}m → {adj:.2f}m")
else:
print(" 未找到 'fAltitudeFused' 列,跳过高度调整")
return df
def merge_timestamp(df):
"""
融合日期和时间列为时间戳(修正时间格式)
Args:
df: 输入DataFrame
Returns:
pd.DataFrame: 融合后的DataFrame
"""
print(" 融合日期和时间为时间戳...")
if 'qStrDate' in df.columns and 'qStrTime' in df.columns:
timestamps = []
for idx, row in df.iterrows():
try:
date_str = str(row['qStrDate']).strip()
time_str = str(row['qStrTime']).strip()
# 处理日期字符串,提取纯日期部分
# 如果日期字符串包含时间部分(如 "2026-01-15 00:00:00"),取日期部分
if ' ' in date_str:
date_parts = date_str.split()
date_str = date_parts[0] # 取第一个部分作为日期
# 处理时间字符串,提取正确的部分
# 如果时间字符串包含多个时间部分(如 "00:00:00 08:37:12"),取最后一个
if ' ' in time_str:
time_parts = time_str.split()
# 取最后一个有效的时间部分
time_str = time_parts[-1]
# 确保时间格式正确
if ':' in time_str and len(time_str.split(':')) >= 2:
# 组合日期和修正后的时间
timestamp = f"{date_str} {time_str}"
else:
timestamp = f"{date_str} 12:00:00" # 默认中午时间
print(f" 时间格式异常 '{row['qStrTime']}',使用默认时间")
timestamps.append(timestamp)
except Exception as e:
print(f"处理第 {idx+1} 行时间戳失败: {e}")
timestamps.append(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
df['timestamp'] = timestamps
print("时间戳融合示例:")
for i in range(min(3, len(timestamps))):
print(f" {df.loc[i, 'qStrDate']} + {df.loc[i, 'qStrTime']} → {timestamps[i]}")
return df
def rename_columns(df):
"""
重命名字段为GasFlux标准格式
Args:
df: 输入DataFrame
Returns:
pd.DataFrame: 重命名后的DataFrame
"""
print(" 重命名字段为GasFlux标准格式...")
# 定义字段映射
column_mapping = {
'timestamp': 'timestamp', # 时间戳(已创建)
'stGPSPositionX': 'longitude', # 经度 → longitude
'stGPSPositionY': 'latitude', # 纬度 → latitude
# 'fAltitudeFused': 'height_ato', # 高度字段已在adjust_altitude中处理
'fFixedWindDirection': 'winddir', # 修正风向 → winddir
'fFixedWindSpeed': 'windspeed', # 修正风速 → windspeed
'fWindTemperature': 'temperature', # 风温 → temperature
'CO2': 'CO2', # CO2浓度 → co2
'pitch': 'course_elevation', # pitch → course_elevation
'yaw': 'course_azimuth' # yaw → course_azimuth
}
# 先复制字段,再对复制的字段重命名
columns_renamed = []
for old_name, new_name in column_mapping.items():
if old_name in df.columns:
# 复制字段到新名称
df[new_name] = df[old_name].copy()
columns_renamed.append((old_name, new_name))
print(f" 复制并重命名: {old_name} → {new_name}")
if columns_renamed:
print(f"共处理了 {len(columns_renamed)} 个字段")
return df
def ensure_float64_types(df, config=None):
"""
确保数值字段为float64类型,以满足GasFlux处理要求
Args:
df: 输入DataFrame
config: 配置字典,包含gases字段
Returns:
pd.DataFrame: 数据类型转换后的DataFrame
"""
print(" 确保数值字段类型为float64(GasFlux要求)...")
# 定义基础需要转换为float64的字段
float64_columns = [
'longitude', 'latitude', 'height_ato', # 位置和高度
'winddir', 'windspeed', 'temperature', # 风和温度
'pressure', # 气压
'course_elevation', 'course_azimuth' # 姿态角
]
# 如果提供了配置,添加gases中的气体列
if config and 'gases' in config:
gas_columns = list(config['gases'].keys())
float64_columns.extend(gas_columns)
print(f"从配置中添加气体列: {gas_columns}")
converted_count = 0
for col in float64_columns:
if col in df.columns:
try:
original_dtype = df[col].dtype
df[col] = df[col].astype('float64')
new_dtype = df[col].dtype
if original_dtype != new_dtype:
print(f" 转换: {col} ({original_dtype} → {new_dtype})")
converted_count += 1
except Exception as e:
print(f" 转换失败: {col} - {e}")
if converted_count > 0:
print(f"共转换了 {converted_count} 个字段的数据类型为float64")
else:
print("所有数值字段已经是float64类型")
return df
def process_excel_file(file_path, config_path=None):
"""
处理单个Excel文件的主函数
Args:
file_path: Excel文件路径
config_path: 配置文件路径,用于读取gases配置
"""
_safe_print(f" === 开始处理文件: {Path(file_path).name} ===\n")
# 1. 读取数据
df = load_excel_data(file_path)
# 2. 坐标转换
df = convert_coordinates(df)
# 4. 计算气压数据
df = calculate_pressure(df, max_samples=None, height_tolerance=10.0, height_bin_size=2.0) # 计算所有行,高度容差10米,分档2米
# 5. 高度调整
df = adjust_altitude(df)
# 6. 时间戳融合(保持原有时间格式)
df = merge_timestamp(df)
# 调试:检查当前列
print(f"时间戳融合后列名: {list(df.columns)}")
if 'timestamp' in df.columns:
print(f"timestamp列示例: {df['timestamp'].head(3).tolist()}")
# 7. 字段重命名
df = rename_columns(df)
# 8. 确保数值字段类型为float64
# 读取配置以获取gases字段
config = None
if config_path:
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
except Exception as e:
print(f"读取配置文件失败: {e}")
df = ensure_float64_types(df, config)
# 保存处理结果
output_path = Path(file_path).with_suffix('.processed.csv')
df.to_csv(output_path, index=False, encoding='utf-8-sig')
print(f"\n 处理完成!")
_safe_print(f" 输出文件: {output_path}")
print(f" 最终数据: {df.shape[0]:,} 行 × {df.shape[1]} 列")
print(f" 最终字段: {', '.join(df.columns)}")
return df
def process_file(input_file, output_file=None, config_file=None):
"""
直接处理Excel文件的函数(不使用命令行参数)
Args:
input_file: 输入Excel文件路径(字符串或Path对象)
output_file: 输出CSV文件路径(可选,字符串或Path对象)
config_file: 配置文件路径(可选,用于读取gases配置)
Returns:
pd.DataFrame: 处理后的DataFrame
"""
# 转换为Path对象
input_path = Path(input_file)
# 检查输入文件
if not input_path.exists():
raise FileNotFoundError(f"输入文件不存在: {input_path}")
if input_path.suffix.lower() not in ['.xlsx', '.xls']:
raise ValueError(f"输入文件必须是Excel格式 (.xlsx 或 .xls),当前文件: {input_path}")
# 处理文件
df = process_excel_file(str(input_path), config_file)
# 如果指定了输出路径,额外保存一份
if output_file:
output_path = Path(output_file)
df.to_csv(output_path, index=False, encoding='utf-8-sig')
_safe_print(f"额外保存到: {output_path}")
return df
def interactive_input():
"""
交互式输入模式 - 手动输入参数
Returns:
tuple: (input_file, output_file) 文件路径元组
"""
print("=== 手动输入模式 ===")
print("请按照提示输入参数...\n")
# 输入文件路径
while True:
input_file = input("请输入Excel文件路径 (例如: data.xlsx): ").strip()
if not input_file:
print("文件路径不能为空,请重新输入")
continue
input_path = Path(input_file)
if not input_path.exists():
print(f"文件不存在: {input_path}")
print("提示: 请确保文件路径正确,或者将文件放在当前目录下")
continue
if input_path.suffix.lower() not in ['.xlsx', '.xls']:
print(f"文件格式错误: {input_path.suffix}")
print("只支持 .xlsx 和 .xls 格式的Excel文件")
continue
break
# 输出文件路径(可选)
output_file = input("请输入输出CSV文件路径 (可选,直接回车使用默认): ").strip()
if not output_file:
output_file = None
print("使用默认输出文件名")
print(f"\n输入确认:")
print(f" 输入文件: {input_file}")
print(f" 输出文件: {output_file or '自动生成'}")
confirm = input("\n确认开始处理? (y/N): ").strip().lower()
if confirm not in ['y', 'yes', '是', '确认']:
print("用户取消操作")
return None, None
return input_file, output_file
def main(input_file=None, output_file=None, interactive=False):
"""
主函数 - 支持多种输入方式
Args:
input_file: 输入文件路径(直接调用时使用)
output_file: 输出文件路径(直接调用时使用)
interactive: 是否启用交互式输入模式
"""
# 如果启用交互式输入
if interactive:
input_file, output_file = interactive_input()
if input_file is None:
return None
# 如果提供了直接参数,使用直接参数
if input_file is not None:
try:
return process_file(input_file, output_file)
except Exception as e:
print(f"处理失败: {e}")
raise
# 否则使用命令行参数
import argparse
parser = argparse.ArgumentParser(
description="无人机数据处理工具 - 将Excel数据转换为GasFlux格式",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用方式:
1. 命令行模式:
python data_processor.py data.xlsx
python data_processor.py data.xlsx -o output.csv
2. 交互式模式:
python data_processor.py --interactive
3. 直接调用:
from data_processor import process_file
df = process_file('data.xlsx', 'output.csv')
4. Python脚本调用:
from data_processor import main
df = main(input_file='data.xlsx', output_file='output.csv')
处理步骤:
1. 读取Excel文件
2. 删除不需要的列
3. 根据文件名修正时间格式
4. 经纬度坐标转换 (除以10^7)
5. 计算气压数据
6. 高度调整 (减去最小值)
7. 时间戳融合
8. 字段重命名为GasFlux格式
"""
)
parser.add_argument('input_file', nargs='?', help='输入的Excel文件路径')
parser.add_argument('-o', '--output', help='输出CSV文件路径(可选,默认自动生成)')
parser.add_argument('-i', '--interactive', action='store_true', help='启用交互式输入模式')
args = parser.parse_args()
# 如果启用交互式模式
if args.interactive:
input_file, output_file = interactive_input()
if input_file is None:
return None
else:
input_file = args.input_file
output_file = args.output
# 如果没有提供输入文件,显示帮助和使用示例
if not input_file:
parser.print_help()
print("\n" + "="*60)
print("使用示例:")
print("="*60)
print("1. 命令行模式:")
print(" python data_processor.py your_file.xlsx")
print(" python data_processor.py data.xlsx -o output.csv")
print("")
print("2. Python脚本中直接调用:")
print(" from data_processor import process_file")
print(" df = process_file('input.xlsx')")
print(" df = process_file('input.xlsx', 'output.csv')")
print("")
print("3. 交互式模式:")
print(" python data_processor.py --interactive")
print("="*60)
return None
# 检查输入文件
input_path = Path(input_file)
if not input_path.exists():
print(f"错误:输入文件不存在: {input_path}")
sys.exit(1)
if input_path.suffix.lower() not in ['.xlsx', '.xls']:
print(f"错误:输入文件必须是Excel格式 (.xlsx 或 .xls)")
sys.exit(1)
# 处理文件
try:
df = process_excel_file(str(input_path))
# 如果指定了输出路径,额外保存一份
if output_file:
output_path = Path(output_file)
df.to_csv(output_path, index=False, encoding='utf-8-sig')
_safe_print(f"额外保存到: {output_path}")
return df
except KeyboardInterrupt:
print("\n用户中断处理")
sys.exit(1)
except Exception as e:
print(f"\n处理失败: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
# 当直接运行脚本时,使用命令行参数模式
main()