File size: 8,862 Bytes
0ed9c08
 
5d4bed3
0ed9c08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d4bed3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed9c08
 
 
 
5d4bed3
 
0ed9c08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d4bed3
 
 
 
 
0ed9c08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d4bed3
0ed9c08
5d4bed3
0ed9c08
 
 
 
5d4bed3
 
 
 
 
 
 
 
 
 
 
0ed9c08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d4bed3
 
0ed9c08
 
 
 
5d4bed3
 
0ed9c08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d4bed3
 
 
 
 
 
0ed9c08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
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()