
简介一套面向城市规划、GIS分析与建筑研究的芜湖市建筑矢量数据集以Shapefile标准格式组织帮助使用者快速获取建筑几何轮廓与空间属性信息适用于土地管理、城市更新、环境评估等场景。压缩包共5个文件核心为shp几何数据配套dbf属性表、shx空间索引、prj坐标系定义及xml元数据说明整体仅1.71MB轻量易用可直接在ArcGIS、QGIS等常见GIS平台中加载。数据包含芜湖市建筑物的边界、高度、形状以及用途、建设年份、建筑面积等属性信息可开展建筑密度计算、空间分布统计与功能分区识别结合遥感影像还能进行光照、绿化覆盖率等环境因子分析。已有151人学习下载适合需要快速获取基础建筑空间数据的高校师生、规划从业者与地理信息爱好者。除支撑专业研究外开发商可了解地块周边建筑状况规划部门可评估新建项目环境影响公众也能便捷获取城市建设信息为城市更新、遗产保护与公共政策制定提供科学依据。1. 芜湖建筑数据到底缺什么先想清楚要回答哪类现实问题“芜湖建筑数据”听起来像一个文件包实际接手过这类数据的人都知道拆开无非是散落的面文件、一张字段残缺的属性表、可能还有一份坐标对不上的CAD总图。真正有价值的形态是把它整理成能按街道、按年代、按用途批量查询的建筑空间底账。做城市更新评估、区域密度分析或者房屋普查校验的人缺的就是这张底账。这篇笔记讲的是一条落地路径把公开POI属性、已有建筑轮廓、人工抽检三股信息对齐清洗最后落到PostGIS一张表里让“这个片区建筑密度多少、上世纪90年代前的老房子占多少、沿街用途怎么构成”这类问题能用查询直接回答。2. 数据从哪里来POI属性采集与坐标统一的最小闭环2.1 目标表长什么样先定字段再找数据别拿回来再后悔在下载任何文件之前我一般先画一张表。芜湖建筑数据的最终形态是“一栋楼一行”的属性表至少包含这些字段id、name、address、geom、source、use_type、floor_count、height、build_year先定字段不是走形式。name、address、use_type主要靠POI接口和现场调查补floor_count、height靠建筑普查数据或遥感影像推定build_year最费劲需要房屋登记资料和历史影像比对着判断。如果先随便拉几个GeoJSON进来等发现字段缺了再回补大部分时间会耗在两张表之间的空间匹配上后期连“按年代统计”都做不出来。这一阶段还要把数据格式的预期定死。我要求所有几何统一为Polygon或MultiPolygon坐标系统一为WGS84经纬度属性字段名统一小写带下划线不允许出现中文字段名。原因很简单后面进PostGIS时字段名带中文和空格会让SQL写起来极其痛苦坐标不统一各种叠图分析就会变成“看起来对了其实全偏”。2.2 用高德POI补建筑属性requests抓取脚本与参数设置POI接口返回的是点不是建筑面它不能替代轮廓数据。它真正的价值是给建筑“挂牌照”名称、地址、分类。拿到这些点之后再用空间就近匹配挂到你已经有的建筑面上。import requests import time def fetch_poi(keywords, city芜湖, page_size25): url https://restapi.amap.com/v3/place/text items [] for page in range(1, 101): params { key: 你的高德KEY, city: city, keywords: keywords, offset: page_size, page: page, extensions: base, } resp requests.get(url, paramsparams, timeout10) data resp.json() if data.get(status) ! 1 or not data.get(pois): break items.extend(data[pois]) total int(data.get(count, 0)) if page * page_size total: break time.sleep(0.3) # 控制QPS避免触发限流 return items这段代码的逻辑是循环翻页抓取高德文本检索接口直到把某个关键词下的POI全部拉完。参数里值得留意的有两点一是city直接传“芜湖”高德会自动限定区域二是sleep(0.3)必须有实测并发请求容易被临时封掉KEY宁可慢一点也别赌运气。返回的JSON里每条POI包含location、name、type、address等字段location是“经度,纬度”字符串需要拆开转成数值。我一般会按建筑相关分类去抓而不是只抓一个大词。比如“商务住宅”“办公”“科教文化服务”“医疗保健服务”“餐饮服务”每个分类抓一遍再合并去重。这里有个常见的认知偏差POI数量分布极不均匀市区密集、城乡结合部稀疏抓回来的数据会有明显的空间偏差后面分析时要注意加权或者单独说明覆盖范围。2.3 WGS84与GCJ02互转一道必做的数学题高德返回的坐标是GCJ02而很多公开建筑轮廓数据用的是WGS84。直接混用地图上看起来只是偏了百来米做面积计算和边界比对时会差出一个身位。坐标转换的代码早就被总结过但参数容易抄错这里给一份跑通的版本import math def out_of_china(lng, lat): return not (72.004 lng 137.8347 and 0.8293 lat 55.8271) def transform_lat(lng, lat): ret -100.0 2.0 * lng 3.0 * lat 0.2 * lat * lat 0.1 * lng * lat ret 0.2 * math.sqrt(abs(lng)) ret (20.0 * math.sin(6.0 * lng * math.pi) 20.0 * math.sin(2.0 * lng * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(lat * math.pi) 40.0 * math.sin(lat / 3.0 * math.pi)) * 2.0 / 3.0 ret (160.0 * math.sin(lat / 12.0 * math.pi) 320.0 * math.sin(lat * math.pi / 30.0)) * 2.0 / 3.0 return ret def transform_lng(lng, lat): ret 300.0 lng 2.0 * lat 0.1 * lng * lng 0.1 * lng * lat 0.1 * math.sqrt(abs(lng)) ret (20.0 * math.sin(6.0 * lng * math.pi) 20.0 * math.sin(2.0 * lng * math.pi)) * 2.0 / 3.0 ret (20.0 * math.sin(lng * math.pi) 40.0 * math.sin(lng / 3.0 * math.pi)) * 2.0 / 3.0 ret (150.0 * math.sin(lng / 12.0 * math.pi) 300.0 * math.sin(lng / 30.0 * math.pi)) * 2.0 / 3.0 return ret def gcj02_to_wgs84(lng, lat): if out_of_china(lng, lat): return lng, lat dlat transform_lat(lng - 105.0, lat - 35.0) dlng transform_lng(lng - 105.0, lat - 35.0) radlat lat / 180.0 * math.pi magic math.sin(radlat) magic 1 - 0.006693421622965943 * magic * magic sqrtmagic math.sqrt(magic) dlat (dlat * 180.0) / ((6378245.0 * (1 - 0.006693421622965943)) / (magic * sqrtmagic) * math.pi) dlng (dlng * 180.0) / (6378245.0 / sqrtmagic * math.cos(radlat) * math.pi) return lng - dlng, lat - dlat这套公式里的6378245.0是地球半径0.006693421622965943是偏心率平方两个参数错了算出来的偏移会非常离谱。需要注意这个转换是GCJ02转WGS84反方向WGS84转GCJ02不要自己推导网上有另外一套正向公式逻辑正好相反。如果手头数据的底图本身就是GCJ02那就不需要做这步转换强行切到WGS84反而会对不上底图。坐标转换这步是很多翻车的源头处理前先用几栋地标建筑核对一下经纬度再批量跑。2.4 拿到文件后的第一轮体检重复、缺失、范围扫描POI抓回来、坐标系转完之后要先做第一轮体检再合并。我用GeoPandas做速度快、中间能看到每一轮的改动。import geopandas as gpd gdf gpd.read_file(wuhu_buildings_raw.geojson) print(原始数量:, len(gdf)) gdf gdf.drop_duplicates(subset[geometry]) gdf gdf[~gdf.geometry.is_empty] gdf gdf[gdf.geometry.geom_type.isin([Polygon, MultiPolygon])] subset gdf.cx[117.9:118.6, 30.8:31.5] # 芜湖主城区粗筛范围 print(清洗后数量:, len(subset))这段代码做四件事删除完全重复的几何对象、过滤空对象、只保留多边形要素、按坐标范围卡掉明显不在芜湖的区域。cx是GeoPandas的坐标切片参数是[经度最小值:经度最大值, 纬度最小值:纬度最大值]。芜湖主城区落在东经117.9到118.6、北纬30.8到31.5附近这个范围可以帮你把误入的周边城市数据挡在外面。注意这里的筛选是在WGS84坐标下做的如果你还没做坐标转换这组范围参数需要用GCJ02坐标重新核算。体检结果如果发现几千个要素只剩一半先别慌先看是不是POI点被当成面读进来了或者轮廓文件本身就只有局部覆盖。这一步能看到数据的真实底子比入库之后再查错省太多时间。3. 把散面拼成可查库几何修复、属性补全与PostGIS入库3.1 用Shapely修拓扑自相交、重复点与细小缝隙从不同来源拼出来的面肉眼看着没问题机器一检查全是毛病。最常见的三种自相交面、闭合点重复、相邻建筑之间的细小缝隙。自相交是内伤面积计算会因此得出负值或重复值缝隙不做处理空间连接的时候会把本属于一栋楼的要素切成几块。import geopandas as gpd from shapely.validation import make_valid gdf gpd.read_file(wuhu_clean_step1.geojson) print(修复前无效几何数量:, (~gdf.geometry.is_valid).sum()) gdf[geometry] gdf.geometry.apply(make_valid) gdf[geometry] gdf.geometry.buffer(0) print(修复后无效几何数量:, (~gdf.geometry.is_valid).sum())make_valid是Shapely里专门修复杂几何的函数它能把自相交环拆掉、把越界点修正。buffer(0)是处理微自相交和退化面的常用手段参数传0表示不改变几何大小只是让Shapely重算一遍边界。跑完之后再统计无效数量正常情况下应该降到0。这里有一个坑make_valid处理MultiPolygon时可能把内部空洞变成单独的面所以修复后要再用一次geom_type检查确认没有生成奇怪的对象。如果对修复精度有更高要求可以进PostGIS后用ST_MakeValid再跑一遍两边结果基本一致。修复完一定记得重新检查面积有些面修复后面积变化超过5%说明原始几何质量问题比较严重需要回到源数据去看原因。3.2 属性补全楼层、高度缺值如何估算芜湖的建筑属性里楼层和高度字段通常是不完整的。POI没有层数公开轮廓数据里也常常缺floor_count。直接删掉这些行会让后续统计产生严重偏差尤其老旧小区住宅本来就是低层高密度删了等于变向抬高平均层数。import numpy as np gdf[floor_count] gdf[floor_count].replace({0: np.nan}) gdf.loc[gdf[floor_count].isna() (gdf[use_type] 住宅), floor_count] 6 gdf.loc[gdf[floor_count].isna() (gdf[use_type] 工业), floor_count] 2 gdf.loc[gdf[floor_count].isna() (gdf[use_type] 商业), floor_count] 8 gdf[height] np.where(gdf[floor_count].isna(), np.nan, gdf[floor_count] * 3.2)这里的参数是估算值住宅按6层、工业按2层、商业按8层层高按3.2米算。整套估算必须留痕我一般会在表里加一个字段叫estimate_flag凡是用估算规则补过的行都标记为“估算”没有补过的标“实测”。不加这个标记数据交付之后别人会把这些估算值当成真实值用最后算出问题还是找到你头上。更讲究一点的做法是按街道分组取中位数而不是用全城固定值。芜湖老城区和城东新区层数差异很大用全城统一默认值会低估新区高度、高估老城高度。这部分建议做两次第一轮用固定值保证数据完整第二轮按街道分组修正修正完对比两版总建筑面积差异超过5%就说明估算逻辑有问题。3.3 PostGIS入库让数据能用在项目里数据清洗到这一步可以入库了。我统一用PostGIS理由是在多张表做空间连接时PostGIS的查询速度远超GeoPandas而且后续增量更新、团队协作都方便。CREATE TABLE wh_building ( id serial PRIMARY KEY, name text, source text, use_type text, build_year smallint, floor_count smallint, height numeric(6,2), estimate_flag boolean DEFAULT false, geom geometry(Polygon, 4326) ); CREATE INDEX idx_wh_building_geom ON wh_building USING gist(geom);srid4326是WGS84经纬度的标准编号。为什么不选更高精度的投影坐标系因为这是一张底账表后面要做各种跨区划、跨街道的空间连接用经纬度最稳妥。如果项目要求算平方米面积查询时临时转投影坐标系再算就行不影响底表结构。Python写GeoPandas配合to_postgis入库from sqlalchemy import create_engine engine create_engine(postgresql://user:passwordlocalhost/gis) gdf.to_postgis( wh_building, engine, if_existsappend, indexFalse, schemapublic )if_existsappend表示追加写入不要用replace否则会把之前的数据整表覆盖。入库后跑一次总数确认SELECT count(*), round(sum(ST_Area(geom))::numeric, 2) AS total_area FROM wh_building;这里算出来的是球面面积跟平面投影面积有差异但用来核对总数量级够用。入库之后这张表就变成了后续所有分析的地基。4. 排查四个让建筑数据翻车的坐标与属性坑4.1 坑一两张图叠不上错位以百米计现象把轮廓数据和POI点同时放进GQIS轮廓在影像上位置正确POI点却整体偏到隔壁街道或者轮廓本身和官方底图对不上平移几十米后勉强重合。原因两个数据源坐标系不一致。最常见的是高德POI默认GCJ02而轮廓数据来自WGS84混用坐标后经纬度差导致点位偏移。还有一种是数据虽然都标着4326实际生成时用了GCJ02坐标只是标签没改。解决在入库前用几栋地标建筑做坐标抽检。我一般选芜湖站、鸠兹广场、中山路步行街三个点分别取它们的WGS84坐标和高德坐标两套数值做对比偏差超过0.001度就需要先统一坐标系再入库。入库后用这条SQL查验当前数据的SRIDSELECT find_srid(public, wh_building, geom);如果返回的不是4326先改坐标再继续。切记不要相信文件的属性标签要以实测为准。4.2 坑二楼层数缺失导致高度估算全部偏小现象统计芜湖各街道平均建筑高度老城区结果异常偏低很多街道清一色“3层楼”的估算值。原因POI不提供层数轮廓数据里老城区楼栋的floor_count大面积为空估算规则用了单一默认值把原本六七层的住宅楼全部算成了低层而且这些估算值后续被当成了真实值做统计。解决估算结果别写进floor_count原始字段新建一个floor_count_est字段存储估算值原始字段保留空值。做高度分析时以floor_count_est为准但交付时单独说明哪些是估算的。更合理的估算方式是按“街道建筑用途”分组取中位数芜湖各街道发展不均衡固定参数只适合第一轮补空不适合最终交付。4.3 坑三建筑年代属性错位老小区识别失真现象用build_year 1995筛芜湖老旧小区筛出来的名单和城市更新摸底名单对不上有的老小区没进结果有的新建小区反而进了。原因建筑年代数据来源混乱。老房子可能在改造后更新了登记年份把“改造年份”写成了“建成年份”新项目则出现了“规划许可年份”和“实际竣工年份”混用的情况。解决把build_year当成参考字段不要做单一筛选条件。我在老旧小区识别里通常会把“层数低砖混结构年代早”三个条件联合使用层数低可以用floor_count_est 6判断结构类型需要额外数据没有就不硬凑。如果条件允许抽20栋楼对比历史影像把明显错位的年份标记出来至少让统计口径可解释。4.4 坑四增量更新后出现重叠数据越修越碎现象第二次拉取全量POI和轮廓后发现同一栋楼在库里出现两个面一个来自旧数据一个来自新数据面积重叠但边界不完全一致统计时被算成两栋楼。原因两次抓取的数据源对建筑边界的描绘本身存在微小差异直接覆盖更新时旧数据没清干净新数据又追加进来自然重叠。更麻烦的是有些楼是人工修正过的全量更新会把人工成果冲掉。解决入库表里加source_version字段每次更新写入当前版本号。更新前先用空间判断找重叠SELECT a.id, b.id FROM wh_building a JOIN wh_building b ON a.id b.id AND ST_Intersects(a.geom, b.geom) AND ST_Area(ST_Intersection(a.geom, b.geom)) ST_Area(a.geom) * 0.5;重叠面积超过小面积对象50%的直接删掉旧版本或者保留人工修正过的版本。5. 进阶玩法用建筑数据算芜湖街道建筑密度并交叉验证5.1 一条SQL算“街道建筑密度”建筑底账入库之后最直接的成果是算街道级的建筑总量。我用边界表和建筑表做空间连接一条SQL完成SELECT b.name AS block_name, round(sum(ST_Area(a.geom))::numeric, 2) AS floor_area_total, round(sum(ST_Area(a.geom))::numeric / NULLIF(ST_Area(b.geom), 0), 4) AS floor_area_ratio, round(count(a.id)::numeric / NULLIF(ST_Area(b.geom), 0), 6) AS building_density FROM wh_block b LEFT JOIN wh_building a ON ST_Intersects(a.geom, b.geom) GROUP BY b.name, b.geom;这段SQL的逻辑是遍历街道边界找出与之相交的楼栋汇总楼栋面积然后除以街道面积。参数里值得注意的是NULLIF它把面积为0的街道转成NULL避免除零错误LEFT JOIN保证没有楼栋的街道也能出现在结果中。floor_area_ratio严格来说是楼栋占地密度不是官方容积率但用于街道间横向比较已经足够。运行前先确认建筑表和街道边界表的坐标系一致否则相交计算会失败或者结果为空。如果两个表坐标系不同临时用ST_Transform(a.geom, 4326)转换不要在底表上反复改保持底表以WGS84为准。5.2 用统计年鉴做交叉验证SQL出的结果不能光看。我习惯拿芜湖市公开的统计年鉴做一次总量核验用年鉴里公布的“房屋面积”或“建筑竣工面积”和库里面的总面积对比。偏差在10%以内说明数据基本可信偏差超过20%就要警惕是采集遗漏还是坐标系错误。另一种验证是抽片区实测。随机选20栋楼在最新影像上人工确认楼层数和轮廓和库里的floor_count_est比对错3栋以上就说明估算规则需要调错5栋以上就直接放弃这版数据回去检查清洗步骤。5.3 收尾习惯一套数据一份版本说明我个人做完建筑底账最后一道工序是在QGIS里做一张总图建筑库叠加影像按build_year做色带渲染检查有没有明显空白的城区和颜色突兀的片区然后连同投影信息、字段说明、估算规则写一份版本说明。这份说明不写进代码单独存一个markdown文件和数据放一起。前几年我总觉得自己记得住每个字段怎么来的过了两个月再打开完全想不起某个估算值的口径是什么后来把版本说明当成数据交付的一部分再也没出过这种问题。数据做完不只是用来算一个数字要让它能应对“这个片区怎么样”“近几年变化如何”这类追问。日常使用中多看交叉验证少依赖单次抽取的结果。希望帮到你。本文还有配套的精品资源点击获取