#!/usr/bin/env python3 """ 高光谱反射率校正工具 功能: - 读取ENVI ASCII Plot File格式的反射率校正文件 - 读取航带文件夹中的高光谱数据文件 (.bil/.bip/.bsq等ENVI格式) 注意:spectral库通过读取对应的.hdr头文件来访问数据 - 根据波长匹配进行反射率校正(航带数据 / 校正值) - 保存校正后的反射率文件 依赖: - numpy - GDAL - 用于读取和保存ENVI格式高光谱数据 - pathlib 使用方法: python redlence.py <航带文件夹路径> <校正文件路径> [输出文件夹路径] 文件要求: - 航带文件夹中应包含 .bil/.bip/.bsq 文件及其对应的 .hdr 头文件 - 校正文件为ENVI ASCII Plot File格式,包含波长和校正值两列数据 """ import numpy as np import os import sys from pathlib import Path from typing import Tuple, List, Dict, Optional import argparse # 可选:GDAL库用于读取和保存ENVI格式文件 try: from osgeo import gdal # GDAL性能优化配置 GDAL_AVAILABLE = True except ImportError: GDAL_AVAILABLE = False print("警告: GDAL不可用,请安装GDAL") def parse_correction_file(correction_file: str) -> Tuple[np.ndarray, np.ndarray]: """ 解析ENVI ASCII Plot File格式的反射率校正文件(优化版本) 参数: ----------- correction_file : str 校正文件路径 返回: ----------- wavelengths : np.ndarray 波长数组 (nm) corrections : np.ndarray 校正值数组 """ try: # 使用numpy的loadtxt加速读取(跳过标题行) # 首先找到数据开始的行号 with open(correction_file, 'r', encoding='utf-8') as f: lines = f.readlines() # 找到数据开始的行索引 data_start_idx = 0 for i, line in enumerate(lines): line = line.strip() if not line or line.startswith(';'): continue if 'ENVI ASCII Plot File' in line or 'Column 1:' in line or 'Column 2:' in line: continue # 找到第一个可能是数据行的行 try: parts = line.split() if len(parts) >= 2: float(parts[0]), float(parts[1]) data_start_idx = i break except (ValueError, IndexError): continue if data_start_idx == 0 and not lines: raise ValueError("校正文件中未找到有效的数据行") # 使用numpy loadtxt从数据开始行读取 data = np.loadtxt(correction_file, skiprows=data_start_idx, dtype=float) if data.ndim == 1: # 如果只有一行数据 wavelengths = np.array([data[0]]) corrections = np.array([data[1]]) else: wavelengths = data[:, 0] corrections = data[:, 1] print(f"✅ 成功解析校正文件: {correction_file}") print(f" 数据点数: {len(wavelengths)}") print(f" 波长范围: {wavelengths.min():.1f} - {wavelengths.max():.1f} nm") print(f" 校正值范围: {corrections.min():.6f} - {corrections.max():.6f}") return wavelengths, corrections except Exception as e: # 如果numpy loadtxt失败,回退到原始方法 print(f"⚠️ numpy加速读取失败,回退到逐行读取: {e}") return parse_correction_file_fallback(correction_file) def parse_correction_file_fallback(correction_file: str) -> Tuple[np.ndarray, np.ndarray]: """ 回退方法:使用原始的逐行解析方式 """ wavelengths = [] corrections = [] try: with open(correction_file, 'r', encoding='utf-8') as f: lines = f.readlines() # 跳过标题行,找到数据开始 for line in lines: line = line.strip() # 跳过空行和注释 if not line or line.startswith(';'): continue # 跳过标题 if 'ENVI ASCII Plot File' in line or 'Column 1:' in line or 'Column 2:' in line: continue # 尝试解析数据行(波长 校正值) try: parts = line.split() if len(parts) >= 2: wavelength = float(parts[0]) correction = float(parts[1]) wavelengths.append(wavelength) corrections.append(correction) except (ValueError, IndexError): # 如果这一行不是数据,可能是其他格式,跳过 continue if not wavelengths: raise ValueError("校正文件中未找到有效的数据行") wavelengths = np.array(wavelengths, dtype=float) corrections = np.array(corrections, dtype=float) return wavelengths, corrections except Exception as e: raise RuntimeError(f"解析校正文件失败: {correction_file}, 错误: {e}") def find_hyperspectral_files(folder_path: str) -> List[str]: """ 查找文件夹中的高光谱数据文件 支持的格式:.bil, .bip, .bsq等ENVI格式文件 注意:GDAL可以直接读取ENVI格式文件,会自动查找对应的.hdr头文件 参数: ----------- folder_path : str 文件夹路径 返回: ----------- file_list : List[str] 高光谱数据文件路径列表 (.bil/.bip/.bsq文件) """ folder = Path(folder_path) if not folder.exists(): raise FileNotFoundError(f"文件夹不存在: {folder_path}") # 支持的ENVI格式扩展名 supported_extensions = ['.bil', '.bip', '.bsq','.dat'] hyperspectral_files = [] for ext in supported_extensions: files = list(folder.glob(f'*{ext}')) hyperspectral_files.extend([str(f) for f in files]) # 去重(防止同一个文件被多次识别) hyperspectral_files = list(set(hyperspectral_files)) if not hyperspectral_files: print(f"警告: 在文件夹 {folder_path} 中未找到高光谱数据文件") print("支持的格式: .bil, .bip, .bsq") return sorted(hyperspectral_files) def load_hyperspectral_data(file_path: str) -> Tuple[np.ndarray, Dict]: """ 使用GDAL读取高光谱数据文件 - 流式版本 不加载整个立方体,只返回数据集句柄和元数据 参数: ----------- file_path : str 高光谱数据文件路径 (.bil/.bip/.bsq) 返回: ----------- dataset : gdal.Dataset GDAL数据集对象(用于流式读取) metadata : dict 元数据信息 """ if not GDAL_AVAILABLE: raise RuntimeError("需要GDAL库来读取高光谱文件,请安装GDAL") file_path = Path(file_path) try: # 使用GDAL打开ENVI文件 dataset = gdal.Open(str(file_path), gdal.GA_ReadOnly) if dataset is None: raise RuntimeError(f"无法打开文件: {file_path}") # 获取基本信息 lines = dataset.RasterYSize samples = dataset.RasterXSize bands = dataset.RasterCount # 查找对应的HDR文件 hdr_file = None hdr_candidates = [ file_path.with_suffix('.hdr'), # datafile.hdr file_path.with_suffix(file_path.suffix + '.hdr'), # datafile.ext.hdr file_path.parent / f"{file_path.name}.hdr", # datafile.ext.hdr (另一种写法) ] for candidate in hdr_candidates: if candidate.exists(): hdr_file = candidate break # 尝试从HDR文件中提取波长信息 wavelengths = None if hdr_file and hdr_file.exists(): try: # 读取HDR文件内容 with open(str(hdr_file), 'r', encoding='utf-8', errors='ignore') as f: hdr_content = f.read() # 提取波长信息 import re wavelength_match = re.search(r'wavelength\s*=\s*\{([^}]+)\}', hdr_content, re.IGNORECASE) if wavelength_match: wavelength_str = wavelength_match.group(1) # 解析波长值 wavelength_values = [] for val in wavelength_str.split(','): val = val.strip() try: wavelength_values.append(float(val)) except ValueError: continue if wavelength_values: wavelengths = np.array(wavelength_values, dtype=float) except Exception as e: print(f"⚠️ 无法从HDR文件读取波长信息: {e}") # 构建元数据 metadata = { 'file_path': str(file_path), # 原始数据文件路径 'hdr_file': str(hdr_file) if hdr_file else None, # 对应的hdr文件路径 'lines': lines, 'samples': samples, 'bands': bands, 'wavelengths': wavelengths, 'data_type': 'float32', # 流式读取时的数据类型 'interleave': 'unknown' # GDAL不直接提供interleave信息 } print(f"✅ 成功打开高光谱文件: {Path(file_path).name}") if hdr_file: print(f" HDR文件: {Path(hdr_file).name}") print(f" 数据尺寸: {metadata['lines']} x {metadata['samples']} x {metadata['bands']}") print(f" 数据类型: {metadata['data_type']} (流式)") if metadata['wavelengths'] is not None: print(f" 波长范围: {metadata['wavelengths'][0]:.1f} - {metadata['wavelengths'][-1]:.1f} nm") return dataset, metadata except Exception as e: raise RuntimeError(f"读取高光谱文件失败: {file_path}, 错误: {e}") def interpolate_corrections(wavelengths_data: np.ndarray, wavelengths_corr: np.ndarray, corrections: np.ndarray) -> np.ndarray: """ 将校正值插值到数据波长上 参数: ----------- wavelengths_data : np.ndarray 数据文件的波长数组 wavelengths_corr : np.ndarray 校正文件的波长数组 corrections : np.ndarray 校正值数组 返回: ----------- interpolated_corrections : np.ndarray 插值后的校正值数组 """ if wavelengths_data is None: raise ValueError("数据文件缺少波长信息,无法进行校正") try: # 使用线性插值 from scipy.interpolate import interp1d interp_func = interp1d(wavelengths_corr, corrections, kind='linear', bounds_error=False, fill_value='extrapolate') interpolated = interp_func(wavelengths_data) print(f"✅ 成功插值校正值到数据波长") print(f" 数据波段数: {len(wavelengths_data)}") print(f" 校正数据点数: {len(wavelengths_corr)}") print(f" 插值范围: {interpolated.min():.3f} - {interpolated.max():.3f}") return interpolated except ImportError: # 如果没有scipy,使用numpy的interp print("警告: scipy不可用,使用numpy进行线性插值") interpolated = np.interp(wavelengths_data, wavelengths_corr, corrections) print(f"✅ 使用numpy插值校正值到数据波长") print(f" 插值范围: {interpolated.min():.3f} - {interpolated.max():.3f}") return interpolated def apply_reflectance_correction_streaming(input_dataset, output_dataset, corrections: np.ndarray, block_size: int = 1024): """ 应用反射率校正 - 流式版本 按块读取→校正→写入,避免加载整个立方体 参数: ----------- input_dataset : gdal.Dataset 输入数据集 output_dataset : gdal.Dataset 输出数据集 corrections : np.ndarray 校正值数组 (bands,) block_size : int 块大小(行数) """ lines = input_dataset.RasterYSize samples = input_dataset.RasterXSize bands = input_dataset.RasterCount if bands != len(corrections): raise ValueError(f"数据波段数 ({bands}) 与校正值数量 ({len(corrections)}) 不匹配") print("🔢 正在应用反射率校正(流式处理,向量化加速)...") # 用“乘倒数”替代逐元素除法,并在无效校正值(0/NaN/Inf)处直接置零 corrections = np.asarray(corrections, dtype=np.float32) scale = np.zeros((bands,), dtype=np.float32) valid = np.isfinite(corrections) & (corrections != 0) scale[valid] = np.float32(10000.0) / corrections[valid] scale_3d = scale[:, None, None] # 提前缓存输出波段对象,避免循环内重复 GetRasterBand output_bands = [output_dataset.GetRasterBand(i + 1) for i in range(bands)] total_blocks = (lines + block_size - 1) // block_size # 按块处理:每个块一次性读全波段并向量化计算 for block_idx, y_start in enumerate(range(0, lines, block_size), start=1): y_end = min(y_start + block_size, lines) actual_block_size = y_end - y_start # 低频打印进度,避免大量I/O拖慢处理 if block_idx == 1 or block_idx == total_blocks or block_idx % 10 == 0: print(f" 处理块 {block_idx}/{total_blocks}: 行 {y_start}-{y_end-1} ({actual_block_size} 行)") block = input_dataset.ReadAsArray(0, y_start, samples, actual_block_size) if block is None: raise RuntimeError(f"读取数据块失败: y_start={y_start}, block_size={actual_block_size}") # 统一维度为 (bands, block_y, samples) if block.ndim == 2: block = block[np.newaxis, :, :] block_f = block.astype(np.float32, copy=False) np.multiply(block_f, scale_3d, out=block_f, casting='unsafe') np.clip(block_f, 0, 65535, out=block_f) block_u16 = block_f.astype(np.uint16, copy=False) # GDAL按波段写出 for band_idx in range(bands): output_bands[band_idx].WriteArray(block_u16[band_idx, :, :], 0, y_start) print("✅ 成功应用反射率校正(流式处理,向量化加速)") def save_corrected_data_streaming(input_dataset, corrections: np.ndarray, output_file: str, wavelengths: Optional[np.ndarray] = None, source_hdr: Optional[str] = None): """ 保存校正后的反射率数据为ENVI格式 - 流式版本 参数: ----------- input_dataset : gdal.Dataset 输入数据集 corrections : np.ndarray 校正值数组 output_file : str 输出文件路径(不含扩展名) wavelengths : np.ndarray, optional 波长信息 source_hdr : str, optional 源HDR文件路径,用于复制HDR内容 """ lines = input_dataset.RasterYSize samples = input_dataset.RasterXSize bands = input_dataset.RasterCount # 确保输出目录存在 output_path = Path(output_file) output_path.parent.mkdir(parents=True, exist_ok=True) # 输出文件路径 bil_file = str(output_path.with_suffix('.dat')) hdr_file = str(output_path.with_suffix('.hdr')) try: # 创建输出数据集 driver = gdal.GetDriverByName('ENVI') output_dataset = driver.Create(bil_file, samples, lines, bands, gdal.GDT_UInt16, options=['INTERLEAVE=BSQ']) if output_dataset is None: raise RuntimeError(f"无法创建ENVI数据集: {bil_file}") # 设置NoData值 for band_idx in range(bands): output_band = output_dataset.GetRasterBand(band_idx + 1) output_band.SetNoDataValue(0) # 应用流式校正和保存 apply_reflectance_correction_streaming(input_dataset, output_dataset, corrections) # 关闭输出数据集 output_dataset = None # 处理HDR文件 if source_hdr and Path(source_hdr).exists(): import shutil shutil.copy2(source_hdr, hdr_file) print(f"✅ 已复制源HDR文件: {Path(source_hdr).name}") else: # 创建HDR头文件 create_envi_header(hdr_file, lines, samples, bands, wavelengths, None) print(f"✅ 成功保存校正结果:") print(f" 数据文件: {bil_file}") print(f" 头文件: {hdr_file}") print(f" 数据尺寸: {lines} x {samples} x {bands}") print(f" 数据类型: uint16 (反射率x10000)") print(f" 处理方式: 流式处理") except Exception as e: raise RuntimeError(f"保存文件失败: {output_file}, 错误: {e}") def save_with_gdal(data: np.ndarray, bil_file: str, wavelengths: Optional[np.ndarray] = None, source_file: Optional[str] = None, source_hdr: Optional[str] = None): """ 使用GDAL保存ENVI格式文件 """ lines, samples, bands = data.shape hdr_file = bil_file.replace('.dat', '.hdr') # 创建GDAL驱动 driver = gdal.GetDriverByName('ENVI') # 创建数据集 - 使用uint16格式优化性能和文件大小 dataset = driver.Create(bil_file, samples, lines, bands, gdal.GDT_UInt16, options=['INTERLEAVE=BSQ']) if dataset is None: raise RuntimeError(f"无法创建ENVI数据集: {bil_file}") try: # 设置元数据 metadata = dataset.GetMetadata() metadata['DESCRIPTION'] = 'Reflectance corrected hyperspectral data using Python' metadata['SENSOR_TYPE'] = 'Hyperspectral' metadata['DATA_UNITS'] = 'Reflectance' metadata['PROCESSING_ALGORITHM'] = 'Reflectance Correction' metadata['CREATION_DATE'] = str(np.datetime64('now')) if source_file: metadata['SOURCE_FILE'] = Path(source_file).name # 添加波长信息到元数据 if wavelengths is not None and len(wavelengths) == bands: metadata['wavelength_units'] = 'nm' for i, wl in enumerate(wavelengths): metadata[f'wavelength_{i+1}'] = str(wl) dataset.SetMetadata(metadata) # 写入数据 - 优化版本:转换为uint16格式 print(f"💾 正在写入 {bands} 个波段的数据...") # 将反射率转换为uint16(乘以10000以保留4位小数精度,裁剪到有效范围) data_uint16 = np.clip(data * 10000, 0, 65535).astype(np.uint16, copy=False) for band_idx in range(bands): band = dataset.GetRasterBand(band_idx + 1) band_data = data_uint16[:, :, band_idx] band.WriteArray(band_data) band.SetNoDataValue(0) # uint16的NoData值为0 # 简化:只在必要时设置波段描述(可选优化:完全移除以提升速度) # if wavelengths is not None and band_idx < len(wavelengths): # band.SetDescription(f'{wavelengths[band_idx]:.1f} nm') # 每处理100个波段显示一次进度(减少打印频率) if bands >= 100 and (band_idx + 1) % 100 == 0: print(f" 已写入 {band_idx + 1}/{bands} 个波段") print(f"✅ 数据写入完成 ({bands} 个波段)") # 创建HDR头文件(GDAL会自动创建基本的HDR,但我们需要添加更多信息) create_envi_header(hdr_file, lines, samples, bands, wavelengths, source_file) finally: # 关闭数据集 dataset = None def save_with_numpy(data: np.ndarray, bil_file: str, hdr_file: str, wavelengths: Optional[np.ndarray] = None, source_file: Optional[str] = None, source_hdr: Optional[str] = None): """ 使用numpy保存ENVI格式文件(GDAL不可用时的回退方案,优化版本) """ lines, samples, bands = data.shape print(f"💾 正在保存 {bands} 个波段的数据...") # 保存二进制数据 - uint16格式:预先转换数据类型 with open(bil_file, 'wb') as f: # 将反射率转换为uint16(乘以10000以保留4位小数精度,裁剪到有效范围) data_to_save = np.clip(data * 10000, 0, 65535).astype(np.uint16, copy=False) data_to_save.tofile(f) print(f"✅ 数据文件写入完成") # 如果有源HDR文件,直接复制 if source_hdr and Path(source_hdr).exists(): import shutil shutil.copy2(source_hdr, hdr_file) print(f"✅ 已复制源HDR文件: {Path(source_hdr).name}") else: # 创建HDR头文件 create_envi_header(hdr_file, lines, samples, bands, wavelengths, source_file) def create_envi_header(hdr_file: str, lines: int, samples: int, bands: int, wavelengths: Optional[np.ndarray] = None, source_file: Optional[str] = None): """ 创建ENVI格式的HDR头文件 """ with open(hdr_file, 'w', encoding='utf-8') as f: f.write("ENVI\n") f.write("description = {\n") f.write(" Reflectance corrected hyperspectral data\n") f.write(" Processed with Python reflectance correction}\n") f.write(f"samples = {samples}\n") f.write(f"lines = {lines}\n") f.write(f"bands = {bands}\n") f.write("header offset = 0\n") f.write("file type = ENVI Standard\n") f.write("data type = 12\n") # uint16 f.write("interleave = bsq\n") f.write("sensor type = Hyperspectral\n") f.write("byte order = 0\n") # little-endian f.write("reflectance scale factor = 10000\n") # 反射率缩放因子 # 添加波长信息 if wavelengths is not None and len(wavelengths) == bands: f.write("wavelength units = nm\n") f.write("wavelength = {\n") for i, wl in enumerate(wavelengths): f.write(f" {wl}") if i < len(wavelengths) - 1: f.write(",") if (i + 1) % 10 == 0: # 每10个波长换行 f.write("\n") f.write("}\n") # 添加波段名称 f.write("band names = {\n") for i, wl in enumerate(wavelengths): f.write(f" {wl:.1f} nm") if i < len(wavelengths) - 1: f.write(",") if (i + 1) % 8 == 0: # 每8个波段名称换行 f.write("\n") f.write("}\n") def process_single_file(hyp_file: str, wavelengths_corr: np.ndarray, corrections: np.ndarray, output_dir: str) -> bool: """ 处理单个高光谱文件 参数: ----------- hyp_file : str 高光谱文件路径 wavelengths_corr : np.ndarray 校正文件的波长 corrections : np.ndarray 校正值 output_dir : str 输出目录 返回: ----------- success : bool 处理是否成功 """ try: print(f"\n🔄 处理文件: {Path(hyp_file).name}") # 读取高光谱数据(流式) input_dataset, metadata = load_hyperspectral_data(hyp_file) try: # 获取数据波长 wavelengths_data = metadata.get('wavelengths') if wavelengths_data is None: print(f"⚠️ 跳过文件 {Path(hyp_file).name}: 缺少波长信息") return False # 判断数据波长和校正波长是否一致 wavelengths_data = np.array(wavelengths_data) if len(wavelengths_data) == len(wavelengths_corr) and np.allclose(wavelengths_data, wavelengths_corr, rtol=1e-6): # 波长完全一致,直接使用校正值 print("✅ 数据波长与校正波长完全一致,无需插值") interpolated_corrections = corrections else: # 波长不一致,需要插值 print("🔄 数据波长与校正波长不一致,进行插值") interpolated_corrections = interpolate_corrections( wavelengths_data, wavelengths_corr, corrections ) # 生成输出文件名 input_name = Path(hyp_file).stem output_file = Path(output_dir) / f"{input_name}_reflectance" # 流式保存结果(包含校正) save_corrected_data_streaming(input_dataset, interpolated_corrections, str(output_file), wavelengths_data, metadata.get('hdr_file')) finally: # 确保关闭输入数据集 input_dataset = None print(f"✅ 成功处理文件: {Path(hyp_file).name}") return True except Exception as e: print(f"❌ 处理文件失败: {Path(hyp_file).name}, 错误: {e}") return False def batch_process(hyperspectral_dir: str, correction_file: str, output_dir: str) -> Dict[str, int]: """ 批量处理文件夹中的所有高光谱文件 参数: ----------- hyperspectral_dir : str 高光谱文件文件夹 correction_file : str 校正文件路径 output_dir : str 输出目录 返回: ----------- results : dict 处理结果统计 """ print("=" * 60) print("🏁 开始高光谱反射率校正批量处理") print("=" * 60) # 检查依赖 if not GDAL_AVAILABLE: print("❌ 错误: 需要安装GDAL库") return {'total': 0, 'success': 0, 'failed': 0} # 确保输出目录存在 Path(output_dir).mkdir(parents=True, exist_ok=True) try: # 解析校正文件 print("📖 解析校正文件...") wavelengths_corr, corrections = parse_correction_file(correction_file) # 查找高光谱文件 print(f"\n📂 查找高光谱文件...") hyp_files = find_hyperspectral_files(hyperspectral_dir) if not hyp_files: print("❌ 未找到高光谱文件,处理终止") return {'total': 0, 'success': 0, 'failed': 0} print(f"找到 {len(hyp_files)} 个高光谱数据文件:") for f in hyp_files: print(f" - {Path(f).name} (需要对应的.hdr头文件)") # 处理每个文件 results = {'total': len(hyp_files), 'success': 0, 'failed': 0} for hyp_file in hyp_files: success = process_single_file(hyp_file, wavelengths_corr, corrections, output_dir) if success: results['success'] += 1 else: results['failed'] += 1 # 输出总结 print("\n" + "=" * 60) print("📊 处理完成总结:") print(f" 总文件数: {results['total']}") print(f" 成功处理: {results['success']}") print(f" 处理失败: {results['failed']}") print(f" 输出目录: {output_dir}") print("=" * 60) return results except Exception as e: print(f"❌ 批量处理失败: {e}") return {'total': 0, 'success': 0, 'failed': 0} def main(): """ 主函数 - 命令行接口 """ parser = argparse.ArgumentParser( description='高光谱反射率校正工具', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 文件要求: - 航带文件夹中应包含ENVI格式的高光谱数据文件(.bil/.bip/.bsq)和对应的头文件(.hdr) - 校正文件应为ENVI ASCII Plot File格式,包含波长和校正值两列数据 使用示例: python redlence.py /path/to/hyperspectral/folder /path/to/correction.txt python redlence.py /path/to/hyperspectral/folder /path/to/correction.txt /path/to/output/folder """ ) parser.add_argument('hyperspectral_dir', help='包含高光谱文件的文件夹路径') parser.add_argument('correction_file', help='反射率校正文件路径 (ENVI ASCII Plot File格式)') parser.add_argument('output_dir', nargs='?', default=None, help='输出目录路径 (可选,默认在输入文件夹下创建output文件夹)') args = parser.parse_args() # 设置默认输出目录 if args.output_dir is None: args.output_dir = str(Path(args.hyperspectral_dir) / 'reflectance_output') # 检查输入文件存在 if not Path(args.correction_file).exists(): print(f"❌ 校正文件不存在: {args.correction_file}") return 1 if not Path(args.hyperspectral_dir).exists(): print(f"❌ 高光谱文件夹不存在: {args.hyperspectral_dir}") return 1 # 执行批量处理 results = batch_process(args.hyperspectral_dir, args.correction_file, args.output_dir) # 返回适当的退出码 if results['success'] > 0: print("✅ 处理完成!") return 0 else: print("❌ 没有成功处理任何文件") return 1 if __name__ == "__main__": exit(main())