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

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

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

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

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

  • 字段名称不统一,同一个含义可能叫 nameNAME名称
  • 存在空几何、无效几何、自相交面、重复要素。
  • 坐标系缺失或坐标系不一致。
  • 属性字段过多,且包含无用字段。
  • 中文字段或中文属性在不同软件中出现乱码。
  • 多个目录下的 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_DIROUTPUT_DIRTARGET_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.csvstatus 是否全部为 success

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

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

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

正确做法是:先通过数据来源、坐标数值范围、元数据或同区域参考图层判断原始坐标系,再决定是否 set_crsto_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批量处理的起点。你只需要根据项目规范调整字段映射、目标坐标系、输出格式和重复判断规则,就能把原来大量手工点击的工作,变成稳定、可记录、可复查的自动化流程。