简介:这套中国历史朝代行政范围矢量数据集,依据谭其骧《中国历史地图集》及历代正史地理志等权威文献数字化重构,覆盖夏商周至明清共28个主要政权或朝代阶段,面向历史地理研究者、GIS开发者和高校教学场景,解决历史政区边界数字化对标问题。压缩包共178个文件,约38.08MB,核心为shp矢量文件,配套dbf属性表、prj坐标系、xml元数据及说明文档,可直接导入主流GIS平台。已有234人浏览学习。每个朝代独立成层,包含省府县三级行政单位多边形,属性表含37项结构化字段,如建置时间、治所经纬度、古今地名对照、史料出处、边界置信等级等,经过拓扑检查,支持叠加分析、时序动画与空间统计,适用于历史疆域演变、军事防御体系、丝绸之路路线复原等研究。
1. 中国历史朝代行政范围矢量SHP:这包数据到底怎么用起来
先说一个反直觉的结论:大多数人拿到「中国历史朝代行政范围矢量SHP」这类数据集,都会先去打开属性表,但真正决定这包数据能不能用的,不是里面有多少个朝代,而是坐标系统、属性编码和几何拓扑这三个最不起眼的环节。你下载的数据集如果覆盖秦到清的主要历史行政区划,理论上能做很多事,但如果你直接把它拖进 QGIS 或者 ArcMap,看到的可能是乱码、错位、甚至无法显示的空白图层。这不是数据本身没价值,而是你还没掌握历史行政区划数据的地层逻辑。这篇文章我会从数据组织、加载预处理、筛选制图和避坑排查四个角度,完全按照一线实操的方式来拆解这份资源,帮你弄清楚它包含什么、能解决什么、以及哪些坑你一定会遇到。
2. SHP数据集的结构与坐标系统:先搞清几何和属性到底存了什么
2.1 属性表里的字段逻辑:朝代、年份与政区名
拿到手的第一步,不是急着加载,而是先打开属性表,搞清楚每一个字段的含义。这类历史行政区划 SHP 数据集的属性表通常包含几个核心字段:朝代名称(dynasty)、政权名称(regime)、起止年份(start_year 和 end_year),以及行政级别(level)和政区名称(name)。不同朝代的数据粒度差异极大,比如秦代可能只有郡级边界,而唐代会细化到州、府级别,明代可能还区分布政使司和都司卫所。你首先要确认的是,你手里的这份资源到底碎到什么程度,因为这会直接影响后续的图层叠加和统计口径。
在看属性表的时候,最容易翻车的是编码问题。很多历史数据是从古籍地图数字化来的,属性字段用的是 GBK 或者 GB2312 编码,而 QGIS 默认用 UTF-8 读取,于是你打开属性表看到的就是一堆「�」符号。这时候先别急着给数据判死刑,用 QGIS 的「数据源管理器」重新设置编码为 GBK 就能正常显示。如果手头没有 GIS 软件,也可以用 Notepad++ 直接打开 DBF 文件(注意不是打开 SHP 主文件),把编码从 ANSI 转为 UTF-8,然后再放回原始目录。我一般会先用 Python 的dbfread库快速检测一下 DBF 的编码字节,避免在 GUI 里反复试错。
import dbfread # 读取属性表,打印前两条记录检查字段和值 table = dbfread.DBF('han_administration.dbf', encoding='gbk') for i, rec in enumerate(table): if i >= 2: break print(rec)这段代码假设你的 DBF 文件编码是 GBK,如果打印出来仍然乱码,就把 encoding 换成 'utf-8' 再试。dbfread不会修改原文件,所以你可以安全地对同一文件多次尝试。实际项目中我更推荐在加载前就确认编码,因为 DBF 一旦转错编码,再转回来时部分汉字可能永久损坏。
2.2 坐标系:为什么图层总是跑到非洲海边
历史行政区划数据的坐标系是另一个大坑。绝大多数这类 SHP 数据集的几何字段是基于经纬度存储的地理坐标系,常见的是 WGS84(EPSG:4326)。但也有一部分来源会默认使用 GCS_Beijing_1954(EPSG:4214)或者甚至是一个未定义的投影坐标系。当你的图层投影与底图不一致时,地图上的边界可能飞到大西洋或者非洲中部的海面上。判断方法很简单:在 QGIS 里右键图层打开「属性—源」,查看「坐标参考系统」。如果显示的是未知(Unknown CRS),那么你需要在图层属性里手动指定一个合理的坐标系。
最稳妥的做法是一律按 WGS84 处理,因为现代底图(如 OpenStreetMap、Google 卫星图)都基于 WGS84 经纬度。如果你发现数据本身是用 GCS_Krasovsky_1940 或类似的老基准面定义的,那就需要用ogr2ogr做一次坐标转换,把几何从老基准面转到 WGS84 上,否则即使你强行指定 EPSG:4326,图层在底图上也会存在至少 100 米到 300 米的偏移。对于历史地理研究的尺度,这种偏移可能还能容忍,但如果你要叠加现代河流、道路或城市边界做精确分析,就必须认真处理基准面转换,不能只改 EPSG 号了事。
ogr2ogr -t_srs EPSG:4326 -s_srs EPSG:4214 han_4326.shp han_4214.shp这条命令的含义是从 EPSG:4214(老北京基准面经纬度)转换到 EPSG:4326(WGS84),输出新文件han_4326.shp。其中-s_srs参数告诉工具输入数据的坐标系,-t_srs指定输出坐标系。如果你的数据没有定义坐标系,你需要先通过周围地理要素的分布特征(比如海岸线形状、河流相对位置)来推断一个近似坐标系统,然后在转换时用-a_srs强行指定,例如ogr2ogr -t_srs EPSG:4326 -a_srs EPSG:4214。这种推断有一定玄学成分,最好的鉴别方式是把图层叠加到现代行政边界上,看海岸线和主要河流是否吻合,再决定是否需要微调。
2.3 几何类型与拓扑结构:面还是多部分面
历史行政区划数据的几何类型通常是 MultiPolygon,而不是简单的 Polygon,因为一个朝代的首都圈、核心州郡和边境军镇往往彼此分离,甚至中间隔着政区飞地。你在做面积统计时必须注意区分几何面积和记录面积,尤其是在联合属性查询时,一个记录可能包含多个不连续的面。用 QGIS 的「矢量—几何工具—分解多边形」可以把多部分面拆成单个面,但这样会丢属性,所以拆分前最好先复制一份数据。
另一个常见问题是自相交和缝隙。历史边界数字化时,手绘节点很容易造成相邻面之间存在细小的缝或重叠,这在制图时看起来不明显,但一旦你用「处理—工具箱—修复几何」去跑拓扑检查,就会报出一大堆错误。我一般在加载后立刻运行一次「几何有效检查」,定位自相交、重复节点、尖刺等无效几何,然后用「修复几何」工具一键修复。记住,修复几何会改变边界的精确度,对历史研究来说,微小的极值点是可接受的,因为原始数据本来就是手工数字化的。
3. 数据加载与预处理:半小时内把历史边界跑起来
3.1 批量加载多个朝代的 SHP
不同朝代的行政范围通常是单独成文件的,例如qin.shp、han.shp、tang.shp,每个文件内部可能还有多个政权(例如三国时期魏、蜀、吴三个图层)。如果你拿到手的是一整个目录几十个文件,批量加载才是正确打开方式。QGIS 里面可以用「图层—添加图层—添加矢量图层」,然后按住 Ctrl 多选所有 SHP 文件,一次性导入。加载完成后,每个图层会默认用随机颜色渲染,你可以在图层列表里批量选中它们,右键「分组」,把全部历史数据放入一个组里统一管理。
如果是用代码批量加载,GDAL 的ogr库可以遍历目录并读取每个文件的元数据,推荐在正式进入桌面软件之前先跑一遍,把每个文件的坐标系、要素数量、字段列表打印出来,形成一份“数据体检报告”。这样你能在一分钟内发现哪个文件编码错了,哪个文件坐标系没有定义,而不需要逐个在 GUI 里点开属性看。
from pathlib import Path from osgeo import ogr shapefiles = list(Path('historical_shp').glob('*.shp')) for shp in shapefiles: ds = ogr.Open(str(shp)) if ds: layer = ds.GetLayer(0) srs = layer.GetSpatialRef() print(shp.stem, layer.GetFeatureCount(), srs.ExportToWkt()[:50])这段代码使用 GDAL 的 Python 绑定,遍历指定目录下的所有 SHP 文件,输出文件名、要素数量和空间参考的 WKT 前 50 个字符。你可以在任何有 GDAL 的 Python 环境里运行它,不需要安装桌面 GIS。如果发现某个文件的空间参考为空(srs是None),你就知道这个文件需要手动指定坐标系了。强制指定时要注意,很多历史数据其实是用经纬度存储的,只是缺少.prj文件,那么你直接指定 EPSG:4326 即可,而不需要复杂的投影转换。
3.2 项目投影选择:做分析时使用什么投影
加载完所有朝代后,你还需要做一件事:为项目设置一个合适的工作投影。历史行政区划跨度极大,从西域到东海,从漠北到岭南,如果直接用 WGS84 经纬度计算面积,结果会严重失真,因为纬度越高,经纬度网格代表的实际面积扭曲越大。做面积统计或距离计算时,必须选择一个适合中国大陆区域的等积投影,我的习惯是用 Albers 等积圆锥投影(EPSG:102025),中央经线选 105°E,双标准纬线选 25°N 和 47°N。不同 GIS 软件设置参数的位置不同,QGIS 中是在「工程—属性—CRS」里选择或自定义。
如果你只做制图展示而不做测量,那么用 WGS84 经纬度也没问题,因为显示效果没有太大差别。但一旦你要计算各朝代疆域面积、统计政区面积分布,就必须在分析前把图层重投影到 Albers 投影下,否则算出来数字会偏大或偏小得离谱。用ogr2ogr批量重投影也很方便:
ogr2ogr -t_srs EPSG:102025 tong_albers.shp tong_4326.shp注意:EPSG:102025 是一个 ESRI 内部定义的 Albers 投影,有些 GDAL 版本可能不认识,你可以用 PROJ 的字符串代替。在 QGIS 里设置自定义投影时,用以下参数:+proj=aea +lat_1=25 +lat_2=47 +lat_0=0 +lon_0=105 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs。这些参数的含义分别是等积圆锥投影、双标准纬线为北纬 25 度和 47 度、中央经线为东经 105 度、单位是米。这样重投影后,面积计算就可靠了。
3.3 字段清洗与时间范围裁剪
历史行政区划数据中,年份字段并非总是干净的。有些文件里只有朝代名,没有具体起止年份;有些文件里年份是文本格式,比如「前221」和「-206」混用;还有些会把西汉和东汉都笼统写成「汉」。在做时间序列分析之前,必须统一年份字段的数据类型和格式。我用 Python 的pandas读取 DBF 文件(通过pandas直接读注意需要安装pandas-dbf插件,或者先用ogr2ogr把 SHP 转成 GeoJSON/CSV),然后清洗start_year和end_year两列,把公元前年份统一为负数。
import pandas as pd # 读取属性表(CSV格式,假设已导出) df = pd.read_csv('song_dynasty_attrs.csv', encoding='utf-8') # 把字符串年份转换为整数,非数字强制为NaN df['start_year'] = pd.to_numeric(df['start_year'], errors='coerce') df['end_year'] = pd.to_numeric(df['end_year'], errors='coerce') # 输出年份范围 print(df['start_year'].min(), df['end_year'].max())这段代码处理了文本年份混乱的情况。pd.to_numeric会把形如“960”的字符串转成整数,同时把无法转换的值变成NaN,方便你后续检查哪些记录缺年份。很多历史数据把朝代之间的过渡期也做进图层里,比如隋末唐初、宋辽金并行这些时期,如果你只想显示某个特定年份(例如公元 1100 年),需要用条件筛选start_year <= 1100 <= end_year,然后导出为新的图层。这里的关键点是,历史行政区划是随时间动态变化的,用单一朝代名替代年份范围做筛选,很容易把多个政权的疆域混杂在一张图中。
4. 按朝代查询与制图:把时间维度装进图层里
4.1 属性筛选实现单朝代显示
当你需要展示某个朝代的疆域时,可以直接用 SQL 查询语句在属性表里做筛选,而不需要拆分成多个文件。QGIS 的「按表达式选择要素」支持 SQL 语法,例如想选中唐代所有州级边界,可以写"dynasty" = '唐' AND "level" = '州'。但前提是字段名和字段值必须与你数据集的属性表一致,否则查询结果为空。为了避免字段名不一致的问题,我建议在加载数据后立即用「属性表—字段计算器」新建一个规范的字段,比如将dynasty统一规范化成全拼或标准简称,同时去除文本中的空格。
如果使用 Python 的fiona或geopandas做筛选,操作更灵活,也更适合批量处理。geopandas可以直接读取 SHP,用布尔索引筛选用例,然后写回新的 SHP 文件。例如要从全部历史数据中提取元代的所有疆域要素,只需要两个条件:朝代名称是「元」或者年份范围跨越 1271 到 1368。需要注意的是,元代疆域包含岭北行省、辽阳行省等几个大区域,而且这些区域与汉地图的比例尺差别大,所以筛选结果可能需要先检查几何有效性再出图。
import geopandas as gpd # 读取元代全图 all_dynasties = gpd.read_file('all_historical.shp') # 条件是朝代字段为“元”,并且开始年份<=1368且结束年份>=1271 yuan = all_dynasties[(all_dynasties['dynasty'] == '元') & (all_dynasties['start_year'] <= 1368) & (all_dynasties['end_year'] >= 1271)] # 重投影到Albers后再保存 yuan.to_crs(epsg=102025).to_file('yuan_albers.shp', encoding='utf-8')这里to_crs(epsg=102025)会把数据重投影为 Albers 等积圆锥,保存时指定encoding='utf-8'防止再次出现中文乱码。我建议所有从中文 SHP 导出的文件都显式指定编码,因为 GDAL 默认生成 DBF 时可能使用系统 ANSI 编码,在 Linux 系统下容易变成未知编码。
4.2 做时间轴动画:多图层叠加与透明度
历史行政区划最吸引人的表达方式是时间动画,以五年或十年为步长,动态改变显示范围。实现方式有两种:一种是在 GIS 软件里手动创建多个年份切片图层,另一种是用 Python 生成一批年份状态的 GeoJSON 文件,再用 QGIS 的时间管理器插件循环播放。我推荐用代码生成切片,因为手动复制图层再多设置可见性非常费时。
使用geopandas,可以按年份循环过滤现有数据,每次生成一个新文件。例如,以 50 年为间隔从公元前 221 年到公元 1368 年,生成约 32 个时间切片。每个切片里只保留在当年存在的政区面。生成后,在 QGIS 中依次加载所有切片,用相同样式渲染,然后使用时间管理器设置开始时间和持续时间即可。这种做法适合做「中国历史疆域变迁」视频,但要注意年份跨度大时数据量也会很大,建议使用 GeoPackage 格式而不是 SHP 来存储多个切片,因为 SHP 文件头有 2GB 限制。
4.3 符号化:核心区与特殊政区的区分
历史行政区的制图不是填充一种颜色就完事。不同等级的政区需要区分符号,例如中央直辖郡县用实心填充,羁縻州或者属国用斜线填充,长城以外的游牧政权控制区用半透明小点阵来表示。在 QGIS 的样式面板中可以使用基于规则(Rule-based)的渲染,写"level" = '郡'、"regime" = '唐'等规则。核心原则是,不要用面填充色的深浅去表示时间先后,因为历史数据的年份范围是区间,不是瞬时值。
我一般在出图时会给每个朝代图层添加白色内边框,避免相邻政区边界粘连。具体做法是「符号层—添加一个简单填充—将笔划颜色设为白色—笔划宽度设为0.2 毫米」,同时把外层符号层的透明度设为 20% 左右。这样能同时看到重叠区域和历史地名注记。如果你用的是 ArcGIS Pro,可以用「属性—Geoprocessing 选项—导出符号」来保存样式模板,以统一整组图件的风格。
5. 避坑指南:历史行政区划数据最常见的五个翻车现场
5.1 打开后所有中文属性都是乱码
现象:图层加载正常,但打开属性表,所有汉字都显示为带菱形问号的乱码,部分字符直接消失。
原因:SHP 的 DBF 属性表采用 GBK 编码,而 GIS 软件默认按 UTF-8 读取。我在用 QGIS 3.x 时遇到这个问题的频率非常高,尤其是数据是从某些平台分享打包时,原始 DBF 没有将编码字头写进文件头。
解决:在加载图层时,「数据源管理器中」找到该图层,在「连接」部分把编码从UTF-8改为GBK或GB2312,再点击「确定」重新加载。如果图层已经加载到工程,右键图层选择「数据属性」—「编码」,改成GBK后会立即刷新。注意,修改后必须重新保存一次文件,用ogr2ogr -lco ENCODING=UTF-8转成 UTF-8 的 SHP,否则下次加载又会乱。从那以后,我每次收到历史数据,都会先强制跑一遍编码检测脚本再入库。
5.2 图层叠加后相距 0.3 度,位置明显偏离
现象:在 QGIS 中把明代边界与现代乡镇边界叠加,明代图层整体往西北或东南偏移 0.3 到 1 个经度,海岸线对不上。
原因:数据源使用老基准面(如 1954 北京坐标系或克拉索夫斯基椭球体),你只是手动把图层的投影声明改成了 WGS84,但实际上几何坐标数值仍然是基于旧椭球体角度,导致系统性偏移。这个偏移不是旋转或平移一条线,而是每个点经纬度都有差异,在中国范围大约几百米。
解决:在未加载前就明确原坐标系。如果你无法从数据源文档或.prj文件判断,可以通过图层与河流/海岸线的相对位置估算偏移方向,然后用控制点做仿射变换。在 QGIS 中用「矢量几何—重新投影图层」配合自定义 PROJ 字符串做七参数转换,但最简单的方式是用ogr2ogr把源强制指定为EPSG:4214(Beijing 1954 经纬度)再转回EPSG:4326。如果还偏,就需要打开.prj文件查看DATUM["D_Beijing_1954"]之类的关键字,再决定基准面参数。
5.3 几何自相交导致面积统计失败
现象:用「字段计算器」计算多边形面积时,某些要素弹出「几何无效」错误,或者在相交分析时部分面缺失。
原因:手绘数字化时,节点顺序错误、线段交叉,导致多边形外环自相交。QGIS 的Fix geometries能解决大部分,但有些自相交发生在多部分面之间,修复后会丢失内部孔洞,比如湖泊和飞地消失。
解决:先用「矢量—几何工具—校验几何」将所有错误打印到信息栏,然后批量修复。修复后再检查要素数量没有减少,如果减少,则需要对原始数据进行人工检查,用顶点编辑器删除多余节点。我曾经处理过一份宋代两浙路的数据,一个多边形有 3000 多个节点,其中十几个节点互相交叉,修复后面积从错误的 12 万平方公里修正到真实的 9.8 万平方公里,结果影响很大,所以面积统计前一定先做几何检查和修复。
5.4 两个朝代的时间范围重叠,筛选结果冲突
现象:查询「公元 980 年」的疆域时,同时选中了宋代和辽代的部分记录,看起来像边界叠加。
原因:历史时期存在并立政权,比如十国中的吴越和南唐,宋建隆年间同时存在多个政权,而年份筛选条件只写了start_year <= 980和end_year >= 980,没有排除政权属性。另外,有些数据把「朝代」字段填成统治者的庙号或年号,比如「北宋」「宋」混用,都被筛选出来了。
解决:给你的筛选条件增加regime字段的判断,明确提出你需要哪个政权。在做时间切片时,先确定切片年号对应的标准政体,然后在iso_year字段(标准化年份)上做交集筛选。更好的办法是建立一份「年份—政权对照表」,把每个年份对应到唯一政权集,再用表连接去重。
5.5 保存的 SHP 到其他软件里打不开
现象:在 QGIS 中正常编辑并保存的 SHP,拿到 ArcGIS 或 FME 里打开时提示「无法打开矢量图层」或只能看到属性表。
原因:QGIS 保存 SHP 时默认在每个要素后附加UTF-8编码的.cpg文件,但 ArcGIS 某些版本对.cpg支持不完整;如果属性字段名包含中文或超过 10 个字符,也会造成兼容问题。此外,保存路径包含中文也可能导致 ArcGIS 无法解析。
解决:通用规则是:字段名一律使用英文字母,长度不超过 10。保存时选择ESRI Shapefile,并在图层选项中设置ENCODING=GBK或UTF-8,同时把文件路径里的中文换成英文目录。如果实在要跨软件协作,最保险的方案是导出为 GeoPackage 格式(.gpkg),几乎所有现代 GIS 软件都能直接读取,且没有 DBF 字段长度和编码问题。从我自己的经验看,用.gpkg代替.shp作为历史数据中间格式,能省下很多不必要的兼容性困扰。
6. 进阶用法:把历史政区中心点与今日地名做模糊匹配
这个数据集的潜力远不止画图。把历史行政范围转为政区中心点,再与今天的地名做关联,可以做人口迁移、政区沿革、稻作扩散等课题。历史政区名与今天地名往往不完全一致,比如「长安」对应今天的「西安」,或者「江陵」对应「荆州」,你需要一个模糊匹配算法来辅助关联。
第一步,从 SHP 中提取每个政区面的质心或中心点。用 QGIS 的「几何工具—多边形质心」即可,注意质心可能落在面外(比如弯月形政区),建议改用Point On Surface函数,它能保证点落在面内。导出 CSV 时,保留政区名和朝代名两个字段。
第二步,使用 Python 的difflib或fuzzywuzzy做文本相似度匹配。历史地名与现代地名存在“古今分合”关系,单纯相似度不够,还需要手工校对。我的习惯是先把所有历史地名与现代行政区划表(可以来自公开的民政数据)进行归一化,去除「郡」「府」「路」等通名,再比较核心专名。如果相似度大于 90,直接接受;在 70 到 90 之间,人工复核;低于 70,记录为未匹配。
from fuzzywuzzy import fuzz # 历史地名列表与现代地名列表(简化示例) hist_names = ['长安', '洛阳', '江陵', '会稽'] modern = ['西安', '洛阳', '荆州', '绍兴'] def match(name, candidates, threshold=70): for cand in candidates: score = fuzz.partial_ratio(name, cand) if score >= threshold: return cand, score return None, 0 for n in hist_names: matched, score = match(n, modern, threshold=80) print(n, '->', matched, score)这段代码利用fuzzywuzzy的partial_ratio做子串匹配,因为历史地名通常是现代地名的旧称,包含关系较多。比如「长安」与「西安」的相似度可能不高,但「长安」与「长乐」等词会有高分,所以实际使用时需要结合朝代年份来精化匹配范围。更高阶的做法是使用一个历史地名词典表,把长安直接映射到西安,数据量不大时,手工建映射比算法靠得住。
第三步,拿到匹配结果后,可以将历史中心点与现代行政区面做空间连接,计算政区面积级别的包含关系。例如,把一个明代「府」的中心点落到今天的某个地级市内,如果落进去,就认为该府的中心区与今天该市存在承载关系。这一步用geopandas.sjoin实现不了(因为点是点与面的连接),但可以用overlay或空间索引加速。我的一个实际做法是,给每个历史中心点添加一个 5 公里半径的缓冲区,再用intersects判断缓冲区是否穿过了今天的边界,这样能忽略政区边界细微变化,提高匹配率。
另一个值得尝试的进阶方向是地形叠加。历史行政区边界经常沿着山脊或河流,将 SHP 数据与 DEM(数字高程模型)叠加,可以分析某一朝代的政区中心分布与海拔、坡度之间的关系。在 QGIS 中,用「采样栅格值」工具把 DEM 的高程值写入政区中心点属性表,然后再用统计工具按朝代分组计算平均海拔。我做过一个唐和宋的对比,发现唐代州治平均海拔比宋代高约 80 米,这间接反映了经济重心从西北向东南转移的地形因素。这种分析需要先把边界重投影到 Albers,然后使用gdalwarp裁剪 DEM 到研究范围,否则数据量很大,计算会很慢。
最后提醒一点:历史地理数据永远不可能 100% 精确,因为它是对古代文献、考古地图和现代测绘的折中复原。在你利用这份数据集做研究或汇报时,一定要在成果中标注原始数据的制图单位和时间分辨率,不要给人「精确边界」的错觉。从那以后,我每次拿到新的历史行政区划数据集,都会强制先完成属性编码检查、坐标系确认、几何修复三件事,再进主题分析。这套流程虽然多花 15 分钟,但能拦下大部分后续返工,希望帮到你。
本文还有配套的精品资源,点击获取