SHP数据清洗太耗时?GeoPandas批量处理实战(附:完整脚本)

GIS基础理论
Dr.GIS
wowwwai GIS研习社 · 工具流程与项目排障

如果你正在为“SHP数据清洗太耗时?GeoPandas批量处理实战(附:完整脚本)”这个问题头疼,通常说明你的工作已经不再是清洗一个 Shapefile,而是几十个甚至几百个 SHP 文件反复做字段整理、几何修复、坐标系统一和格式输出。本文用 GeoPandas 给出一套可复用的 SHP数据清洗流程,重点解决批量处理、规则统一、结果可检查这三个实际问题。

引言:为什么 SHP数据清洗 很容易变成重复劳动

在 GIS 项目中,SHP 数据常见来源包括自然资源、规划、测绘、第三方项目成果和历史资料归档。看起来都是 Shapefile,但实际打开后经常会遇到这些情况:

  • 字段名称不统一,同一个含义可能叫 name、NAME、名称。
  • 存在空几何、无效几何、自相交面、重复要素。
  • 坐标系缺失或坐标系不一致。
  • 属性字段过多,且包含无用字段。
  • 中文字段或中文属性在不同软件中出现乱码。
  • 多个目录下的 SHP 文件需要统一清洗后再入库或制图。

如果全部依赖 QGIS 或 ArcGIS Pro 手工处理,少量数据没问题;但当数据数量增加后,手工操作不仅慢,还容易因为某一步漏做而导致成果不一致。GeoPandas批量处理的优势就在于把清洗规则写成脚本,让每个 SHP 都按同一套规则处理。

SHP数据清洗 GeoPandas批量处理流程图
GeoPandas 批量清洗 SHP 的推荐流程:先检查,再修复,最后统一输出。

背景:本文要解决的具体清洗场景

本文假设你有一个文件夹,里面存放了多个 SHP 数据,每个 SHP 代表一类矢量数据,例如地块、道路、管线、行政区或兴趣点。你的目标是使用 GeoPandas 完成以下 SHP数据清洗任务:

  • 批量读取指定目录及子目录中的 .shp 文件。
  • 统一检查和设置坐标系。
  • 修复无效几何并删除空几何。
  • 统一字段名,删除无用字段。
  • 处理重复要素。
  • 输出到清洗后的目录。
  • 生成一个简单的清洗日志,方便核查每个文件处理了什么。

这个流程适合 GIS 学生、初级 GIS 工程师、空间数据分析人员,以及需要把 SHP 数据整理后导入 PostGIS、ArcGIS Pro 或 WebGIS 平台的用户。

原理:GeoPandas清洗SHP到底在做什么

GeoPandas 是 Python 生态中常用的矢量 GIS 数据处理库。它把空间数据读成 GeoDataFrame,可以理解为“带几何字段的表格”。属性字段像 Pandas 表格一样处理,空间几何则由 Shapely 负责计算和修复。

一次完整的 SHP数据清洗通常包含三类操作:

  • 属性清洗:整理字段名、删除空字段、统一编码、去除重复记录。
  • 几何清洗:删除空几何、修复无效几何、统一几何类型。
  • 空间参考清洗:检查 CRS,也就是坐标参考系统,并根据项目要求统一投影。

需要特别注意:GeoPandas 不是简单地替代桌面 GIS 软件,而是适合把确定性的、重复性的清洗规则自动化。对于需要人工判断的地物分类、拓扑关系认定、边界取舍,仍然建议结合 QGIS 或 ArcGIS Pro 进行抽查。

步骤:GeoPandas批量处理SHP数据的完整脚本

1. 准备 Python 环境

建议使用 Conda 环境安装 GeoPandas,因为它依赖 GDAL、Fiona、pyproj、Shapely 等底层 GIS 库。安装命令如下:

conda create -n gpd_clean python=3.11
conda activate gpd_clean
conda install -c conda-forge geopandas pyogrio shapely pandas

如果你使用 pip,也可以安装:

pip install geopandas pyogrio shapely pandas

在 Windows 环境下,如果 pip 安装 GDAL 相关依赖失败,优先使用 conda-forge,可以减少环境问题。

2. 推荐的目录结构

为了便于批量处理,建议按下面的方式组织文件:

project/
  input_shp/
    roads.shp
    parcels.shp
    village/
      boundary.shp
  output_clean/
  logs/

input_shp 放原始数据,output_clean 放清洗结果,logs 放清洗日志。不要直接覆盖原始 SHP 文件,这是数据清洗中最重要的安全习惯之一。

3. 完整脚本:批量清洗 SHP

下面脚本可以直接保存为 batch_clean_shp.py。运行前请把 INPUT_DIR、OUTPUT_DIR 和 TARGET_CRS 改成你的实际路径和目标坐标系。

from pathlib import Path
import pandas as pd
import geopandas as gpd

INPUT_DIR = Path(r"D:/gis_project/input_shp")
OUTPUT_DIR = Path(r"D:/gis_project/output_clean")
LOG_DIR = Path(r"D:/gis_project/logs")

TARGET_CRS = "EPSG:4547"

DROP_FIELDS = [
    "OBJECTID",
    "FID",
    "Shape_Leng",
    "Shape_Area",
    "备注1",
    "临时字段"
]

RENAME_FIELDS = {
    "NAME": "name",
    "Name": "name",
    "名称": "name",
    "类型": "type",
    "类别": "type"
}

OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
LOG_DIR.mkdir(parents=True, exist_ok=True)

log_rows = []

def clean_field_names(gdf):
    gdf = gdf.rename(columns=RENAME_FIELDS)

    new_columns = {}
    for col in gdf.columns:
        if col == gdf.geometry.name:
            continue
        new_col = str(col).strip()
        new_col = new_col.replace(" ", "_")
        new_col = new_col.replace("-", "_")
        new_col = new_col.lower()
        new_columns[col] = new_col

    gdf = gdf.rename(columns=new_columns)
    return gdf

def drop_unused_fields(gdf):
    existing_drop_fields = [c for c in DROP_FIELDS if c in gdf.columns]
    existing_drop_fields += [c.lower() for c in DROP_FIELDS if c.lower() in gdf.columns]
    existing_drop_fields = list(set(existing_drop_fields))
    if existing_drop_fields:
        gdf = gdf.drop(columns=existing_drop_fields)
    return gdf, existing_drop_fields

def repair_geometry(gdf):
    before_count = len(gdf)

    gdf = gdf[~gdf.geometry.isna()].copy()
    gdf = gdf[~gdf.geometry.is_empty].copy()

    empty_removed = before_count - len(gdf)

    invalid_before = int((~gdf.geometry.is_valid).sum())

    if invalid_before > 0:
        gdf["geometry"] = gdf.geometry.make_valid()

    invalid_after = int((~gdf.geometry.is_valid).sum())

    return gdf, empty_removed, invalid_before, invalid_after

def handle_crs(gdf, target_crs):
    original_crs = gdf.crs

    if gdf.crs is None:
        gdf = gdf.set_crs(target_crs, allow_override=True)
        crs_action = "missing_crs_set_to_target"
    elif str(gdf.crs) != target_crs:
        gdf = gdf.to_crs(target_crs)
        crs_action = "reprojected_to_target"
    else:
        crs_action = "already_target_crs"

    return gdf, original_crs, crs_action

def remove_duplicates(gdf):
    before_count = len(gdf)

    non_geom_cols = [c for c in gdf.columns if c != gdf.geometry.name]
    if non_geom_cols:
        gdf["_geom_wkt"] = gdf.geometry.to_wkt()
        subset_cols = non_geom_cols + ["_geom_wkt"]
        gdf = gdf.drop_duplicates(subset=subset_cols).copy()
        gdf = gdf.drop(columns=["_geom_wkt"])
    else:
        gdf["_geom_wkt"] = gdf.geometry.to_wkt()
        gdf = gdf.drop_duplicates(subset=["_geom_wkt"]).copy()
        gdf = gdf.drop(columns=["_geom_wkt"])

    duplicate_removed = before_count - len(gdf)
    return gdf, duplicate_removed

def safe_output_path(input_path):
    relative_path = input_path.relative_to(INPUT_DIR)
    output_path = OUTPUT_DIR / relative_path
    output_path.parent.mkdir(parents=True, exist_ok=True)
    return output_path

def clean_one_shp(shp_path):
    print(f"正在处理:{shp_path}")

    try:
        gdf = gpd.read_file(shp_path, engine="pyogrio")

        input_count = len(gdf)
        input_fields = len(gdf.columns)

        gdf = clean_field_names(gdf)
        gdf, dropped_fields = drop_unused_fields(gdf)
        gdf, empty_removed, invalid_before, invalid_after = repair_geometry(gdf)
        gdf, original_crs, crs_action = handle_crs(gdf, TARGET_CRS)
        gdf, duplicate_removed = remove_duplicates(gdf)

        output_path = safe_output_path(shp_path)

        gdf.to_file(output_path, encoding="utf-8", engine="pyogrio")

        log_rows.append({
            "file": str(shp_path),
            "status": "success",
            "input_count": input_count,
            "output_count": len(gdf),
            "input_fields": input_fields,
            "output_fields": len(gdf.columns),
            "dropped_fields": ",".join(dropped_fields),
            "empty_removed": empty_removed,
            "invalid_before": invalid_before,
            "invalid_after": invalid_after,
            "duplicate_removed": duplicate_removed,
            "original_crs": str(original_crs),
            "crs_action": crs_action,
            "output": str(output_path),
            "error": ""
        })

    except Exception as e:
        log_rows.append({
            "file": str(shp_path),
            "status": "failed",
            "input_count": "",
            "output_count": "",
            "input_fields": "",
            "output_fields": "",
            "dropped_fields": "",
            "empty_removed": "",
            "invalid_before": "",
            "invalid_after": "",
            "duplicate_removed": "",
            "original_crs": "",
            "crs_action": "",
            "output": "",
            "error": str(e)
        })

def main():
    shp_files = list(INPUT_DIR.rglob("*.shp"))

    if not shp_files:
        print("没有找到 SHP 文件,请检查 INPUT_DIR。")
        return

    for shp_path in shp_files:
        clean_one_shp(shp_path)

    log_df = pd.DataFrame(log_rows)
    log_path = LOG_DIR / "shp_clean_log.csv"
    log_df.to_csv(log_path, index=False, encoding="utf-8-sig")

    print(f"处理完成,共处理 {len(shp_files)} 个 SHP。")
    print(f"日志已输出:{log_path}")

if __name__ == "__main__":
    main()

4. 运行脚本

进入脚本所在目录后执行:

python batch_clean_shp.py

运行完成后,重点检查两个位置:

  • output_clean:是否生成了清洗后的 SHP 文件。
  • logs/shp_clean_log.csv:每个文件是否成功处理,删除了多少空几何、无效几何和重复要素。

5. 如何验证清洗结果

SHP批量处理完成后,不要直接把结果交付或入库。建议按下面步骤验证:

  1. 用 QGIS 或 ArcGIS Pro 打开清洗后的 SHP,确认图层能正常加载。
  2. 检查图层坐标是否落在正确位置,不应出现飞到国外或海上的情况。
  3. 打开属性表,查看字段是否按预期重命名和删除。
  4. 使用选择工具检查是否存在空几何或明显异常图形。
  5. 随机抽取几个文件,与原始数据对比要素数量变化是否合理。
  6. 查看 shp_clean_log.csv 中 status 是否全部为 success。

常见坑:SHP数据清洗中最容易出错的地方

1. 坐标系缺失不等于可以随便指定

脚本中如果发现 CRS 为空,会使用 set_crs 指定为目标坐标系。但这一步本质上是“声明坐标系”,不是“投影转换”。如果原始数据实际是 CGCS2000 经纬度坐标,却被你强行设为投影坐标,结果一定会错。

正确做法是:先通过数据来源、坐标数值范围、元数据或同区域参考图层判断原始坐标系,再决定是否 set_crs 或 to_crs。

2. Shapefile 字段名长度有限制

SHP 的 DBF 属性表对字段名长度有历史限制,很多软件仍按 10 个字符处理。因此,如果你希望保留较长字段名,更建议输出为 GeoPackage,而不是继续输出 SHP。

例如可以把输出部分改为:

output_path = output_path.with_suffix(".gpkg")
gdf.to_file(output_path, layer=shp_path.stem, driver="GPKG", engine="pyogrio")

3. 中文编码问题不能只靠 encoding 解决

脚本中输出使用了 encoding="utf-8",但一些旧版软件或历史数据可能使用 GBK、GB2312 或其他编码。如果读取时字段乱码,可以尝试:

gdf = gpd.read_file(shp_path, encoding="gbk")

如果一个项目里数据来源混杂,建议先抽样检查编码,再分批处理,不要盲目套用一个编码参数。

4. make_valid 可能改变几何类型

make_valid 可以修复很多无效几何,但修复后可能出现 GeometryCollection、MultiPolygon 等结果。如果你的下游系统只接受 Polygon,需要在清洗后增加几何类型过滤或转换逻辑。

5. 删除重复要素前要确认业务规则

本文脚本使用属性和几何共同判断重复。实际项目中,有些要素几何相同但属性不同,可能并不是重复,而是不同权属、不同时间或不同业务分类。做 GeoPandas清洗SHP 时,不建议不加判断地删除所有几何重复项。

方法比较:手工清洗、模型工具和 GeoPandas 哪个更适合

方法 适合场景 优点 限制
QGIS 或 ArcGIS Pro 手工清洗 少量数据、需要人工判断的图形编辑 可视化直观,适合检查细节 批量重复操作效率低,容易漏步骤
ArcGIS ModelBuilder 或 QGIS 处理模型 固定流程、团队内软件环境一致 图形化流程清晰,便于非代码用户使用 复杂字段规则和日志控制不如脚本灵活
GeoPandas批量处理 大量 SHP 文件、规则明确、需要可复现 自动化程度高,便于记录日志和二次开发 需要 Python 基础,复杂拓扑修复仍需人工复核
PostGIS 入库后清洗 数据量大、需要多人协作和空间 SQL 查询 适合数据库化管理和空间索引查询 前期入库和数据库维护成本更高

如果你的目标是快速清洗一两个图层,桌面 GIS 更直接;如果你面对的是一批结构相似的 SHP 文件,GeoPandas批量处理更稳定;如果数据最终要进入数据库,建议先用脚本做基础清洗,再导入 PostGIS 做空间索引和高级质检。

检查清单:批量清洗前后必须确认的事项

清洗前检查

  • 是否备份了原始 SHP 数据。
  • 是否确认了原始数据的真实坐标系。
  • 是否明确哪些字段要保留、重命名或删除。
  • 是否知道下游系统需要 SHP、GeoPackage 还是 PostGIS。
  • 是否抽样打开过原始数据,确认不是损坏文件。

清洗中检查

  • 脚本是否只读取输入目录,不覆盖原始数据。
  • 日志是否记录处理成功和失败原因。
  • 无效几何修复后是否还有未修复对象。
  • 重复删除规则是否符合业务含义。
  • 字段名是否会被 SHP 格式截断。

清洗后检查

  • 输出图层能否在 QGIS 或 ArcGIS Pro 中正常打开。
  • 图层位置是否与参考底图或已有数据吻合。
  • 要素数量变化是否合理。
  • 关键字段是否仍然存在。
  • 中文属性是否正常显示。
  • 日志中是否存在 failed 记录。

FAQ:关于 SHP数据清洗 和 GeoPandas 的常见问题

1. GeoPandas 可以批量处理所有 SHP 文件吗?

大多数标准 SHP 文件都可以处理。但如果 SHP 配套文件缺失,例如缺少 .shx、.dbf 或 .prj,读取可能失败或坐标系无法识别。SHP数据清洗前建议先检查文件完整性。

2. GeoPandas批量处理比 ArcGIS Pro 更快吗?

这取决于数据量、磁盘速度、几何复杂度和清洗规则。GeoPandas 的优势不是绝对速度,而是自动化和可复现。对于大量重复任务,它能减少人工点击和漏操作。

3. 为什么清洗后的 SHP 字段名变短了?

这是 Shapefile 格式自身限制导致的。SHP 的属性表来自 DBF,字段名长度支持有限。如果你需要完整字段名、更多字段类型和更稳定的编码,建议输出为 GeoPackage。

4. 坐标系为空时,脚本自动设置 TARGET_CRS 是否安全?

不一定安全。坐标系为空只表示文件没有记录 CRS,并不代表它一定就是目标坐标系。只有当你确认原始坐标就是 TARGET_CRS 时,才能使用 set_crs。否则需要先找到真实坐标系再转换。

5. GeoPandas清洗SHP 能修复所有拓扑错误吗?

不能。make_valid 可以修复常见无效几何,但无法替你判断边界是否正确、缝隙是否应该合并、重叠是否符合业务规则。涉及权属、规划边界、宗地边界等数据时,仍需人工复核。

6. 清洗结果应该继续保存为 SHP 吗?

如果只是交给要求 SHP 的单位,可以继续输出 SHP。如果用于长期管理、字段较多、中文属性较多,建议保存为 GeoPackage 或导入 PostGIS。SHP 是通用格式,但并不是最适合复杂数据管理的格式。

结论:把重复的 SHP数据清洗 变成可复用流程

SHP数据清洗最耗时的地方,不是某一个工具按钮,而是同样的检查和修复要在大量文件上重复执行。通过 GeoPandas批量处理,你可以把字段标准化、几何修复、坐标系统一、重复检查和日志输出整合成一套脚本流程。

实际项目中,建议遵循一个简单原则:先用脚本完成规则明确的基础清洗,再用 QGIS 或 ArcGIS Pro 对关键图层进行抽查。如果数据最终要进入数据库或 WebGIS 平台,再根据需要转换为 GeoPackage、PostGIS 或其他更适合管理和发布的格式。

这套完整脚本可以作为 SHP批量处理的起点。你只需要根据项目规范调整字段映射、目标坐标系、输出格式和重复判断规则,就能把原来大量手工点击的工作,变成稳定、可记录、可复查的自动化流程。