4. 批量读取多种格式的地质矢量数据

地质矢量数据在实际项目中很少只有一种格式。一个区域地质调查项目里,可能同时存在:从地质云下载的 1:20 万图幅 Shapefile(.shp/.shx/.dbf/.prj 一组文件)、从在线服务导出的 GeoJSON 断层线、以及用 QGIS 或 ArcGIS Pro 整理后保存的 GeoPackage(.gpkg)多图层数据库。批处理的第一步,就是把这些异构文件统一读入为 GeoDataFrame,并让每个结果都带上“来源路径”这一可追溯信息。本节围绕三件事展开:格式识别与读取、通配符与路径遍历、读取异常处理。

一、格式识别:先看扩展名,再看驱动

GeoPandas 的 gpd.read_file() 底层依赖 pyogrio(或 fiona)驱动,能自动根据扩展名选择驱动。常见地质矢量格式的读取要点如下:

格式典型扩展名读取注意点
Shapefile.shp必须与 .shx/.dbf/.prj 同目录同名;只传 .shp 路径即可;字段名限 10 字符,中文易被截断
GeoJSON.geojson / .json单文件单图层;坐标默认 WGS84;大文件解析较慢
GeoPackage.gpkg一个文件可含多图层,需用 layer= 指定;不指定时读第一个图层
KML / GML.kml / .gml需相应驱动;KML 常含样式与多几何类型

关键点在于:Shapefile 是“一组文件”而非“一个文件”。遍历目录时如果按所有文件收集,会把 .dbf、.shx 也当成数据源,导致重复读取或报错。因此遍历时应只匹配主文件扩展名(.shp、.geojson、.gpkg 等),把附属文件排除在外。

二、通配符与路径遍历

Python 标准库 pathlib 的 rglob() 支持递归遍历,配合通配符即可按格式收集文件。下面是一个可直接使用的收集函数,它把“扩展名 → 读取方式”的映射集中管理:

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

# 扩展名白名单:只收主文件,排除 .dbf/.shx/.prj 等附属文件
EXT_DRIVER = {
    ".shp":     "ESRI Shapefile",
    ".geojson": "GeoJSON",
    ".json":    "GeoJSON",
    ".gpkg":    "GPKG",
    ".kml":     "KML",
}

def collect_layers(root: str):
    """递归收集矢量文件,返回 [(路径, 扩展名), ...]"""
    root = Path(root)
    hits = []
    for p in root.rglob("*"):
        if p.is_file() and p.suffix.lower() in EXT_DRIVER:
            hits.append((p, p.suffix.lower()))
    return sorted(hits, key=lambda x: str(x[0]))

注意 p.suffix.lower() 统一小写,避免 .SHP 与 .shp 混用漏读;sorted 保证每次运行顺序一致,便于结果复现与比对。若目录层级很深,也可用 root.glob("**/*.shp") 逐个格式匹配,但多格式混合时上面的白名单字典更易维护。

三、统一读取与来源标注

读入后必须给每个 GeoDataFrame 增加来源字段,否则合并后无法回溯某条断层线来自哪个图幅。同时要处理 GPKG 的多图层问题:一个 .gpkg 可能含“断层”“地层”“钻孔”多个图层,应逐层读取而非只读第一个。

def read_one(path: Path, ext: str):
    """读取单个文件,返回 GeoDataFrame 列表(GPKG 可能多图层)"""
    frames = []
    if ext == ".gpkg":
        import pyogrio
        layers = pyogrio.list_layers(path)[:, 0]   # 图层名数组
        for lyr in layers:
            gdf = gpd.read_file(path, layer=lyr)
            gdf["src_file"] = path.name
            gdf["src_layer"] = lyr
            frames.append(gdf)
    else:
        gdf = gpd.read_file(path)
        gdf["src_file"] = path.name
        gdf["src_layer"] = None
        frames.append(gdf)
    return frames

这里用 pyogrio.list_layers() 先枚举图层名,再逐层读取,避免遗漏。对 Shapefile 与 GeoJSON,src_layer 置空即可。所有结果最终可用 pd.concat(frames, ignore_index=True) 合并,但合并前务必确认各图层 CRS 一致——地质图幅常为 CGCS2000 高斯投影,而在线 GeoJSON 多为 WGS84,直接合并会错位,需先 to_crs() 统一。

多格式地质矢量数据批量读取与异常分流 数据根目录 survey_2024/ ├ 图幅A.shp ├ 断层.geojson └ 钻孔.gpkg rglob 递归遍历 扩展名白名单过滤 排除 .dbf/.shx/.prj 格式判断 扩展名 → 驱动 .shp → Shapefile 单图层,读 .shp 主文件 .geojson → GeoJSON 单图层,注意 CRS 为 WGS84 .gpkg → GPKG list_layers 逐层读取 try / except 异常分流 • 文件损坏 / 驱动缺失 → 记录路径,跳过 • 空图层(0 要素)→ 记录警告,不中断 • CRS 缺失 → 标记待人工确认 • 成功 → 追加 src_file / src_layer 字段 合并结果 GeoDataFrame 列表 + 读取日志表 (成功/失败/跳过)

四、读取异常处理:让批处理不因单个坏文件中断

批量读取最怕“一个文件报错,整批停摆”。地质数据来源杂,常见异常包括:Shapefile 缺少 .dbf 导致属性表读不出、GeoJSON 坐标越界或几何为空、GPKG 图层名含中文或特殊字符、文件被其他软件占用。处理原则是逐文件隔离异常,记录后继续:

log = []
frames = []
for path, ext in collect_layers("survey_2024"):
    try:
        parts = read_one(path, ext)
        for gdf in parts:
            if len(gdf) == 0:
                log.append({"file": path.name, "status": "empty"})
                continue
            if gdf.crs is None:
                log.append({"file": path.name, "status": "no_crs"})
            frames.append(gdf)
            log.append({"file": path.name, "status": "ok", "n": len(gdf)})
    except Exception as e:
        log.append({"file": path.name, "status": "fail", "err": str(e)[:80]})

log_df = pd.DataFrame(log)
print(log_df["status"].value_counts())

这段代码的价值在于:try 只包住单个文件的读取,异常不会扩散;log 表把每个文件的处理结果结构化保存,最后用 value_counts() 一眼看出成功、空图层、缺 CRS、失败各多少。对于 no_crs 的文件,不要静默丢弃,应单独列出待人工确认——地质图幅的坐标系一旦搞错,后续空间叠加全部失效。

实践建议:把 collect_layers、read_one 和异常循环封装成一个 batch_read(root) 函数,返回 (frames, log_df) 二元组。这样后续章节做裁剪、投影、合并时,只需调用一次即可拿到干净数据与完整日志,避免重复编写遍历逻辑。
常见坑:① 用 glob("*.shp") 只匹配当前目录,漏掉子目录图幅,应改用 rglob;② 把 .dbf 当数据源读取,报“不是有效数据源”;③ GPKG 不指定 layer 只读到第一个图层,误以为数据缺失;④ 合并前未统一 CRS,导致断层线与地层边界错位数百米。

完成本节后,你应能对任意一个混格式的地质数据目录,稳定地收集主文件、按格式分派读取、为每条记录标注来源,并输出一份可核查的读取日志。这份日志既是质量凭证,也是后续排障的起点。