简介:这份芜湖市建筑物矢量数据包,面向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", dtype=str) # 建筑类型合并:先把原始值去空格,再做白名单映射 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({"混合": "混合结构", "框架": "框架结构"})这段代码里有两个关键细节。dtype=str必须在读 Excel 时就指定,否则门牌号“0012”会被读成数字 12,地址信息直接丢了。.str.strip()去掉不可见空格,这类空格在复制粘贴出来的建筑表格里几乎必然存在,不处理的话“住宅 ”和“住宅”会被统计成两个类别。clean_year里我只匹配 19 或 20 开头的四位年份,避免把楼栋编号里的“802”之类误判成年份,正则匹配失败就返回None,后续在数据库里用year_built IS NULL统一排查,而不是替原始数据编一个年份。
清洗完一定要跑一遍df["building_type"].value_counts(dropna=False),看看有没有映射之后仍为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, rcond=None)[0] coef_lat = np.linalg.lstsq(A, b_lat, rcond=None)[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"]), axis=1))这套拟合的思路是把经纬度看作平面坐标做仿射变换。三个控制点能求一组最小二乘解,控制点越多越好,我在芜湖项目里常用六个点,覆盖城东、城西、江南、江北,拟合残差控制在五米以内。两点需要注意:控制点必须覆盖整个数据集范围,只用一个街道的点做外推,到城郊误差会迅速放大;拟合参数只对芜湖这种几十公里尺度的城市有效,换城市就要重新采控制点,不要想着一套参数全国通用。
坐标纠偏结束后要立刻做空间校验。方法很简单,把转换后的点位导进 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,直接上 PostGIS
MySQL 从 8.0 起也有空间数据类型和空间索引,但和 PostGIS 一比差距明显。PostGIS 提供完整的空间函数体系,支持ST_Transform做 SRID 实时转换,支持ST_DWithin按真实距离过滤,还有ST_Centroid、ST_Buffer这些建筑分析高频函数。建筑数据做片区统计时,经常要按公里范围做缓冲区查询,这类查询在 PostGIS 里写得很顺手,在 MySQL 里就要绕路。
从生态看,QGIS 原生支持直接连接 PostGIS,ArcGIS 也支持,数据入库后能直接在桌面端出图。而且 PostGIS 的geography类型能正确处理地球曲率,芜湖虽然城市不大,但跨几十公里做距离计算时,直接用平面坐标算会有几十米误差。选 PostGIS 不是因为它更高级,而是它是这个领域的事实标准。
| 能力 | PostgreSQL + PostGIS | MySQL 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", schedule="@monthly", start_date=datetime(2024, 1, 1), catchup=False) as dag: t1 = PythonOperator(task_id="load", python_callable=load_new_building) t2 = PythonOperator(task_id="validate", python_callable=validate_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 技巧,而是把每一件重复的事都固化成校验规则的耐心。数据会更新,建筑会新建,规则也要跟着版本走。希望这套思路帮你也少踩几个坑。
本文还有配套的精品资源,点击获取