
简介这份芜湖市建筑物矢量数据包面向GIS学习者、城市规划与地理信息研究人员可用于地图展示、建筑空间分布、密度测算及土地用途分析。压缩包内共有5个文件包括存储几何形状的shp、记录建筑用途/年份/面积等属性的dbf、提供空间索引的shx、坐标投影prj以及元数据xml各文件协同构成完整的地理空间记录整个压缩包约1.71MB体积小巧便于在ArcGIS、QGIS等平台快速加载与二次处理。该资源已有151人学习下载。其核心价值在于将建筑物轮廓与属性信息关联既支持统计不同类型建筑数量、分析各年代建筑分布也能结合遥感影像开展光照、绿化覆盖率等叠加评估还可借助建筑密度与形态特征判别住宅区、商业区、工业区为城市更新、遗产保护及规划决策提供科学依据。1. 芜湖建筑数据从一张残缺表格到一套可用的建筑数据底座我去年接到一份芜湖市镜湖区、弋江区范围内的建筑数据Excel 表格有好几万行字段看着齐全楼栋名称、地址、建筑类型、结构类型、总层数、建筑面积、竣工年份、经纬度一应俱全。可真正打开扫了一遍才知道这份“建筑数据”离可用差得远坐标是互联网地图常用的 GCJ-02地址有的只到路名、有的到门牌建筑面积有的写平方米、有的写着亩建筑类型里“住宅”“居住”“商品房”混在一起竣工年份从“2009”到“二〇〇九年”都有。直接拿去分析就是一场灾难。这个标题要解决的问题就是把这些“原生状态”的建筑数据清洗、纠偏、入库、切片最终变成一套可检索、可做空间分析、可持续更新的城市建筑数据底座。做的事不依赖某个特定平台核心是 Pandas 清洗、PostGIS 空间化和 QGIS/ECharts 出图这条标准链。适合刚接手城市建筑数据、要做 GIS 叠加分析或建筑能耗评估的从业者也适合想自己搭一套建筑数据管线的团队参考。2. 先把数据洗干净用 Python 处理建筑数据的三个必做步骤建筑数据区别于一般业务数据的最大特点是字段体系极不统一。同一批数据从住建、规划、测绘三个口子出来字段名和枚举值能差出十几个版本。不先做字段级标准化后面做连接、聚合、空间分析都会反复返工。我做这件事的顺序固定是三步字段枚举统一、坐标纠偏、生成唯一键缺一个后面都会翻车。2.1 字段级标准化统一建筑类型、结构类型和年份格式拿到原始表第一件事不是在数据库建表而是先用 Pandas 把字典字段全部打平。建筑类型、结构类型这种字段必须做白名单映射否则后面 GROUP BY 出来的结果没法看。我用一段脚本把原始表里的常见写法收拢成四类住宅、公建、工业、其他。import pandas as pd import re df pd.read_excel(wuhu_building_raw.xlsx, dtypestr) # 建筑类型合并先把原始值去空格再做白名单映射 type_map { 住宅: 住宅, 居住: 住宅, 商品房: 住宅, 公寓: 住宅, 办公楼: 公建, 写字楼: 公建, 商业: 公建, 厂房: 工业, 仓库: 工业, 库房: 工业, } df[building_type] df[building_type].str.strip().map(type_map) # 竣工年份清洗兼容 2009、2009年、建成年份2009 这类文本 def clean_year(v): if pd.isna(v): return None s str(v).strip() if s.endswith(年): s s[:-1] if len(s) 4 and s.isdigit(): return int(s) m re.search(r(19|20)\d{2}, s) return int(m.group()) if m else None df[year_built] df[year_built].apply(clean_year) # 结构类型统一把简写补齐避免 混合 和 混合结构 变成两个值 df[structure] df[structure].replace({混合: 混合结构, 框架: 框架结构})这段代码里有两个关键细节。dtypestr必须在读 Excel 时就指定否则门牌号“0012”会被读成数字 12地址信息直接丢了。.str.strip()去掉不可见空格这类空格在复制粘贴出来的建筑表格里几乎必然存在不处理的话“住宅 ”和“住宅”会被统计成两个类别。clean_year里我只匹配 19 或 20 开头的四位年份避免把楼栋编号里的“802”之类误判成年份正则匹配失败就返回None后续在数据库里用year_built IS NULL统一排查而不是替原始数据编一个年份。清洗完一定要跑一遍df[building_type].value_counts(dropnaFalse)看看有没有映射之后仍为NaN的值。最常见的原因是原始值里混入了全角括号、异体字这类数据需要单独列出来人工看。这一步要的是可追溯不是一次映射干净。我在项目里会把 type_map 单独存成 YAML方便下次拿到新一批数据时直接复用也方便别人知道“商品房”为什么被归进了“住宅”。2.2 坐标纠偏把建筑点位从火星坐标转到 WGS-84 并做空间校验建筑数据里带坐标的十有八九是直接从互联网地图或手机采集端拿回来的坐标基准是 GCJ-02俗称火星坐标。它和测绘部门提供的 WGS-84 或 CGCS2000 标准数据叠加时点位会整体偏移几百米。这个偏移不是固定值在不同城区方向还不一样所以不能简单减一个常量。我一般不做网络上流传的公式纠偏那种公式在大范围跨省时会抖动城市级数据用控制点拟合更稳。操作是在芜湖城区选至少三个均匀分布的已知点既要有 GCJ-02 坐标又要有从标准地形图上读出的 WGS-84 坐标然后做线性最小二乘拟合。import numpy as np # 控制点格式(gcj_lng, gcj_lat, wgs_lng, wgs_lat) control_points [ (118.377, 31.326, 118.3735, 31.3248), (118.412, 31.352, 118.4087, 31.3509), (118.388, 31.365, 118.3846, 31.3637), ] A np.array([[p[0], p[1], 1] for p in control_points]) b_lng np.array([p[2] for p in control_points]) b_lat np.array([p[3] for p in control_points]) coef_lng np.linalg.lstsq(A, b_lng, rcondNone)[0] coef_lat np.linalg.lstsq(A, b_lat, rcondNone)[0] def gcj_to_wgs(lng, lat): lng_w coef_lng[0] * lng coef_lng[1] * lat coef_lng[2] lat_w coef_lat[0] * lng coef_lat[1] * lat coef_lat[2] return lng_w, lat_w df[lng_wgs], df[lat_wgs] zip(*df.apply( lambda r: gcj_to_wgs(r[lng], r[lat]), axis1))这套拟合的思路是把经纬度看作平面坐标做仿射变换。三个控制点能求一组最小二乘解控制点越多越好我在芜湖项目里常用六个点覆盖城东、城西、江南、江北拟合残差控制在五米以内。两点需要注意控制点必须覆盖整个数据集范围只用一个街道的点做外推到城郊误差会迅速放大拟合参数只对芜湖这种几十公里尺度的城市有效换城市就要重新采控制点不要想着一套参数全国通用。坐标纠偏结束后要立刻做空间校验。方法很简单把转换后的点位导进 QGIS叠加一份公开的影像底图随机抽二十栋建筑看是否落在对应楼栋轮廓内。这一步不做后面入库、出图全是白干偏移几百米的数据在楼栋粒度分析上是灾难。2.3 地址补全与唯一键生成为什么我坚持给每栋建筑生成 UID建筑数据的地址字段是最难用的字段没有之一。有的写到“镜湖区文化路”有的写到“XX 小区 3 栋”还有的直接是空的。地址清洗没有银弹常规做法是先把路名校验一遍建筑名称里带小区名的用小区名补齐路名。更重要的其实是唯一键。我坚持给每栋建筑生成一个业务无关的 UID而不是直接用楼栋名称或地址做主键因为名称可能重复、地址格式随时会变只有 UID 能跨版本稳定关联。import hashlib def gen_uid(building_name, address, lng, lat): raw f{building_name}|{address}|{round(lng, 5)}|{round(lat, 5)} return WH hashlib.sha1(raw.encode(utf-8)).hexdigest()[:14].upper() df[uid] [ gen_uid(n, a, lng, lat) for n, a, lng, lat in zip(df[name], df[address], df[lng_wgs], df[lat_wgs]) ]UID 的生成规则看起来简单里面有几个原则。前缀 WH 表示芜湖后面是 SHA-1 摘要的 14 位十六进制碰撞概率对于十万栋级别的建筑足够低。关键在round(lng, 5)保留五位小数对应约一米精度这样同一栋建筑在不同批次数据里坐标有几米抖动时UID 依然不变数据更新时能关联上。同时它保留了坐标区分能力两栋贴着的建筑不会因为坐标被过度舍入而生成同一个 UID。我踩过的一个坑是用地址做关联键结果住建局第二批数据把“文化路 18 号”改成了“文化路 18-1 号”关联直接断掉。引入 UID 之后后续所有增量更新、手工修正、空间校验都围绕 UID 做历史数据能追溯错误能回滚。这个字段建议放在表格第一列入库时直接设为主键。3. 入库与空间化用 PostgreSQL PostGIS 装下芜湖建筑数据清洗完的数据还躺在 CSV 里接下来要入库。建筑数据有天然的空间属性普通的关系型数据库能存但做空间查询会很吃力。我的选择是直接上 PostgreSQL PostGIS这一步选型能省掉后面大量做空间分析的麻烦。3.1 选型理由为什么不用 MySQL直接上 PostGISMySQL 从 8.0 起也有空间数据类型和空间索引但和 PostGIS 一比差距明显。PostGIS 提供完整的空间函数体系支持ST_Transform做 SRID 实时转换支持ST_DWithin按真实距离过滤还有ST_Centroid、ST_Buffer这些建筑分析高频函数。建筑数据做片区统计时经常要按公里范围做缓冲区查询这类查询在 PostGIS 里写得很顺手在 MySQL 里就要绕路。从生态看QGIS 原生支持直接连接 PostGISArcGIS 也支持数据入库后能直接在桌面端出图。而且 PostGIS 的geography类型能正确处理地球曲率芜湖虽然城市不大但跨几十公里做距离计算时直接用平面坐标算会有几十米误差。选 PostGIS 不是因为它更高级而是它是这个领域的事实标准。能力PostgreSQL PostGISMySQL 8.0 Spatial坐标系实时转换ST_Transform 支持任意 SRID 互转支持有限转换函数不完整真实距离查询ST_DWithin(geography) 按米计算需手动换算查询易错空间索引类型GiST 索引复杂查询稳定R-Tree 索引场景简单QGIS / ArcGIS 连接QGIS 原生支持ArcGIS 有驱动需通过 ODBC 桥接不常用社区资料与函数生态函数几百个建筑分析方案多函数少新特性依赖版本这个表不是比谁功能全而是突出建筑数据最常用的能力按距离筛选、按范围聚合、按坐标转换。我见过不少团队用 MySQL 建库做到后面为了算一个“3 公里内建筑密度”被迫把坐标全捞出来在 Python 里算性能和可维护性都吃亏最后还是要迁到 PostGIS。3.2 建表与导入把清洗后的 CSV 变成带空间索引的建筑表库选定了建表语句要按建筑数据的查询习惯来设计。空间字段用 geometry 类型并指定 SRID 为 4326这是 WGS-84 经纬度的标准编号。楼层用 smallint面积用 numeric年份用 smallint这些都能省存储空间。主键直接用上一步生成的 UID。CREATE TABLE wuhu_building ( uid varchar(16) PRIMARY KEY, name text NOT NULL, address text, building_type varchar(16), structure varchar(16), year_built smallint, floors smallint, area_sqm numeric(12, 2), geom geometry(Point, 4326), data_source text, updated_at timestamptz DEFAULT now() ); CREATE INDEX idx_wuhu_building_geom ON wuhu_building USING GIST (geom); CREATE INDEX idx_wuhu_building_year ON wuhu_building (year_built);geometry(Point, 4326) 里的 4326 必须显式声明很多导入工具默认不写 SRID导致后面空间查询时坐标系不一致。GIST 索引是空间查询的生命线没有它几万条数据做一次ST_DWithin全表扫描要几百毫秒加了索引能压到个位数毫秒。year_built 上建普通 B-Tree 索引是因为按年代过滤是高频操作PostGIS 的查询计划器会自动选择合适的索引。导入我常用shp2pgsql psql的组合。如果数据带属性没带空间字段也可以先普通 COPY 进临时表再用ST_SetSRID(ST_MakePoint(lng, lat), 4326)生成 geom。这里有个操作细节如果前面清洗好的 CSV 里经纬度列还没变成 geometry就得在 SQL 里写一段更新。# 把 shp 文件导入 PostGIS-s 指定源坐标系为 4326 shp2pgsql -s 4326 -g geom wuhu_building.shp wuhu_building | psql -U postgres -d wuhu导入完成后第一件事是验证行数和样本数据我一般会跑一句SELECT count(*), count(geom) FROM wuhu_building;两个 count 应该相等如果不相等说明有些行 geom 是空的通常来自经纬度缺失。这个检查不能省后面做空间分析时空点会让聚合结果悄悄变少。3.3 空间拓扑检查找出重叠建筑和离群点位的数据校验 SQL数据入库不代表数据可信。建筑数据最常见的空间问题是重叠点同一栋建筑被采集了两次或者坐标纠偏后两栋原本不同的楼叠在一起。另一个问题是离群点明明应该在市区的建筑跑到郊外几十公里多半是坐标转换时那条记录控制点没覆盖到。这两类问题用 SQL 能直接排查。-- 找到距离小于 5 米的建筑对疑似重复记录 SELECT a.uid, b.uid, ST_Distance(a.geom::geography, b.geom::geography) AS dist FROM wuhu_building a JOIN wuhu_building b ON a.uid b.uid WHERE ST_DWithin(a.geom::geography, b.geom::geography, 5) ORDER BY dist; -- 找到周围 500 米内没有其他建筑的离群点位 SELECT uid, name, geom FROM wuhu_building AS b WHERE NOT EXISTS ( SELECT 1 FROM wuhu_building AS n WHERE n.uid b.uid AND ST_DWithin(n.geom::geography, b.geom::geography, 500) );第一句里的a.uid b.uid是为了避免同一对建筑被查两遍。ST_DWithin的第二个参数是距离单位由传入类型决定传::geography单位是米传 geometry 类型单位是度。这是新手最容易踩的坑直接填个 5 以为是 5 米实际是 5 度也就是大概五百公里会把全城建筑都查出来。第二句查离群点时500 米这个阈值可以根据城市建筑密度调整市区建筑间距通常小于 100 米郊区可能 300 米内没有邻栋阈值太小会把正常郊区建筑也标成异常。这些 SQL 检查完成后我会把结果导入一个wuhu_building_check表标记出重复、离群、空 geom、楼层年矛盾四类问题。这个表在后续数据更新时要重新跑结果和上一次对比看新增了哪些问题点。重点不在于一次清干净而是让每一条有问题的数据都可追踪。4. 从表格到地图用 QGIS ECharts 做建筑数据初筛建筑数据入库后下一步是把数据“看”出来。空间数据只靠表格和 SQL 做分析效率很低。我的常规做法是 QGIS 做空间分布类分析ECharts 做属性统计类图表两者配合能把建筑数据的特征快速摸一遍。这一阶段的目标不是出最终成果而是发现数据里的异常模式。4.1 QGIS 里做建筑高度分布热区QGIS 连接 PostGIS 是标准操作连接参数填好数据库地址、库名 wuhu、用户名密码加载 wuhu_building 图层。加载后我要做的第一件事不是出图而是把图层的坐标系设置成 WGS-84 / Pseudo-Mercator也就是 EPSG:3857。这个投影下距离和面积估算接近真实能避免直接用经纬度图层看扁形分布。楼层字段做分级渲染时我一般先用“自然间断点法”分成五级。这个方法的特点是类内方差小、类间方差大比手动分区间更能反映数据的真实分布。芜湖老城区多为多层住宅楼层集中在 5 到 7 层滨江一带新建高层能到 30 层以上自然间断点能把这些密度变化体现出来。分级后如果某个颜色块在空间上特别集中就要怀疑那个片区有异常高密度开发比如同一个小区被合并成了一个大面这时回到 SQL 查那个范围内的 uid 清单。QGIS 还有一个用途是做人工抽检。把底图换成卫星影像随机抽 50 个点用“识别”工具点开属性核对楼栋名称和楼层数跟影像是否一致。这一步能发现坐标纠偏没纠干净的点位也能发现某些小区在数据里被写成了整片建筑群而不是单栋楼。人工抽检不用全查重点是边缘区域和楼层特别高的点。4.2 ECharts 展示建筑年代与楼层分布空间分布用 QGIS属性分布我习惯用 ECharts 快速画图。原因是 ECharts 的出图速度和交互体验比 QGIS 打印地图快适合在数据清洗过程中反复看分布形状。读接口做聚合比直接读全量数据快得多后端用一条 SQL 按年份分组统计。// 后端接口返回 [{ year: 2005, count: 123 }] 这样的结构 const res await fetch(/api/building/year-dist); const data await res.json(); const chart echarts.init(document.getElementById(main)); chart.setOption({ xAxis: { type: category, data: data.map((d) d.year), name: 竣工年份 }, yAxis: { type: value, name: 建筑数量 }, series: [ { type: bar, data: data.map((d) d.count), itemStyle: { color: #2f7ed8 } } ], tooltip: { trigger: axis } });这段配置里没有多余的功能但有两个细节值得注意。tooltip.trigger设成axis能让鼠标滑过时同时显示相邻年份的数据比默认的item更适合看趋势。itemStyle.color固定为一个蓝色因为年代分布图的重点是形状而不是颜色渐变颜色太花哨会影响对峰值的判断。数据返回前我一般会先确认聚合 SQL 用的是year_built而不是year_built::text否则排序会变成字典序2006 会排在 2000 前面。看完年代分布再看楼层分布会发现很多数据矛盾。一栋 8 层的住宅楼写着总高 12 米一栋 2005 年的公建写着 2018 年竣工这类矛盾在 ECharts 上会表现为离群柱或断裂的折线。发现了就回到数据库逐条核实不要直接改因为可能是原始表字段串位了。建筑数据的质量就是在这样一轮一轮“看图看库”中提起来的。5. 芜湖建筑数据落地避坑我踩过的五个坑和排查清单建筑数据项目做得越多越发现坑不在技术难点上而在那些看着不起眼的数据细节里。下面五条是我在芜湖建筑数据项目里真实踩过、也真实花时间排查过的记录每条按现象、原因、解决展开希望能帮你少走一段弯路。5.1 坐标系统一一个字段没写对导致全图偏移几百米现象QGIS 里叠加建筑点和遥感影像底图所有点位统一往东北方向偏移约三百米整个城区的建筑点都悬在马路和绿地上。原因原始 shapefile 里坐标确实是 GCJ-02但导入 PostGIS 时我图省事直接用了shp2pgsql -s 4326把坐标系写成了 WGS-84等于告诉数据库“这些点就是标准经纬度”。坐标本身没变是标注骗了数据库。解决导入前先确认源文件坐标系用 QGIS 打开原始数据后右键图层属性查看坐标系千万别凭文件名猜导入时-s参数写源坐标系导入后再用ST_Transform把 geometry 从 4326 转到目标坐标系或者在清洗阶段先统一转好再入库。5.2 建筑类型枚举混乱“住宅”与“居住”到底怎么合并现象统计建筑类型分布时住宅、居住、商品房、职工宿舍、商住两用各占一块住宅类数据被别人质疑统计口径不准。原因原始数据来自不同部门同一栋楼在不同口径里可能叫“住宅”也可能叫“居住”而且“商住两用”这种混合类型不该被粗暴归进某一类。解决先跑SELECT building_type, count(*) FROM wuhu_building GROUP BY building_type把所有枚举值拉全建立映射表时把“职工宿舍”归到“住宅”、“商住两用”单独拆成一个字段is_mixed而不是强制塞进住宅或公建。枚举清洗要讲究能拆不清不能只求数量对得上。5.3 楼层与高度互相矛盾数据体检脚本怎么设计现象一张分析报告里某栋 18 层的高层住宅被算成总高 12 米导致日照分析结果异常整张图被人质疑。原因原始表的“高度”字段填的是建筑底面海拔高度不是建筑总高还有的记录把层高 3.2 米误乘成了 0.32。解决我在校验 SQL 里加了一条规则height floors * 2.5 OR height floors * 5的疑似矛盾记录全部捞出来人工核验。2.5 米和 5 米是居住建筑层高的合理边界超出这个范围基本是数据错误。这类校验规则要沉淀成一张规则表每次数据更新自动跑一遍。5.4 刷新数据把手工修正覆盖了版本管理怎么救回来现象第二轮拿到新增数据后导入更新QA 发现有 200 多条之前手工修正的建筑名称和楼层被新数据覆盖修正全部白做。原因更新时用了 DELETE 全量重新 INSERT 的粗暴方式把上一轮的人工成果直接清掉了。解决把人工修正单独存一张wuhu_building_overrides表字段包括 uid、字段名、修正值、修正人、修正时间更新流程先导入新数据再用 overrides 表通过UPDATE ... FROM把修正值覆盖回来。这样每次刷新数据人工修正永远有后悔药。5.5 面积字段单位混用平方米与亩的灾难现象汇总建筑面积时用 SUM 算出来一个离谱的结果总量比芜湖全城建筑面积还大好几倍。原因原始 Excel 不同 sheet 的单位不一样住宅类用平方米几个工业地块用亩汇入同一列后面积直接膨胀。解决入库脚本里统一做一次单位换算凡是从原始表“亩”字段来的数据先乘 666.67 再写入area_sqm并在字段注释里写明单位。面积字段命名强制带单位后缀_sqm从命名层面杜绝再发生一次。这个时候我意识到数据清洗的底线不是把数据变对而是让错误数据能在合入之前就被看到。6. 让建筑数据持续自己跑起来增量更新与质量门禁所有清洗和入库工作做完芜湖建筑数据项目才走完一半另一半是让它能持续更新。静态 CSV 导入是单次行为真正做城市级建筑数据的人每个月都会拿到新一批测绘或竣工数据手工重复清洗流程一定不可持续。我最后把整套流程封装成一条 pipeline并把质量校验规则变成门禁。6.1 用 Airflow 做按月增量更新增量更新的核心是“只处理变化的行不动历史数据”。我按updated_at字段识别变更Airflow 里建一个按月调度的 DAG先把新数据清洗成标准结构再把新增或变更的行写进wuhu_building_staging临时表最后用一条 UPSERT 语句合入主表。没有 Airflow 的团队用 cron 加 Python 脚本也能做差别只在可观测性和失败重试。from airflow import DAG from airflow.operators.python import PythonOperator from datetime import datetime def load_new_building(): # 读取本批次增量文件清洗后返回标准 DataFrame # 只处理 updated_at 相比上次运行时间之后变化的记录 df_new read_and_clean_incremental() upsert_to_staging(df_new) def validate_and_merge(): # 先跑质量门禁再写入主表 run_quality_gates() merge_staging_to_main() with DAG(wuhu_building_update, schedulemonthly, start_datedatetime(2024, 1, 1), catchupFalse) as dag: t1 PythonOperator(task_idload, python_callableload_new_building) t2 PythonOperator(task_idvalidate, python_callablevalidate_and_merge) t1 t2Excel 里的updated_at可能没有那就在清洗时统一取文件入库时间。增量批次的核心是合并逻辑PostGIS 的INSERT ... ON CONFLICT (uid) DO UPDATE在入库时比 DELETE INSERT 安全得多能保留历史版本也能让 overrides 表继续生效。6.2 把质量校验规则放进 pipeline失败自动告警质量门禁不能只在项目最开始人工看一次要在每次数据更新时都自动跑。我把第五章的五类校验规则全写进一个 SQL 脚本包括坐标偏移量超过阈值、建筑类型枚举不在白名单、楼层与高度矛盾、UID 重复、面积单位异常。pipeline 里加一个检查任务把异常数量和应用阈值比对超过就说FAIL并阻止合入主表。失败时的告警方式我走的是最简单的邮件加企业微信机器人。告警内容不只是“有异常”而是把异常 SQL 的结果聚合成 JSON 发出来接受的人能直接看出是哪个片区、哪类问题。这个改动让数据质量从“事后发现问题”变成“推送问题”把排查时间从几天压到几小时。最深的体会是建筑数据项目做到最后拼的不是 SQL 技巧而是把每一件重复的事都固化成校验规则的耐心。数据会更新建筑会新建规则也要跟着版本走。希望这套思路帮你也少踩几个坑。本文还有配套的精品资源点击获取