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() 统一。
四、读取异常处理:让批处理不因单个坏文件中断
批量读取最怕“一个文件报错,整批停摆”。地质数据来源杂,常见异常包括: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,导致断层线与地层边界错位数百米。完成本节后,你应能对任意一个混格式的地质数据目录,稳定地收集主文件、按格式分派读取、为每条记录标注来源,并输出一份可核查的读取日志。这份日志既是质量凭证,也是后续排障的起点。