
简介这份资源面向地理信息、城乡规划、交通与水文等方向的研究者和学生提供江苏省成套的Shapefile矢量数据可直接导入ArcGIS、QGIS等软件开展地图制作与空间分析。压缩包共83个文件约974KB以shp几何数据、dbf属性表、shx索引、prj投影及sbn、sbx空间索引为主另含少量xml元数据覆盖河流、湖泊、县域、地级市、高速公路、国道、铁路、长江、丘陵、湿地与经纬网等要素可支撑洪水风险叠加、区域发展差异对比、交通网络优化等分析。目前已有2379人学习下载数据经整理可直接调用省去自行采集与配准的环节适合作为课程作业、论文制图与规划研究的基础底图也便于按要素拆分组合快速搭建江苏地理格局的可视化方案。1. 江苏省河流、湖泊、县域、高速等shp数据从拿到文件到跑出第一张图做江苏相关项目的工程师大概率都遇到过这个场景领导或甲方丢过来一句“把江苏的河流、湖泊、县域边界和高速路网叠一张图”你打开文件夹一看是一堆.shp、.dbf、.shx、.prj后缀的文件双击打不开拖进 QGIS 能看但坐标系对不上导进 PostGIS 又报 SRID 错误。这套“江苏省河流、湖泊、县域、高速等shp数据”本质上是一组矢量地理数据用 Shapefile 格式组织覆盖水系、行政区划、交通三类基础图层。它解决的是“底图从哪来”的问题适合做空间分析、专题制图、选址评估、路径规划前期准备的从业者。新手能照着下面的步骤跑通熟手可以重点看坐标系和属性表那两节的边界处理。2. 先搞懂Shapefile到底装了什么四类图层与坐标系选型2.1 河流、湖泊、县域、高速各自是什么几何类型拿到一套江苏 shp 数据第一件事不是急着打开而是先判断每个图层是什么几何类型因为这决定了你后面能用什么分析工具。常见做法是河流多为LineString或MultiLineString线状水系用于长度统计、连通性分析、缓冲区。湖泊多为Polygon或MultiPolygon面状水体用于面积计算、水域范围裁剪。县域Polygon行政区划面用于分区统计、按县聚合、裁剪其他图层。高速LineString路网线用于距离计算、可达性分析、叠加县域做密度。判断几何类型不用打开软件用ogrInfo或 Python 一行就能看ogrinfo -so jiangsu_county.shp jiangsu_county输出里的Geometry: Polygon就是几何类型Feature Count是要素数量。这一步很关键因为如果你拿湖泊面去做“道路长度统计”逻辑上就不成立后面全是白干。2.2 坐标系不统一是第一个翻车点江苏常用的坐标系有两类地理坐标系GCS_WGS_1984EPSG:4326和投影坐标系WGS_84_UTM_zone_50NEPSG:32650。很多网上流传的 shp 数据河流是 4326县域是 32650高速又是另一个自定义投影。你直接叠图会发现高速跑到海里去了这就是坐标系没对齐。查看当前坐标系ogrinfo -so -al jiangsu_river.shp | grep -i PROJCRS\|GEOGCRS\|EPSG如果输出里 EPSG 不一致必须先统一。做江苏全省分析我一般统一到EPSG:32650因为它是 UTM 50N 带适合江苏经度范围约 116°E–121°E面积和距离计算误差小。统一用ogr2ogrogr2ogr -t_srs EPSG:32650 jiangsu_river_utm.shp jiangsu_river.shp-t_srs是目标坐标系源坐标系 ogr2ogr 会自己读.prj文件。如果.prj缺失或写错就得用-s_srs手动指定否则转换结果会偏到离谱。这一步做完四个图层再叠位置才对得上。2.3 属性表里哪些字段真正有用Shapefile 的属性存在.dbf里字段名限制 10 个字符中文常被截断或乱码。用ogrinfo看字段ogrinfo -so -al jiangsu_county.shp县域图层常见字段有NAME、ADCODE、PROVINCE河流图层可能有NAME、LEVEL、LENGTH高速图层可能有NAME、CODE、TYPE。做分区统计时ADCODE比NAME可靠因为名称可能重名或带“市辖区”后缀。如果字段乱码用 QGIS 打开时在图层属性里把编码改成GBK或UTF-8试Shapefile 的.cpg文件就是干这个的没有就手动建一个写UTF-8。3. 用Python把四类shp读进来并做一次叠加分析3.1 环境准备与依赖安装我一般用geopandas做原型底层是fiona和pyproj读 shp 和转坐标系都很顺。安装pip install geopandas matplotlib shapely pyproj fiona如果fiona装不上多半是 GDAL 版本冲突用 conda 装更稳conda install -c conda-forge geopandas装完先验证import geopandas as gpd print(gpd.__version__)版本不用追新能读能转就行。这一步的坑在于 Windows 下 pip 直接装 GDAL 容易失败conda 省心很多。3.2 读取四个图层并统一坐标系import geopandas as gpd # 读取四类数据注意路径按实际改 river gpd.read_file(jiangsu_river.shp) lake gpd.read_file(jiangsu_lake.shp) county gpd.read_file(jiangsu_county.shp) highway gpd.read_file(jiangsu_highway.shp) # 统一到 UTM 50N适合江苏 target_crs EPSG:32650 river river.to_crs(target_crs) lake lake.to_crs(target_crs) county county.to_crs(target_crs) highway highway.to_crs(target_crs) # 检查几何有效性面数据尤其重要 for name, gdf in [(river, river), (lake, lake), (county, county), (highway, highway)]: invalid (~gdf.is_valid).sum() print(f{name}: {len(gdf)} features, {invalid} invalid geometries)逻辑说明to_crs会重投影几何前提是原数据有正确 CRS。如果读进来crs是None得先set_crs再to_crs。is_valid检查自相交、环方向错误等问题湖泊和县域面数据经常有无效几何后面做空间连接会报错。参数说明target_crs选 32650 是因为江苏在 UTM 50N 带内单位是米算面积和距离直接得平方米和米不用再换算。如果你只做展示4326 也行但别混用。3.3 按县域统计高速里程和湖泊面积# 高速按县域裁剪后统计长度 highway_in_county gpd.overlay(highway, county, howintersection) highway_in_county[seg_len] highway_in_county.geometry.length highway_stat highway_in_county.groupby(ADCODE)[seg_len].sum().reset_index() highway_stat.columns [ADCODE, highway_m] # 湖泊按县域统计面积 lake_in_county gpd.overlay(lake, county, howintersection) lake_in_county[area_m2] lake_in_county.geometry.area lake_stat lake_in_county.groupby(ADCODE)[area_m2].sum().reset_index() lake_stat.columns [ADCODE, lake_m2] # 合并结果 result county[[ADCODE, NAME]].merge(highway_stat, onADCODE, howleft) \ .merge(lake_stat, onADCODE, howleft) result result.fillna(0) print(result.head())逻辑说明overlay做交集把高速线按县域面切开每条线段落在哪个县就归哪个县然后按ADCODE分组求和。湖泊同理面与面相交后算面积。fillna(0)处理没有高速或湖泊的县避免 NaN。参数说明howintersection只保留相交部分如果你要保留所有县即使没有高速用howunion再补零。ADCODE是行政区代码比名称稳定。这一步的坑在于overlay对无效几何很敏感前面is_valid检查出的问题要先buffer(0)修复。3.4 出图验证河流、湖泊、高速、县域叠一张import matplotlib.pyplot as plt fig, ax plt.subplots(figsize(12, 12)) county.plot(axax, facecolornone, edgecolorgray, linewidth0.5) lake.plot(axax, facecolorlightblue, edgecolorblue, linewidth0.3) river.plot(axax, colorblue, linewidth0.5) highway.plot(axax, colorred, linewidth0.8) plt.title(Jiangsu River/Lake/County/Highway) plt.axis(off) plt.savefig(jiangsu_overlay.png, dpi200, bbox_inchestight)逻辑说明先画面县域、湖泊再画线河流、高速线压在上面才看得见。facecolornone让县域只显示边界不挡住下面。dpi200保证出图清晰。参数说明linewidth按图层重要性调高速最粗河流细一点。如果图出来高速位置明显偏移回去查坐标系是否真的统一了别怀疑数据本身。4. 避坑与排查shp数据落地时最容易翻车的5个点4.1 中文属性乱码导出后全是问号现象QGIS 里看属性表中文正常用 Python 读出来是å®è‹这类乱码或者直接问号。原因Shapefile 的.dbf默认编码是系统区域设置Windows 中文环境常是 GBK但.cpg文件可能写的是 UTF-8读取时按 UTF-8 解就乱。解决先看有没有.cpg文件没有就手动建一个内容写GBK或UTF-8试。Python 里可以指定编码gdf gpd.read_file(jiangsu_county.shp, encodingGBK)如果还乱用ogrinfo看Data axis to CRS和编码提示或者用 QGIS 另存为时选 UTF-8 重新生成.cpg。4.2 坐标系标了4326但实际是投影坐标现象数据.prj写的是 WGS84 地理坐标但坐标值是500000, 3500000这种大数明显是投影坐标。原因数据制作者用投影坐标导出但.prj文件复制错了或者从别的数据里带过来的。解决看坐标值范围。经度 116–121、纬度 30–35 是地理坐标几十万、几百万是投影坐标。如果数值是投影但标 4326用set_crs(EPSG:32650, allow_overrideTrue)强制改再转你要的坐标系。别直接to_crs会算出离谱结果。4.3 湖泊面自相交导致overlay报错现象gpd.overlay抛TopologyException或GEOSException。原因湖泊边界有自相交、重复点、环方向错误GEOS 无法处理。解决先修复几何lake[geometry] lake.geometry.buffer(0)buffer(0)是常用修复手段能把自相交面变成有效面。修完再is_valid检查一遍。如果还有问题用shapely.make_valid或explain_validity定位具体要素。4.4 高速线在县域边界处被切断后长度偏大现象按县统计高速里程总和比实际高速总里程大。原因overlay在边界处会把一条线切成两段如果边界有重叠或缝隙同一段可能被算两次。解决先对县域做dissolve合并成全省一个面再和高速做intersection这样不会在县界重复切。或者统计前对高速按ADCODE去重但更稳的是保证县域面之间没有重叠。用county.overlaps(county)检查自重叠。4.5 导出GeoJSON后文件巨大网页加载卡死现象shp 转 GeoJSON 后几百 MB前端加载不动。原因Shapefile 坐标精度高GeoJSON 文本格式冗余加上属性字段全带。解决导出前简化几何、保留必要字段county_simplified county[[ADCODE, NAME, geometry]].copy() county_simplified[geometry] county_simplified.geometry.simplify(0.001) county_simplified.to_file(county_simplified.geojson, driverGeoJSON)simplify(0.001)容差按坐标系单位32650 下是米0.001 米太细可以调到 10 或 50 米看效果。字段只留需要的别把整个.dbf都带过去。5. 进阶用空间索引加速大范围查询与批量裁剪5.1 为什么江苏全省数据也需要空间索引江苏县域约 90 多个高速几千条线段湖泊几百个面数据量不算大但如果你做的是“每个县分别和高速求交”这种循环没有索引就是 O(n×m) 全表扫描。geopandas的sjoin和overlay底层会用 R-tree 索引但如果你手动循环for county in counties: highway.intersects(county)每次都要遍历全部高速慢得明显。我一般先建空间索引highway_sindex highway.sindexsindex是geopandas基于shapely的 STRtree建一次后面查询快很多。查某个县范围内的高速from shapely.geometry import box county_geom county[county[ADCODE] 320100].geometry.iloc[0] possible_idx list(highway_sindex.intersection(county_geom.bounds)) candidates highway.iloc[possible_idx] actual candidates[candidates.intersects(county_geom)]逻辑说明先用bounds做粗筛拿到可能相交的索引再对候选做精确intersects。这样避免每次全表扫描。参数说明bounds是(minx, miny, maxx, maxy)sindex.intersection返回索引列表。如果数据量再大考虑用 PostGIS 的 GiST 索引但江苏这个量级geopandas够用。5.2 批量裁剪并导出每个县的独立shpimport os out_dir county_clip os.makedirs(out_dir, exist_okTrue) for _, row in county.iterrows(): adcode row[ADCODE] geom row.geometry # 用空间索引粗筛高速 idx list(highway_sindex.intersection(geom.bounds)) sub_highway highway.iloc[idx] sub_highway sub_highway[sub_highway.intersects(geom)] if len(sub_highway) 0: continue clipped gpd.clip(sub_highway, geom) clipped.to_file(os.path.join(out_dir, fhighway_{adcode}.shp), encodingUTF-8)逻辑说明遍历每个县用索引粗筛高速再精确相交最后clip裁剪。gpd.clip比overlay轻量适合只裁剪不计算属性的场景。参数说明encodingUTF-8保证导出属性不乱码。如果县很多可以并行但注意sindex不是线程安全的每个进程单独建。5.3 验证裁剪结果是否完整裁剪完要验证有没有漏。简单办法是统计所有导出高速的总长度和原始高速总长度比total_original highway.geometry.length.sum() total_clipped 0 for f in os.listdir(out_dir): if f.endswith(.shp): gdf gpd.read_file(os.path.join(out_dir, f)) total_clipped gdf.geometry.length.sum() print(foriginal: {total_original:.2f}, clipped: {total_clipped:.2f}, diff: {total_original - total_clipped:.2f})如果差值明显大于 0说明有高速落在所有县之外或者县边界有缝隙。检查县域dissolve后是否覆盖全省或者高速是否有部分在省界外。这个验证步骤我每次做完裁剪都会跑一遍血泪经验是别信“应该没问题”数字对不上就是有问题。5.4 一个实用技巧用ADCODE做关联而不是NAME最后说一个我踩过的坑。早期做分区统计用NAME关联县域和统计结果结果南京的“鼓楼区”和徐州的“鼓楼区”重名数据全串了。后来一律用ADCODE江苏县级代码是 6 位前两位是市中间两位是区县唯一且稳定。如果你拿到的数据没有ADCODE用NAME加CITY组合或者自己按行政区划表补一列。这个习惯帮我省了很多后悔药希望帮到你。本文还有配套的精品资源点击获取