☰
历史人口普查shp数据高效处理指南:解压校验与代码对齐
2026/10/7 10:44:33 网站建设 项目流程

简介:覆盖一普至七普的区县级人口数据以shp格式打包,适配ArcGIS等GIS软件,面向城市规划、区域经济与社会学研究者,可用于人口增长、老龄化、迁移等趋势分析及区域差异对比。压缩包共7个文件,包含shp几何边界、dbf属性表、prj坐标系统、shx/sbn/sbx空间索引及xml元数据,整体大小46.62MB,结构完整,可直接加载使用。已有1174人学习/下载。数据中每份shp对应一次普查的区县边界,dbf中记录人口总量、结构等属性字段,便于跨年份纵向比较;同时可叠加地形、交通、公共服务设施等外部图层,开展人口密度、公共服务均等化等空间分析。xml元数据与prj坐标信息则帮助使用者快速确认数据来源和投影参数,降低预处理门槛,为政策制定与学术研究提供可靠的基础数据支撑。

1. “一普到七普shp格式人口数据.zip”:这套历史人口数据到底怎么用才不亏

“一普到七普shp格式人口数据.zip”说白了就是一份把 1953 年第一次全国人口普查到 2020 年第七次全国人口普查的数据,按县级行政区划整理成 shapefile 矢量格式后打成的压缩包。做 GIS、城乡规划或者人口研究的人拿到它,通常是想在 QGIS 里按县画人口热力、出历史人口变化图、或者做跨期面板数据。可真正动手你会发现,最麻烦的不是解压 zip,也不是加载 shp,而是让七次普查的行政区划对齐:同一个县在不同普查年份里可能换了代码、改了归属,直接 join 大概率拼出一堆空洞。本文就按我平常用的流程,从解压检查讲到区划代码归一化、普查公报校准、以及导出 WKT 做交叉验证,适合想用这套历史人口数据做正经分析、但不想在数据处理上反复翻车的从业者。

2. 先把 zip 安全打开:解压、完整性校验与 shp 文件组体检

2.1 解压前先做完整性校验,别等 QGIS 报错才后悔

很多人拿到 zip 的第一反应是双击解压,等 QGIS 打开 shp 时才发现缺文件或者属性表乱码。我的习惯是先在命令行里做一轮完整性校验,因为 zip 在传输过程中损坏的概率不低,尤其是动辄几百 MB 的历史数据包。Python 的 zipfile 模块自带 CRC 校验,跑一遍就知道哪个文件坏了。

import zipfile from pathlib import Path zp = Path("一普到七普shp格式人口数据.zip") with zipfile.ZipFile(zp) as zf: bad = zf.testzip() if bad is not None: raise RuntimeError(f"zip 内文件已损坏:{bad}") for info in zf.infolist(): print(info.filename, info.file_size, info.compress_size)

testzip()会逐个读取包内文件并比对 CRC32 校验值,一旦发现坏文件就返回文件名,正常则返回 None。infolist()能列出包内每个文件的原始大小file_size和压缩后大小compress_size,这两个值能帮你快速判断文件是否空壳——比如某个 shp 的 .dbf 压缩后才几百字节,十有八九属性表是空的。解压时还有一个隐藏坑:Windows 上打包的 zip 有时会用反斜杠作为路径分隔符,直接解压到 Linux 上会变成一个带反斜杠的怪文件名,所以解压前最好用os.path.normpath()规范一下内部路径。

如果你不想解压,GDAL 也支持用/vsizip/虚拟路径直接读压缩包里的 shp,比如/vsizip/一普到七普shp格式人口数据.zip/七普2020/七普县界.shp。但虚拟路径方式在文件损坏时排错更麻烦,我一般还是先跑一遍testzip(),确认 zip 完整再解压。这一步最多花一分钟,却能省掉后面一晚上的排错时间。

2.2 检查 shp 四件套:缺 .shx 和 .prj 最要命

shapefile 从来不是单一文件,一个完整的数据集至少需要四个:存几何的 .shp、存几何索引的 .shx、存属性表的 .dbf、存坐标系的 .prj。很多公开数据下载包会漏掉 .prj,结果加载到 GIS 里默认按 WGS84 画,和底图对不上;漏掉 .shx 则部分软件直接打不开。解压完之后我习惯先扫一遍目录,看每一年普查数据缺不缺文件。

import os root = "一普到七普" required = {".shp", ".shx", ".dbf", ".prj"} for d in sorted(os.listdir(root)): full = os.path.join(root, d) if not os.path.isdir(full): continue exts = {os.path.splitext(f)[1].lower() for f in os.listdir(full)} missing = required - exts if missing: print(f"{d}: 缺失 {missing}")

这段脚本遍历每一年的子目录,把实际扩展名集合和四件套比对,缺失内容一目了然。注意 .shx 在 ArcGIS 里如果丢了,软件有时会自动重建,但 QGIS 和大部分命令行工具不一定有这个能力;缺 .prj 则意味着坐标系未知,后续任何投影转换都是在赌。所以四件套检查是 shp 数据落地前面最基础的一次体检。

2.3 用 GDAL/OGR 做 shp 体检:字段类型和几何类型决定你能做什么分析

四件套齐了还不代表能直接用,我会再用 GDAL 检查一遍几何类型和字段定义。老历史数据的坑在于字段类型很随意:有的年份“人口”字段是字符串,里面还可能混着逗号;有的年份几何是带 Z 值的 25D 多边形,做空间连接时容易报错。先看统计信息再动手,能少踩很多坑。

ogrinfo -so -al "一普到七普/七普2020/七普县界.shp"

-so表示只读取统计信息,-al表示列出所有图层。这条命令会输出要素个数、几何类型、空间参考和字段列表。我通常会顺手再用 Python 把字段名和类型拉一遍,确认哪些字段可以直接做数值计算,哪些需要清洗。

from osgeo import ogr ds = ogr.Open("一普到七普/七普2020/七普县界.shp") layer = ds.GetLayer(0) print("要素数:", layer.GetFeatureCount()) print("几何类型:", layer.GetGeomType()) defn = layer.GetLayerDefn() for i in range(defn.GetFieldCount()): fd = defn.GetFieldDefn(i) print(fd.GetName(), fd.GetTypeName())

这一步尤其要留意几何类型:产出为wkbPolygon25D时,后续做面积计算和空间连接都带着 Z 值,部分空间索引会失效;字段类型为String的“人口”列则必须先转数值才能做统计。老数据里还有一类常见现象是字段名重复或者带空格,这多半是当年从 FoxPro 转出来的历史遗留,后续做属性映射时要单独处理。体检结束,才算真正“打开”了这套数据。

3. 七次普查区划代码漂移:用市级代码做锚点做归一化匹配

3.1 为什么直接按县名和代码 join 会翻车

七次普查跨越 67 年,中国行政区划经历了大规模调整。第三次普查(1982 年)开始有 GB 2260 系列六位行政区划代码,但 GB 2260 本身几乎每年都在修订:撤地设市、地市合并、县改区、新设县级市,每一次调整都会废弃一批代码。第四次普查(1990 年)前后的代码体系,和第七次普查(2020 年)相比已经面目全非。

第一次和第二次普查时还没有统一的六位码,多数公开 shp 里的“code”其实是制图者自己编的省份代码加专区代码,比如用两位省码加两位专区码再加两位县码凑成六位。这种自编码和 GB 2260 没有任何对应关系,直接拿它和七普 join 必然大面积落空。即便有六位码的三普数据,也会遇到“同一个县名在不同年份对应不同代码”的现象,比如某个县先改成县级市、后来又设区,代码改了两次,名字倒是没大变。

普查年份编码背景常见坑点
一普 1953无 GB 2260,多为省份+专区自编码六位码含义不明,专区范围与现地级市不对应
二普 1964同上,县域拆分合并频繁部分县名已消失,档案文字编码
三普 1982GB 2260-1980 首版,县改市起步代码修订频繁,同名县市并存
四普 1990GB 2260-1988 版,撤地设市前夕地区与地级市并存,代码重叠
五普 2000GB 2260-1999 版,县改区密集与七普县级对应关系已明显拉大
六普 2010GB 2260-2007 版撤县设区仍在继续,但总体接近
七普 2020GB 2260 后续修订版作为基准底图

所以做这套数据的第一个核心任务不是画图,而是把历史 shp 的行政区划代码“翻译”成七普基准代码。直接按县名 join 也不行,因为“城区”“市辖区”这类名字在同一个地级市里能出现好几个,同名不同代码的情况很常见。

3.2 用市级代码锚定、县级名称匹配:一份最小可用的归一化脚本

我处理这类问题的思路是分级匹配:先用六位码的前四位(地级市)做粗锚点,再用县级名称做模糊匹配。这样能最大限度利用代码本身的层级信息,又不会在县级代码漂移时直接放弃。

先做公共的代码清洗函数,用于统一历史数据和七普基准数据的编码格式:

import re def clean_code(x): if x is None: return None s = str(x).strip().replace("\u3000", "") s = s.replace("0", "0").replace("1", "1").replace("2", "2") num = re.sub(r"\D", "", s) # 去掉所有非数字字符 return num.zfill(6) if num else None

zfill(6)很关键:早期数据里常有“1101”这样的四位代码,本质上代表直辖市的市辖区,补零成“110100”才能和七普代码对齐;反过来,有的代码被 Excel 读成了浮点数2020.0,经过正则去非数字再补零也能还原。清洗完代码后再做锚定匹配:

from rapidfuzz import process, fuzz # base_pairs 是七普 shp 里清洗后的 (code, name) 列表 base_pairs = [(clean_code(r["code"]), r["name"]) for r in seven_records] base_names = [name for _, name in base_pairs] def match_history(row): code = clean_code(row.get("code", "")) if not code: return None # 先用前四位市级代码在七普数据里锚定 same_city = [bp for bp in base_pairs if bp[0][:4] == code[:4]] if len(same_city) == 1: return same_city[0][0] # 市级有多个候选或锚定失败,走县级名称模糊匹配 best_name, score = process.extractOne(row["name"], base_names, scorer=fuzz.WRatio) if score >= 85: return base_pairs[base_names.index(best_name)][0] return None

匹配逻辑分两层:如果七普基准里恰好只有一个县级单元的前四位代码和历史数据相同,说明市级锚定是唯一的,直接采用;否则就退回县级名称模糊匹配。rapidfuzz里的fuzz.WRatio综合了部分比率和乱序比率,对“新城区”和“城区”这类带前后缀差异的行政区划名称,匹配效果比默认的ratio好。85 分这个阈值是经验值,短名称如“城区”“矿区”容易互相误伤,遇到这类名称我会提到 90 以上,宁可匹配不上也不要匹配错。

历史普查数据的名称里还经常带有“省”“市”“自治县”等后缀,匹配前同样需要做清洗:把“某某县”“某某市”“某某区”统一去掉行政通名再匹配,能显著提升准确率。对匹配不上或者分数低于阈值但高于 60 的记录,我不会硬改,而是导出成一个unmatched.csv交给人工核对。这套流程跑完,大部分 2000 年以后的数据能对齐到 90% 以上,1953 和 1964 年的数据则必须有第三步。

3.3 一普到四普没有标准六位码:用人工映射表补上最后一段

对 1982 年以前的数据,市级锚定也不好使,因为当时还没有地级市概念,取而代之的是“专区”和“地区”。专区的管辖范围和今天的地级市并不完全一致,有的专区后来拆分成了两个地级市,有的地级市则由多个专区拼成。这时候只能用人工映射表:把每一条历史记录的“省-专区-县”组合翻译成七普的最可能归属地级市和县。

我一般会维护一张 CSV 映射表,字段包括source_code, source_name, source_year, target_code, verified。先从公开的普查资料汇编和历年行政区划简册里把专区沿革梳理出来,再将能在档案里确认归属的记录标记为verified=1,确认不了的保留空值,不在代码里硬猜。这个映射表看起来笨重,但对于 1953 年和 1964 年两期普查几乎是唯一可靠路径。跑完这一步,七次普查的县级数据才真正有了统一的代码基准。

4. 属性字段与人口口径:从普查公报校准总量

4.1 字段长什么样,先看清楚再动手

归一化代码之后,下一个问题是属性字段。不同年份的 shp 里字段命名风格差异很大,早期数据常见全中文缩略,后期则是英文缩写。共通字段通常是行政区划代码、名称、总人口、分性别人口、城乡人口等。我建议先建一张字段映射表,把各年份的字段名统一映射到标准名,否则后面做面板数据时列名对不上非常难受。

标准字段名通常的原始写法类型含义
codeCODE、行政区划代码、区码字符型六位行政区划代码
nameNAME、名称、县名字符型县级单位名称
pop_totalPOPTOTAL、总人口、人口数双精度普查总人口(人)
pop_malePOP_MALE、男、男口径双精度男性人口(人)
pop_femalePOP_FEMALE、女、女口径双精度女性人口(人)
pop_urbanURBAN、城镇人口双精度城镇常住人口(人)
pop_ruralRURAL、乡村人口双精度乡村常住人口(人)
yearYEAR、年份整型普查年份,如 2020

字段映射不是可有可无的整理工作,它会直接影响后续所有分析脚本的可复用性。我会把这套映射表存成 JSON 或者一个独立脚本,每次拿到新批次数据就套用,而不是在分析脚本里临时改列名。另外,老数据里常见“总人口”为单位“万人”的情况,字段名里却不写单位,这种必须在映射后统一折算为“人”。

4.2 用普查公报总量做比例校验,而不是直接覆盖

shp 里的属性表数据来源复杂,有的是档案扫描录入,有的是历年资料汇编转制,难免有漏填和错填。我拿到数据后会先和同期的公开普查公报里的分县人口做总量比对,校验口径是否对得上。

import pandas as pd # shp_df: 从 dbf 读出的属性表,经过字段映射和代码清洗 # gazette_df: 从各期普查公报整理的分县人口,至少含 code 和 gazette_pop 两列 df = shp_df.merge(gazette_df, on="code", how="left") df["ratio"] = df["gazette_pop"] / df["pop_total"] # 正常的比例应该接近 1,偏离超过 20% 就要重点排查 df.loc[df["ratio"] < 0.8, "flag"] = "总量明显偏低" df.loc[df["ratio"] > 1.2, "flag"] = "总量明显偏高" print(df.groupby("flag").size())

这里的关键是不直接用公报数据覆盖 shp 里的原始人口,而是先算比例。如果某个县比例严重偏离 1,优先怀疑的是行政代码错位——也就是这个县的代码其实对应另一个县,而不是人口录入错误。我还遇到过整体比例稳定在 1.3 左右的情况,那不是错误,而是口径差异:公报用的是常住人口,shp 里存的是户籍人口。两类口径不同,直接覆盖会把后续分析带偏。

判断口径是否一致有个土办法:看比例的中位数。如果中位数接近 1,说明大多数县级记录和公报对得上,少数异常记录单独查;如果中位数整体偏移到 1.2 或 0.8,就要怀疑口径问题,这时候应该做的不是校准单个县,而是统一全表的统计口径。早几期普查的属性表里“总人口”有时是登记人口,有时是常住人口,字段名看不出来,只能靠公报交叉验证。

4.3 单位统一与数值清洗:一个字段也不能漏

人口字段被 Excel 存成科学计数法、字符串里带逗号千分位、甚至混入全角数字,都属于历史数据的家常便饭。在做比例校准之前,数值清洗必须到位。

for col in ["pop_total", "pop_male", "pop_female", "pop_urban", "pop_rural"]: sr = df[col] if sr.dtype == object: df[col] = ( sr.astype(str) .str.replace(",", "", regex=False) .str.replace(",", "", regex=False) .str.replace("人", "", regex=False) .str.replace(" ", "", regex=False) .astype(float) )

这段代码把常见的千分位逗号、中文逗号、“人”字后缀和空格全部剥离后再转数值。注意.astype(float)之前必须保证字符串里没有非数字残留,否则会抛异常,所以上面的replace链要按实际数据补全。这里我踩过一次坑:某个年份的“总人口”列里有几十条记录用了全角数字,正则在第 3 章的clean_code()里处理的是代码,属性值里的全角数字也得先转半角,否则astype(float)直接报错。数值清洗完毕,这套数据才具备做定量分析的基本条件。

5. 避坑案例:shp 打不开、中文乱码、join 空值、渲染卡顿

5.1 dbf 中文乱码:属性表全是问号和乱码

现象:在 QGIS 里打开某个年份的 shp 后,属性表的中文县名全部变成“锟斤拷”或者一串问号,但几何边界显示正常。

原因:dbf 是上世纪 90 年代格式,老数据的字符编码多为 GBK 或 GB2312,而现代 GIS 默认按 UTF-8 解码。编码不匹配导致中文全部解析失败。

解决:先确认 dbf 实际编码,再用环境变量强制指定。Linux 和 Windows 下都可以在打开数据前设置:

export SHAPE_ENCODING=GBK

如果是用 Python 的 pyshp 读取,则在初始化时直接指定编码:

import shapefile sf = shapefile.Reader("一普到七普/四普1990/四普县界.shp", encoding="gbk")

更彻底的做法是把 dbf 里的中文统一转成 UTF-8:用 GDAL 的ogr2ogr将原 shp 转一份新文件,输出时指定编码,转完用 UTF-8 打开就不再乱码。要注意的是,部分数据集里不同年份用了不同编码,不能一刀切全按 GBK 处理,先读每条记录的原始字节判断编码更稳妥。

5.2 解压后 shp 打不开:缺 .shx 是最常见的暗坑

现象:zip 解压后目录里只有 .shp 和 .dbf,拖进 ArcMap 提示“Failed to open”,QGIS 里则只显示边界却不显示属性表。

原因:下载包被分包传输时丢了三件套里的 .shx。没有 .shx 时,老版本 ArcGIS 无法读取几何索引,直接拒绝打开。QGIS 相对宽容,部分版本可以自动重建索引,但属性关联经常出问题。

解决:不要手动写二进制去补 .shx,最常见的可靠做法是用 QGIS 打开(只要能显示边界),然后另存为一个新 shp,QGIS 会自动生成完整的 .shx、.prj 和 .cpg。命令行下也可以尝试 GDAL 的ogr2ogr直接转存,前提是源文件里的 .dbf 字段结构完整,否则 GDAL 同样会拒绝读取。

5.3 按 code 做 join 后大面积空值:代码列藏了看不见的字符

现象:把七普属性表和历史年份属性表按 code 合并,结果超过一半行的目标字段是 NaN,单独抽查却看不出代码有什么异常。

原因:历史数据里的 code 列看着是“110101”,实际字符串里可能带全角空格、不可见控制字符,或者是 pandas 把代码读成了浮点数“110101.0”。肉眼比对一模一样,程序比对却完全不同。

解决:在做任何 join 之前,强制对所有代码列跑一遍清洗函数,也就是第 3 章里的clean_code()。先把全角空格替换掉、把全角数字转半角、去除非数字字符、最后zfill(6)补零。浮点数问题用str(x).split(".")[0]截断也可以,但更通用的做法还是正则去非数字。这一步放进所有数据加载流程里,join 空值率会大幅下降。

5.4 图层加载正常但缩放渲染卡顿:动态投影拖慢速度

现象:数据在 QGIS 里能打开,县界也能显示,但只要一缩放或平移,整个界面都卡,CPU 占用拉满。

原因:这套数据的坐标系五花八门,有的年份是旧北京 54 坐标系,有的是西安 80,还有的直接缺失投影信息。QGIS 默认地图投影是 Web Mercator,每次重绘都要把整个县界数据做动态投影重算,数据量大时自然卡顿。

解决:把矢量数据从 shp 转成 GeoPackage 格式,用ogr2ogr一条命令就能完成:

ogr2ogr -f GPKG 七普县界.gpkg 七普县界.shp -nlt PROMOTE_TO_MULTI

GeoPackage 是单文件 SQLite 存储,读取多边形比文件型 shp 快,而且支持空间索引。-nlt PROMOTE_TO_MULTI把所有单多边形统一提升为多多边形,虽然对绘图结果没有影响,但后续做空间连接时不容易因为几何类型不一致报错。如果只需要做静态出图,也可以先用-t_srs EPSG:3857把数据预投影转到 Web Mercator,省掉运行时动态投影的每一帧开销。

6. 进阶验证:导出 WKT 并用七普乡镇网格交叉检查历史县级数据

6.1 把 shp 格式矢量数据导出为 WKT 与 GeoJSON

数据处理完成之后,免不了要把矢量数据交给其他系统使用。shp 格式在空间数据库和 Web 前端里都不太友好,常见做法是导出成 WKT 文本或 GeoJSON。用ogr2ogr一行命令就能搞定:

ogr2ogr -f CSV 七普县界_wkt.csv 七普县界.shp -lco GEOMETRY=AS_WKT -t_srs EPSG:4326 ogr2ogr -f GeoJSON 七普县界.geojson 七普县界.shp -t_srs EPSG:4326

-lco GEOMETRY=AS_WKT会在 CSV 里加一个包含完整 WKT 字符串的几何列,方便直接导入 PostgreSQL 或做文本匹配;-t_srs EPSG:4326输出前统一转成 WGS84 经纬度,避免 WKT 里出现大量米制坐标。导出 GeoJSON 时同理,先统一坐标系再输出。这个小步骤看起来不起眼,却是很多后续 Web 可视化和空间数据库入库工作的前置条件。

6.2 用七普乡镇级数据反推历史县级数据,验证归一化结果

最后一道验证,我习惯用七普的乡镇级人口边界来交叉检查历史县级数据。思路并不复杂:把七普乡镇边界和历史县级边界做空间叠加,按面积比例把历史县总人口分摊到乡镇切片,再把同一乡镇的切片汇总,得到一套“反推的历史乡镇人口”。反推结果如果和同期公报趋势差异过大,通常说明县级代码或边界匹配出了问题,需要回到第 3 章的归一化流程重新排查。

# 伪代码:示意反推思路,实际用 geopandas 处理 # sub: 七普乡镇级 shp,含乡镇人口 # hist: 历史县级 shp,含县级人口 # 1. 对 hist 每个县,找出其覆盖的乡镇切片 # 2. 按切片面积占县级面积的比例,分摊县级人口到切片 # 3. 按乡镇汇总所有切片人口,得到反推乡镇人口

这里只做验证,不做精确反演,因为人口分布不均匀,面积权重本身就带有误差。我会把差异率超过 30% 的乡镇单独拉出来看边界和代码,通常能找出早期普查数据里“县界重叠”或“代码张冠李戴”的问题。这套流程跑下来,七次普查数据才算真正可信可用。

回头看我最初处理这套数据的经历,最深的教训就是:历史普查数据的价值在于时间序列,而时间序列的命门是行政区划代码的对齐。代码清洗和人工映射表这些工作枯燥且不显眼,却能决定后面所有分析的生死。现在每拿到一批新数据,我都会把 zip 校验、四件套检查、code 清洗和公报校准写成固定流程,先跑完再谈分析。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询