#! /usr/bin/env python3 # -*- coding: utf-8 -*- ########################################################################### # Header information ########################################################################### """API_download_S1_SLC.py: Script to download Sentinel-1 SLC images from the ASF mirror Supports both bounding box and Shapefile for spatial filtering""" __author__ = "Alexis Hrysiewicz" __copyright__ = "Copyright 2022" __credits__ = ["Alexis Hrysiewicz"] __license__ = "GPL" __version__ = "2.0.0" __maintainer__ = "Alexis Hrysiewicz" __status__ = "Production" __date__ = "Jan. 2022" ########################################################################### # Python packages ########################################################################### import os import sys import pandas as pd import datetime import os.path import optparse # 尝试导入 geopandas 和 shapely 用于 Shapefile 支持 try: import geopandas as gpd from shapely.geometry import box, Polygon, MultiPolygon HAS_SHAPEFILE_SUPPORT = True except ImportError: HAS_SHAPEFILE_SUPPORT = False print("Warning: geopandas/shapely not installed. Shapefile support disabled.") print("Install with: pip install geopandas shapely") ########################################################################### ########################################################################### class OptionParser (optparse.OptionParser): def check_required(self, opt): option = self.get_option(opt) if getattr(self.values, option.dest) is None: self.error("%s option not supplied" % option) def bbox_from_shapefile(shapefile_path): """ 从 Shapefile 文件提取边界框 Args: shapefile_path: Shapefile 文件路径(.shp) Returns: 边界框字符串格式: "min_lon,min_lat,max_lon,max_lat" """ try: # 读取 Shapefile gdf = gpd.read_file(shapefile_path) # 计算总边界框 total_bounds = gdf.total_bounds # total_bounds = (minx, miny, maxx, maxy) # 转换为字符串格式 bbox_str = f"{total_bounds[0]},{total_bounds[1]},{total_bounds[2]},{total_bounds[3]}" print(f"Shapefile bounding box: {bbox_str}") return bbox_str except Exception as e: print(f"Error reading shapefile: {str(e)}") return None def check_slc_exists(slc_name, path_SLC, path_RSLC, acquisition_date): """ 检查 SLC 文件是否已存在 Args: slc_name: SLC 文件名 path_SLC: SLC 存储路径 path_RSLC: RSLC 存储路径 acquisition_date: 采集日期字符串 Returns: bool: 文件是否已存在 """ # 检查 .zip 文件 if os.path.exists(os.path.join(path_SLC, slc_name + '.zip')): return True # 检查 .rslc 文件 try: datei = datetime.datetime.strptime(acquisition_date.split('.')[0], '%Y-%m-%dT%H:%M:%S').strftime("%Y%m%d") if os.path.exists(os.path.join(path_RSLC, datei + '.vv.rslc')): return True except: pass return False def read_netrc(machine_name): """ 从 ~/.netrc 文件读取登录凭证 Args: machine_name: 机器名称 (例如: urs.earthdata.nasa.gov) Returns: tuple: (username, password) 或 (None, None) 如果未找到 """ netrc_path = os.path.expanduser('~/.netrc') if not os.path.exists(netrc_path): return None, None try: with open(netrc_path, 'r') as f: lines = f.readlines() i = 0 while i < len(lines): line = lines[i].strip() # 查找 machine 行 if line.startswith('machine ' + machine_name): username = None password = None # 查找接下来的几行中的 login 和 password j = i + 1 while j < len(lines) and j < i + 5: # 最多查找5行 next_line = lines[j].strip() tokens = next_line.split() if len(tokens) >= 2: if tokens[0] == 'login': username = tokens[1] elif tokens[0] == 'password': password = tokens[1] # 如果找到下一个 machine,停止查找 if next_line.startswith('machine '): break j += 1 if username and password: return username, password i += 1 except Exception as e: print(f"Warning: Error reading .netrc file: {str(e)}") return None, None def download_slc(url, username, password, path_SLC): """ 下载单个 SLC 文件 Args: url: 下载 URL username: ASF 用户名 password: ASF 密码 path_SLC: 存储路径 Returns: bool: 下载是否成功 """ # 如果提供了用户名和密码,使用它们 if username and password: cmd = f'wget -c --http-user={username} --http-password={password} "{url}" -P {path_SLC}' else: # 否则使用 .netrc 文件 # --auth-no-challenge: 立即发送认证信息,不等待服务器挑战 # --netrc: 从 ~/.netrc 读取认证信息 cmd = f'wget -c --auth-no-challenge --netrc "{url}" -P {path_SLC}' return os.system(cmd) == 0 def convert_yyyymmdd_to_iso(date_str): """ 将 YYYYMMDD 格式转换为 ISO 格式 (YYYY-MM-DDTHH:MM:SS) Args: date_str: 日期字符串 (YYYYMMDD) Returns: str: ISO 格式日期字符串 (YYYY-MM-DDT00:00:00) """ if len(date_str) == 8: year = date_str[0:4] month = date_str[4:6] day = date_str[6:8] return f"{year}-{month}-{day}T00:00:00" else: # 如果已经是其他格式,直接返回 return date_str ########################################################################### ########################################################################### if len(sys.argv) < 3: prog = os.path.basename(sys.argv[0]) print("="*80) print("Sentinel-1 SLC Downloader from ASF") print("="*80) print("\nUsage examples:") print("\n1. Using bounding box:") print(" python3 " + prog + " -u username -p password -s . -r . \\") print(" -b -10.78,51.27,-5.03,55.70 \\") print(" -i 20170101 -j 20231231 \\") print(f" -o 1 -f a -q IW -m csv -w n") print("\n2. Using Shapefile (new feature):") print(" python3 " + prog + " -u username -p password -s . -r . \\") print(" --shp study_area.shp \\") print(" -i 20170101 -j 20231231 \\") print(f" -o 1 -f a -q IW -m csv -w n") print("\nOptions:") print(" -u, --username ASF username (optional, can use .netrc)") print(" -p, --password ASF password (optional, can use .netrc)") print(" -n, --netrc Machine name in ~/.netrc (default: urs.earthdata.nasa.gov)") print(" -s, --path_SLC Output directory for SLC files") print(" -r, --path_RSLC Directory to check for processed RSLC files") print(" -b, --bbox Bounding box (min_lon,min_lat,max_lon,max_lat)") print(" --shp Shapefile path (.shp) for spatial filtering") print(" -i, --date_start Start date (YYYYMMDD)") print(" -j, --date_end End date (YYYYMMDD)") print(" -o, --orbit_relative Relative orbit number (optional)") print(" --frame Frame number (optional)") print(" -f, --flight_direction Flight direction (a=ascending, d=descending, or all)") print(" -q, --q_acqui_mode Acquisition mode (IW, EW, SM, or all)") print(" -m, --mode Output format (csv or kml)") print(" -w, --write Download files? (Y/N)") print(" --parallel Number of parallel downloads (default: 1, serial)") print("\nNote: Shapefile support requires geopandas and shapely") print("="*80) sys.exit(-1) else: usage = "usage: %prog [options] " parser = OptionParser(usage=usage) parser.add_option("-u", "--username", action="store", type="string", default=None, help="ASF username (can be read from ~/.netrc)") parser.add_option("-p", "--password", action="store", type="string", default=None, help="ASF password (can be read from ~/.netrc)") parser.add_option("-n", "--netrc", action="store", type="string", default='urs.earthdata.nasa.gov', help="Machine name in ~/.netrc file (default: urs.earthdata.nasa.gov)") parser.add_option("-s", "--path_SLC", action="store", type="string", default='.') parser.add_option("-r", "--path_RSLC", action="store", type="string", default='.') # Only available for GAMMA stack parser.add_option("-b", "--bbox", action="store", type="string", default='-10.78,51.27,-5.03,55.70') parser.add_option("--shp", "--shapefile", action="store", type="string", default=None, help="Shapefile path for spatial filtering") parser.add_option("-i", "--date_start", action="store", type="string", default='20170101') parser.add_option("-j", "--date_end", action="store", type="string", default='20240101') parser.add_option("-o", "--orbit_relative", action="store", type="float", default=None, help="Relative orbit number (optional, if not specified will search all orbits)") parser.add_option("--frame", action="store", type="int", default=None, help="Frame number (optional, if not specified will search all frames)") parser.add_option("-f", "--flight_direction", action="store", type="string", default='a', help="Flight direction (a=ascending, d=descending, or 'all' for both)") parser.add_option("-q", "--q_acqui_mode", action="store", type="string", default='IW') parser.add_option("-m", "--mode", action="store", type="string", default='csv') parser.add_option("-w", "--write", action="store", type="string", default='n') parser.add_option("--parallel", action="store", type="int", default=1, help="Number of parallel downloads (default: 1, serial)") (options, args) = parser.parse_args() ########################################################################### # Main ########################################################################### date_format = "%Y-%m-%d" # 转换日期格式 date_start_iso = convert_yyyymmdd_to_iso(options.date_start) date_end_iso = convert_yyyymmdd_to_iso(options.date_end) # 处理用户名和密码:如果未提供,尝试从 .netrc 读取 if options.username is None or options.password is None: netrc_username, netrc_password = read_netrc(options.netrc) if netrc_username and netrc_password: if options.username is None: options.username = netrc_username print(f"Username read from ~/.netrc ({options.netrc})") if options.password is None: options.password = netrc_password print(f"Password read from ~/.netrc ({options.netrc})") else: if options.username is None: print("Error: Username not provided and not found in ~/.netrc") print("Please provide username with -u option or configure ~/.netrc") sys.exit(1) if options.password is None: print("Error: Password not provided and not found in ~/.netrc") print("Please provide password with -p option or configure ~/.netrc") sys.exit(1) # 处理空间范围:优先使用 Shapefile,否则使用 bounding box if options.shp: if not HAS_SHAPEFILE_SUPPORT: print("Error: Shapefile support requires geopandas and shapely") print("Install with: pip install geopandas shapely") sys.exit(1) print(f"Using Shapefile: {options.shp}") bbox = bbox_from_shapefile(options.shp) if bbox is None: print("Error: Failed to extract bounding box from shapefile") sys.exit(1) else: bbox = options.bbox print(f"Using bounding box: {bbox}") # 构建 API 查询命令 # 轨道号、frame 和飞行方向都是可选的 api_params = f"platform=s1&bbox={bbox}&start={date_start_iso}-UTC&end={date_end_iso}-UTC&processingLevel=SLC&maxResults=10000" # 如果指定了轨道号,添加到查询参数 if options.orbit_relative is not None: api_params += f"&relativeOrbit={int(options.orbit_relative)}" # 如果指定了 frame 号,添加到查询参数 if hasattr(options, 'frame') and options.frame is not None: api_params += f"&frame={options.frame}" # 如果指定了飞行方向且不是 'all',添加到查询参数 if options.flight_direction and options.flight_direction.upper() != 'ALL': api_params += f"&flightDirection={options.flight_direction.upper()}" # 添加输出格式 api_params += f"&output={options.mode.upper()}" cmd1 = f'curl "https://api.daac.asf.alaska.edu/services/search/param?{api_params}" > SLC_list.{options.mode.lower()}' print(f"Querying ASF API for SLC data...") print(f" Bounding Box: {bbox}") print(f" Date Range: {options.date_start} to {options.date_end}") if options.orbit_relative is not None: print(f" Orbit: {options.orbit_relative}") else: print(f" Orbit: All") if hasattr(options, 'frame') and options.frame is not None: print(f" Frame: {options.frame}") else: print(f" Frame: All") if options.flight_direction and options.flight_direction.upper() != 'ALL': print(f" Flight Direction: {options.flight_direction.upper()}") else: print(f" Flight Direction: All") print(f" Mode: {options.q_acqui_mode.upper()}") print() # 执行查询 if options.mode.upper() == 'KML': os.system(cmd1) elif options.mode.upper() == 'CSV': os.system(cmd1) else: print('Error: Please select a correct mode (csv or kml)...') sys.exit(1) # 根据波束模式过滤列表 print(f"Filtering results by acquisition mode: {options.q_acqui_mode.upper()}") if os.path.exists("SLC_list.csv"): os.rename("SLC_list.csv","SLC_list_orig.csv") h = 0 fout = open("SLC_list.csv",'w') total_count = 0 matched_count = 0 with open("SLC_list_orig.csv") as fi: for li in fi: total_count += 1 if h > 0: if options.q_acqui_mode.upper() == 'all': fout.write(li) matched_count += 1 elif options.q_acqui_mode.upper() in li: fout.write(li) matched_count += 1 else: fout.write(li) h = h + 1 fout.close() os.remove("SLC_list_orig.csv") print(f" Total results: {total_count - 1}") print(f" Filtered results: {matched_count}") else: print("Warning: SLC_list.csv not found") sys.exit(1) # 下载文件 if options.mode.upper() == 'CSV' and options.write.upper() == 'Y': try: listSLC = pd.read_csv("SLC_list.csv") except Exception as e: print(f"Error reading SLC_list.csv: {str(e)}") sys.exit(1) print(f"\nFound {len(listSLC)} SLC files to process") print("="*80) # 创建输出目录 if not os.path.exists(options.path_SLC): os.makedirs(options.path_SLC) print(f"Created output directory: {options.path_SLC}") # 检查是否有并行参数 parallel_numb = getattr(options, 'parallel', 1) if parallel_numb is None: parallel_numb = 1 # 准备下载数据列表 download_tasks = [] skipped_tasks = [] for h, slci in enumerate(listSLC['Granule Name']): url = listSLC['URL'][h] acquisition_date = listSLC['Acquisition Date'][h] # 检查文件是否已存在 if check_slc_exists(slci, options.path_SLC, options.path_RSLC, acquisition_date): skipped_tasks.append((h+1, slci, "Already exists")) else: download_tasks.append((h+1, slci, url, acquisition_date)) print(f"Files to download: {len(download_tasks)}") print(f"Files skipped (already exists): {len(skipped_tasks)}") # 并行下载函数 def download_worker(task): idx, slci, url, acquisition_date = task print(f"\n[{idx}/{len(listSLC)}] Downloading: {slci}") if download_slc(url, options.username, options.password, options.path_SLC): print(f" Status: Download SUCCESS") return (idx, slci, "SUCCESS") else: print(f" Status: Download FAILED") return (idx, slci, "FAILED") # 执行下载 downloaded = 0 failed = 0 if parallel_numb > 1 and len(download_tasks) > 0: # 并行下载 from multiprocessing.pool import ThreadPool print(f"\nStarting parallel download with {parallel_numb} workers...") pool = ThreadPool(parallel_numb) results = pool.map(download_worker, download_tasks) pool.close() pool.join() # 统计结果 for result in results: if result[2] == "SUCCESS": downloaded += 1 else: failed += 1 elif len(download_tasks) > 0: # 串行下载 print("\nStarting serial download...") for task in download_tasks: result = download_worker(task) if result[2] == "SUCCESS": downloaded += 1 else: failed += 1 # 打印摘要 print("\n" + "="*80) print("DOWNLOAD SUMMARY") print("="*80) print(f"Total SLC files: {len(listSLC)}") print(f"Downloaded: {downloaded}") print(f"Skipped (already exists): {len(skipped_tasks)}") print(f"Failed: {failed}") print("="*80) if failed > 0: print(f"\nWarning: {failed} downloads failed. Check the output above for details.") sys.exit(1) elif options.mode.upper() == 'KML' and options.write.upper() == 'Y': print('Please, select CSV mode to download..') elif options.write.upper() != 'Y': print(f"\nSLC list saved to: SLC_list.csv") print("To download files, re-run with -w Y option")