Spaces:
Sleeping
Sleeping
| import argparse | |
| import io | |
| import tempfile | |
| import zipfile | |
| from pathlib import Path | |
| import pandas as pd | |
| def find_gfw_zip(gfw_zip=None): | |
| """ | |
| 查找GFW压缩包。 | |
| 如果运行时指定了压缩包路径,就直接使用; | |
| 如果没有指定,就依次在脚本所在文件夹、当前运行目录、 | |
| """ | |
| if gfw_zip: | |
| path = Path(gfw_zip).expanduser() | |
| if not path.exists(): | |
| raise FileNotFoundError(f"指定的GFW压缩包不存在:{path}") | |
| return path | |
| search_dirs = [ | |
| Path(__file__).resolve().parent, | |
| Path.cwd(), | |
| Path.home() / "Desktop" / "资料下载" / "柔鱼数据", | |
| ] | |
| for folder in search_dirs: | |
| if not folder.exists(): | |
| continue | |
| files = sorted(folder.glob("GFW*.zip")) | |
| if files: | |
| return files[0] | |
| raise FileNotFoundError( | |
| "没有找到GFW压缩包。请把GFW zip文件与本脚本放在同一文件夹," | |
| "或运行时使用 --gfw-zip 指定文件路径。" | |
| ) | |
| def _open_year_zip_streaming(outer_zip, year_zip_path): | |
| year_zip_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip") | |
| try: | |
| with outer_zip.open(year_zip_path, "r") as source, year_zip_file as target: | |
| while True: | |
| chunk = source.read(4 * 1024 * 1024) | |
| if not chunk: | |
| break | |
| target.write(chunk) | |
| except Exception: | |
| year_zip_file.close() | |
| Path(year_zip_file.name).unlink(missing_ok=True) | |
| raise | |
| return zipfile.ZipFile(year_zip_file.name, "r") | |
| def read_gfw_month(year, month, gfw_zip=None): | |
| """读取指定年份和月份的GFW月尺度数据。""" | |
| if month < 1 or month > 12: | |
| raise ValueError("月份必须在1到12之间") | |
| if not 2012 <= year <= 2024: | |
| raise ValueError("年份只允许2012到2024年") | |
| gfw_zip = find_gfw_zip(gfw_zip) | |
| month_csv_name = ( | |
| f"fleet-monthly-csvs-10-v3-{year}-{month:02d}-01.csv" | |
| ) | |
| year_zip_name = f"fleet-monthly-csvs-10-v3-{year}.zip" | |
| with zipfile.ZipFile(gfw_zip, "r") as outer_zip: | |
| file_list = outer_zip.namelist() | |
| # 外层压缩包中直接存在月份CSV | |
| for file_name in file_list: | |
| if file_name.endswith(month_csv_name): | |
| with outer_zip.open(file_name) as file: | |
| return pd.read_csv(file), gfw_zip | |
| # 月份CSV位于年度压缩包中 | |
| year_zip_path = None | |
| for file_name in file_list: | |
| if file_name.endswith(year_zip_name): | |
| year_zip_path = file_name | |
| break | |
| if year_zip_path is None: | |
| raise FileNotFoundError(f"没有找到{year}年的GFW数据") | |
| with _open_year_zip_streaming(outer_zip, year_zip_path) as year_zip: | |
| for file_name in year_zip.namelist(): | |
| if file_name.endswith(month_csv_name): | |
| with year_zip.open(file_name) as file: | |
| return pd.read_csv(file), gfw_zip | |
| raise FileNotFoundError(f"没有找到{year}年{month}月的GFW数据") | |
| def query_gfw( | |
| year, | |
| month, | |
| lat_min, | |
| lat_max, | |
| lon_min, | |
| lon_max, | |
| flag=None, | |
| geartype=None, | |
| min_fishing_hours=None, | |
| gfw_zip=None, | |
| ): | |
| """ | |
| 按时间和空间范围查询GFW渔船作业努力量。 | |
| lon_min大于lon_max时,表示查询范围跨越180°经线。 | |
| 例如145°E至175°W可写为lon_min=145、lon_max=-175。 | |
| """ | |
| if lat_min > lat_max: | |
| raise ValueError("最小纬度不能大于最大纬度") | |
| data, gfw_zip = read_gfw_month( | |
| year=year, | |
| month=month, | |
| gfw_zip=gfw_zip, | |
| ) | |
| required_columns = [ | |
| "date", | |
| "year", | |
| "month", | |
| "cell_ll_lat", | |
| "cell_ll_lon", | |
| "flag", | |
| "geartype", | |
| "hours", | |
| "fishing_hours", | |
| "mmsi_present", | |
| ] | |
| missing_columns = [ | |
| column for column in required_columns | |
| if column not in data.columns | |
| ] | |
| if missing_columns: | |
| raise ValueError(f"数据中缺少必要字段:{missing_columns}") | |
| numeric_columns = [ | |
| "cell_ll_lat", | |
| "cell_ll_lon", | |
| "hours", | |
| "fishing_hours", | |
| "mmsi_present", | |
| ] | |
| invalid_value_warnings = [] | |
| for column in numeric_columns: | |
| original_non_null_count = data[column].notna().sum() | |
| data[column] = pd.to_numeric( | |
| data[column], | |
| errors="coerce", | |
| ) | |
| coerced_null_count = data[column].isna().sum() | |
| invalid_count = coerced_null_count - (len(data) - original_non_null_count) | |
| if invalid_count > 0: | |
| invalid_value_warnings.append((column, int(invalid_count))) | |
| if invalid_value_warnings: | |
| warning_parts = [ | |
| f"{column}: {count} 个值无法转换为数值" | |
| for column, count in invalid_value_warnings | |
| ] | |
| print("数值转换警告:以下列存在无法解析的脏值,已按 NaN 处理 -> " + ";".join(warning_parts)) | |
| result = data[ | |
| (data["cell_ll_lat"] >= lat_min) | |
| & (data["cell_ll_lat"] <= lat_max) | |
| ].copy() | |
| if lon_min <= lon_max: | |
| result = result[ | |
| (result["cell_ll_lon"] >= lon_min) | |
| & (result["cell_ll_lon"] <= lon_max) | |
| ] | |
| else: | |
| result = result[ | |
| (result["cell_ll_lon"] >= lon_min) | |
| | (result["cell_ll_lon"] <= lon_max) | |
| ] | |
| if flag: | |
| result = result[ | |
| result["flag"].notna() | |
| & (result["flag"].astype(str).str.upper() == flag.upper()) | |
| ] | |
| if geartype: | |
| result = result[ | |
| result["geartype"].notna() | |
| & (result["geartype"].astype(str).str.lower() == geartype.lower()) | |
| ] | |
| if min_fishing_hours is not None: | |
| result = result[ | |
| result["fishing_hours"] >= min_fishing_hours | |
| ] | |
| result = result.sort_values( | |
| by=["fishing_hours", "hours"], | |
| ascending=[False, False], | |
| ).reset_index(drop=True) | |
| summary = { | |
| "data_file": str(gfw_zip), | |
| "year": year, | |
| "month": month, | |
| "lat_range": [lat_min, lat_max], | |
| "lon_range": [lon_min, lon_max], | |
| "flag": flag if flag else "all", | |
| "geartype": geartype if geartype else "all", | |
| "record_count": len(result), | |
| "total_hours": round( | |
| result["hours"].fillna(0).sum(), | |
| 4, | |
| ), | |
| "total_fishing_hours": round( | |
| result["fishing_hours"].fillna(0).sum(), | |
| 4, | |
| ), | |
| } | |
| return result, summary | |
| def save_result(result, output_file): | |
| """保存查询结果。""" | |
| output_path = Path(output_file).expanduser() | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| result.to_csv( | |
| output_path, | |
| index=False, | |
| encoding="utf-8-sig", | |
| ) | |
| return output_path | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="查询GFW月尺度渔船作业努力量数据" | |
| ) | |
| parser.add_argument("--gfw-zip", default=None) | |
| parser.add_argument("--year", type=int, default=2012) | |
| parser.add_argument("--month", type=int, default=1) | |
| parser.add_argument("--lat-min", type=float, default=-10) | |
| parser.add_argument("--lat-max", type=float, default=10) | |
| parser.add_argument("--lon-min", type=float, default=145) | |
| parser.add_argument("--lon-max", type=float, default=-175) | |
| parser.add_argument("--flag", default=None) | |
| parser.add_argument("--geartype", default=None) | |
| parser.add_argument("--min-fishing-hours", type=float, default=None) | |
| parser.add_argument("--output", default="GFW查询结果.csv") | |
| args = parser.parse_args() | |
| if args.year is not None and not 2012 <= args.year <= 2024: | |
| raise ValueError("年份只允许2012到2024年") | |
| if args.month is not None and not 1 <= args.month <= 12: | |
| raise ValueError("月份只允许1到12月") | |
| result, summary = query_gfw( | |
| year=args.year, | |
| month=args.month, | |
| lat_min=args.lat_min, | |
| lat_max=args.lat_max, | |
| lon_min=args.lon_min, | |
| lon_max=args.lon_max, | |
| flag=args.flag, | |
| geartype=args.geartype, | |
| min_fishing_hours=args.min_fishing_hours, | |
| gfw_zip=args.gfw_zip, | |
| ) | |
| print("\n查询汇总") | |
| print("-" * 40) | |
| for key, value in summary.items(): | |
| print(f"{key}: {value}") | |
| print("\n捕捞小时数最高的前10条记录") | |
| print("-" * 40) | |
| if result.empty: | |
| print("没有查询到符合条件的数据") | |
| else: | |
| print(result.head(10).to_string(index=False)) | |
| output_path = save_result(result, args.output) | |
| print("\n结果已保存到:") | |
| print(output_path) | |
| if __name__ == "__main__": | |
| main() | |