
把一张 Excel 表格变成地图上能点、能高亮的区域中间隔着的那个东西通常就是 GeoJSON。地理信息这行做久了会发现真正卡住大多数人的不是渲染而是手上那份数据的坐标系、几何格式、行政边界对不上——而在线制作 GeoJSON 恰好能把这些环节压缩进浏览器里完成不用装 ArcGIS也不用配 Python 环境。这篇内容写给几类人手上有一堆门店客户设备的经纬度想让它们在地图上显示出来的运营和产品同学做数据可视化需要省市区边界轮廓的前端和数据分析同学刚接触地理信息被 Shapefile 那一堆 .shp/.dbf/.prj 文件搞晕的 GIS 入门者以及那些搜过geojson 用什么软件打开、结果被引到一堆看不懂的文档里的朋友。下面我会把坐标系、在线制作路线、工具选择、阿里 geojson 这类行政区划数据的获取、文件瘦身和一堆实际踩过的坑全部摊开讲尽量做到看完能直接上手。1. 坐标系这件事不聊透后面的活基本白干1.1 GeoJSON 的骨架其实只有三种东西很多人第一次打开 GeoJSON 文件会被吓到一屏的方括号和大括号。其实它的规范RFC 7946非常克制几何体一共就七种点、多点、线、多线、面、多面、几何集合。再加两个包装Feature一个几何体 一组属性和 FeatureCollection一堆 Feature 的集合。日常业务里 90% 的场景只会用到三种——点、线、面。结构上记住一句话就够了外面是 FeatureCollection里面装 FeatureFeature 的 geometry 装坐标Feature 的 properties 装业务字段。剩下那一堆括号全是坐标数组。地理信息数据之所以能跨工具流通就是因为这套结构足够简单任何语言都能几行代码解析。{ type: FeatureCollection, features: [ { type: Feature, properties: { id: 1, name: 示例点位, type: store }, geometry: { type: Point, coordinates: [116.397, 39.908] } } ] }1.2 坐标顺序反了是新手最容易栽的跟头上面那段代码里coordinates是[116.397, 39.908]经度在前、纬度在后。这是 GeoJSON 规范强制要求的x 在前、y 在后但国内大量工具和接口的习惯是lat, lng纬度在前。你在 Excel 里把两列粘到在线工具里的时候只要顺序反了点会跑到南极洲附近或者干脆渲染不出来。判断方法很简单中国的经度范围大约是 73 到 135纬度大约是 3 到 54。如果你看到一个点的坐标是[39.9, 116.4]那第一位数 39.9 落在纬度区间里几乎可以肯定顺序反了。我处理过好多次别人发来的地图打不开的数据三分之一都是这个问题。1.3 WGS84、GCJ02、BD09偏移是真实存在的这是国内做地理信息绕不过去的一道坎。同一个地点用不同的坐标系表达落在地图上的位置能差几百米。坐标系常见来源与 WGS84 的关系WGS84EPSG:4326GPS 设备、卫星影像、OpenStreetMap基准无偏移GCJ02高德、腾讯地图及其开放接口在 WGS84 基础上做了非线性偏移BD09百度地图及其接口在 GCJ02 基础上再偏移一次偏移量在城市里通常在 300 到 600 米之间看着不大但在一张 15 级缩放的地图上足够让你的门店标记跑到隔壁街区。更麻烦的是这个偏移不是简单的加减一个常数而是跟经纬度相关的非线性函数所以只能靠转换库或者在线转换服务来批量处理不能手算。1.4 三招判断手上数据是哪个坐标系不用去查数据文档用下面三个办法基本能定性看来源从 GPS 设备、无人机、OSM、卫星底图上取的基本是 WGS84从高德、腾讯的接口或者它们底图上描的是 GCJ02从百度系产品里导的是 BD09。叠已知地标挑一个你百分百确定位置的点比如某城市火车站站前广场把它分别按三种坐标系投到同一张底图上比对哪个严丝合缝就是哪个。看数值分布这个方法只能辅助。有些转换工具处理后会留下极其微小的尾数差异但大多数情况下看不出来别指望靠这个。转换本身不难前端有 gcoord 这类库Python 有 coord-convert 系列包在线也有批量转换工具。关键是转换之后一定要把 properties 里的说明字段一起改掉我见过太多项目里数据已经转成 GCJ02 了字段里还写着 wgs84半年后接手的人照着字段用又是一轮排查。2. 在线制作 GeoJSON 的四条路线先选对再动手2.1 鼠标描点适合零星、不规则、需要人工判断的区域这是最直观的一条路打开一个在线编辑器底图铺开工具栏里选点、线、面在图上直接画。画完在右侧的表格里填属性导出就是 GeoJSON。适合的场景临时圈一块配送范围、标几个候选门店位置、画一条巡检路线。工具上geojson.io 是这一类里认知度最高的界面左侧地图、右侧 JSON、中间工具栏改完立刻能看到文本变化特别适合用来理解我画的东西到底对应哪段结构。它的替代品还有 geojson.net 等一批同类站点功能大同小异。这里有一个必须提醒的坑你在哪张底图上描描出来的就是那张底图的坐标系。如果你用的是高德或腾讯的瓦片底图描出来的点实际是 GCJ02如果你用的是 OSM 或者卫星影像描出来的是 WGS84。为了让手绘数据跟后续要用的底图对齐描点的时候尽量选择和最终底图同源的瓦片能省掉一次转换和一次对不上时的排查。2.2 表格转几何批量点位最省事的路子如果你的数据已经在 Excel 或 CSV 里每行有经度和纬度两列那就不该手绘应该直接转。这条路的效率高出一个数量级几百上千个点几秒钟就出来了。在线工具里把 CSV 拖进支持表格导入的编辑器指定哪一列是经度、哪一列是纬度它会自动生成点要素。需要注意几个细节列名要规范。尽量用lng/lat或者longitude/latitude有些工具认x/y但经度、纬度这种中文列名识别率很低改一下列名比事后调整划算得多。CSV 编码要用 UTF-8。Excel 直接另存为 CSV在国内环境下很容易出 GBK 编码导入之后属性里的中文全是乱码。稳妥做法是另存为CSV UTF-8逗号分隔。空值行要先清理。经纬度缺失的行会让整批转换失败或者生成 geometry 为 null 的要素后续渲染时又得单独处理。如果量特别大或者要跑成固定流程用脚本更稳。这是我最常用的写法import json import pandas as pd df pd.read_csv(points.csv, encodingutf-8) features [] for _, row in df.iterrows(): if pd.isna(row[lng]) or pd.isna(row[lat]): continue features.append({ type: Feature, properties: { name: str(row[name]), type: str(row.get(type, )), }, geometry: { type: Point, coordinates: [float(row[lng]), float(row[lat])], }, }) fc {type: FeatureCollection, features: features} with open(points.geojson, w, encodingutf-8) as f: json.dump(fc, f, ensure_asciiFalse)ensure_asciiFalse这一句不加导出的文件里中文会变成\u5f20\u4e09这种转义形式。文件本身是合法的但人看着难受做代码 review 的时候也容易被当成问题。2.3 行政区划边界要的是现成的面不是自己画的面做省市区着色图、做区域筛选、做数据下钻需要的是行政区划的边界多边形。这个东西千万别自己描描一个区县能把人描到崩溃而且精度和拓扑一致性都没法保证。获取路径有几条一是各类公开的地理数据服务平台通常提供按行政区代码查询边界的能力二是 OpenStreetMap通过它的边界关系导出数据详细但格式需要再处理三是国家地理信息公共服务平台数据权威但使用前要认真读一遍它的使用条款别踩到授权问题上。后面第 5 节我会专门讲阿里那套被搜索最多的行政区划数据那里面的细节坑比想象中多。2.4 格式转换你手里八成不是 GeoJSON很多人的起点根本不是一张白纸而是别人给的 Shapefile、KML、甚至 CAD 的 dxf。这时候在线制作的实际含义是在线转换。Shapefile 要特别注意它从来不是一个文件而是一组.shp存几何、.dbf存属性、.shx存索引、.prj存坐标系。四个文件缺一个就打不开。在线转换时要把这一组文件一起打包成 zip 再上传只传 .shp 是没用的。KML/KMZ 相对省事是单文件导入导出一般都支持。CAD 的 dxf 麻烦一些线宽、图层、块参照这些东西在转换时会有信息损失通常建议先在桌面 GIS 里导入、检查图层、再导出成 GeoJSON。坐标系同样要在这个过程中确认一遍CAD 图纸里的坐标经常是施工坐标系或地方坐标系不是标准的经纬度直接转出来的 GeoJSON 坐标值可能是几十万上百万前端根本没法用。3. 完整走一遍从空白画布到可交付的 GeoJSON3.1 先把绘制顺序想清楚假设我要给一个连锁品牌圈出三家门店的配送范围步骤是这样的先确定输出目标。这份数据是给内部看还是要落到前端做交互内部看的话精度和体积都不敏感前端交互的话后面第 6 节的瘦身必须做。统一坐标系。先问清楚最终地图底图是什么。如果是高德底图最终数据就要是 GCJ02手绘时也应该用高德瓦片。放大到 14 级以上再画。缩放级别太低的时候屏幕上一个像素代表的实际距离可能有几百米画出来的边界误差极大。我的习惯是至少到街道级别可见再动手。画点先于画面。点位好确认画完之后可以拿点位去反推面的边界是否合理。先粗后细。先把大致形状框出来再逐个顶点微调别一开始就追求顶点级精确那样改起来非常痛苦。3.2 属性字段怎么设计才不返工画几何不难难的是属性字段设计得一塌糊涂后来全部返工。我总结了几条经验字段名用英文小写加下划线。store_name比门店名称通用得多虽然 GeoJSON 本身支持中文键但下游但凡经过一次 Shapefile 转换中文键就有乱码风险。一个要素一个稳定的唯一 ID。这个 ID 是后面做数据关联、做增量更新、做点击高亮的基础。千万别用数组下标当 ID数据一排序就全乱了。类型统一。数值字段就存数值不要一会儿是123一会儿是123。前端拿到不一致的类型排序和比较都会出问题。少用 null。用空字符串或者干脆不写这个字段都比 null 处理起来省心。预留一个category之类的分类字段。地图上做分级配色、做图例基本都靠这个字段。3.3 导出前的自检清单每次导出前我都按下面这张表过一遍。这五条检查花不了一分钟但能挡掉绝大多数后续问题。检查项判断方法不合格的表现坐标顺序看第一个坐标经度是否在 73-135 之间点在境外或直接不显示坐标系匹配叠加到目标底图上比对地标整体或局部偏移几百米面是否闭合面的首尾坐标是否完全相同渲染异常、填充失败属性完整性抽查几个要素的 properties字段缺失、中文乱码文件体积看导出的文件大小省市级数据超过几 MB4. geojson 用什么软件打开按场景分别乱试4.1 只想看一眼浏览器就够如果只是这个文件里到底是什么最省事的办法有几个。geojson.io 直接粘贴内容或者拖文件进去就能看同时右侧能看到原始文本改一个坐标地图立刻响应非常直观。mapshaper 更适合看大文件加载快还能顺手做简化和格式转换。GitHub 现在也能直接渲染仓库里的 .geojson 文件团队协作的时候把数据提交上去点开就是一张地图这个功能知道的人不多但非常好用。另外像 kepler.gl 这类工具适合把 GeoJSON 拖进去做点密度、热力、轨迹之类的可视化探索不算打开看看这个层级但如果你要做数据探索它比在 QGIS 里点来点去快。4.2 要编辑和分析上桌面 GISQGIS 是免费里最能打的直接把 .geojson 拖进图层面板就行不需要任何转换步骤。它的价值在于批量编辑属性表、按位置选择、做空间连接、坐标系动态投影。这些操作在在线工具里基本做不了。ArcGIS 系列需要多一步GeoJSON 不是它的原生格式得走 JSON To Features 之类的工具转成要素类。如果你的单位只有 ArcGIS那就按这个流程走转完之后坐标系记得确认。桌面 GIS 有一个容易被忽略的好处它能明确告诉你数据现在是什么坐标系。右键图层看属性投影信息一目了然。在线工具往往不告诉你这些这也是很多人在网页上看是对的换到别的地方就偏了的原因。4.3 要写代码开发者的日常姿势做前端的话VS Code 装一个能预览 GeoJSON 的插件就够了边改边看。真正调试地图渲染的时候还是得在页面里用 Leaflet、OpenLayers 或者各家地图 SDK 加载因为你要验证的是在实际渲染环境下对不对而不是在编辑器里对不对。做数据分析的话Python 里 geopandas 读 GeoJSON 就是一行gdf gpd.read_file(data.geojson)读进来直接当表格用画图可以用 folium 直接出交互式 HTML。这条路适合做数据清洗和批量处理比在图形界面里点强得多。4.4 打开之后一片空白按这个顺序排查白屏是最常见的求助场景排查顺序我一般是这样看坐标数值范围。如果坐标是[12900000, 4800000]这种量级那是投影坐标米为单位不是经纬度。GeoJSON 规范要求用经纬度这种数据得先反投影回 WGS84。看几何类型和坐标层级是否匹配。Point的坐标是[lng, lat]Polygon的坐标是[[[lng, lat], ...]]少一层或多一层方括号都会解析失败。这是手写 JSON 时的高发错误。看文件里是不是别的东西。TopoJSON、Esri JSON 的字段名和 GeoJSON 都不太一样有时候文件后缀是 .geojson 但内容是别的格式工具加载时会静默失败。看编码。带 BOM 头的文件某些解析库会直接报错。看渲染配置。前面几步都没问题的话问题大概率在前端图层没加到地图上、样式里 opacity 设成了 0、或者数据范围跟当前视野完全不重叠。5. 阿里 geojson 这类行政区划数据拿和用都有讲究5.1 那套工具能拿到什么搜索量一直居高不下的阿里 geojson指的通常是某云平台提供的地理小工具这个工具的核心价值是你搜索一个行政区名字它给你这个区域的边界数据可以在地图上预览也可以直接复制或下载 JSON。能拿到的字段一般包括行政区名称、行政区代码六位数字的那个、层级省/市/区县、所属上级、中心点坐标、以及子级数量。这几个字段组合起来足够支撑省市区三级联动这类最常见的需求。接口地址一般长这样具体以官网页面当前提供的形式为准# 获取某个行政区自身的边界 https://该服务的域名/areas_v3/bound/{adcode}.json # 获取某个行政区及其所有下级边界体积大很多 https://该服务的域名/areas_v3/bound/{adcode}_full.json{adcode}就是六位行政区划代码比如北京市是 110000某个区是它的下一级代码。带_full和不带_full是两个完全不同的文件前者包含所有子孙级区域一个省的全量文件能有几 MB 甚至更大直接塞进前端会卡。做联动的时候通常的节奏是先加载省用户选了省再请求这个省的 full 数据用户选了市再请求市的 full 数据逐级按需加载。5.2 拿到的边界数据三个问题要提前有心理准备第一是对齐问题。这类公开边界数据跟不同底图叠加时可能存在微小的偏移。不要默认它一定跟你的底图严丝合缝正确做法是先选一个面积小、形状有辨识度的区域比如某个老城区叠加到你的底图上放大到街道级别看一眼。如果偏了就得整体做一次坐标系转换。这一步五分钟能省掉后面几天的争论。第二是精度和体积的取舍。公开数据为了控制体积边界本身就做过简化。你在省级地图上看不出来一旦放大到区县级边界线和实际的地块边界、道路走向会有肉眼可见的出入。这个在做一张示意性的着色图场景里完全够用但如果你要做面积计算、做地块级分析就必须换更精细的数据源。第三是飞地和多面问题。一个行政区在地理上不一定是一整块可能有一个或多个不与主体相连的飞地。这种情况下导出的 geometry 类型会是 MultiPolygon 而不是 Polygon。你的前端代码必须同时处理这两种类型不然遇到飞地区域就会渲染不出来。同样地一个区域内部如果有被挖掉的洞比如某个区内包含一个独立的开发区坐标数组的层级会比普通面多一层。这两个问题在测试阶段很难发现因为大部分人只拿一两个区试一上线就出问题。5.3 自己拼一个省市区联动数据包如果不想每次上线都依赖外部服务可以把数据打包到自己的静态资源里。做法是按 adcode 逐个拉取整理成{adcode: geojson}的结构用一个索引文件记录父子关系。这里有一个容易忽略的细节索引文件里的层级关系要以行政区划代码为准不要用名称做键。名称会变撤县设区、更名代码相对稳定。而且同名地区在全国范围内是存在的用名称做键迟早出问题。打包之后记得做一次瘦身下一节讲一个包含全国三四级数据的包瘦身前后体积能差十倍以上。6. 几 MB 的 GeoJSON 为什么能把页面卡住6.1 小数点后十五位不是精度是负担打开任何一个从 GIS 软件导出的 GeoJSON你大概率会看到这样的坐标116.39742869572783。小数点后 14 位对应到实际距离是纳米级。对一个用来在地图着色的区域来说这完全是浪费。一个粗略的换算小数点后 6 位大约是 0.1 米5 位大约是 1 米4 位大约是 11 米3 位大约是 110 米。你的区县着色图误差容忍度是多少如果地图最大只放大到 12 级屏幕上一个像素代表几十米那保留 4 位小数绰绰有余。把精度从 14 位砍到 4 位体积能减少四成左右肉眼几乎看不出差别。6.2 用简化工具做拓扑简化而不是随便删点减体积最有效的办法是简化顶点但简化有个大坑独立简化每个多边形会让相邻区域的公共边界产生缝隙或重叠地图上一放大就是一条条白线或者色块互相盖住。正确做法是用支持拓扑简化的工具它会把共享边界当成同一组顶点统一处理简化之后相邻区域依然严丝合缝。mapharper 是这类工具里最实用的可以网页操作也可以命令行mapshaper input.geojson \ -simplify 5% keep-shapes \ -clean \ -o formatgeojson precision0.0001 output.geojson几个参数的意义simplify 5%表示保留大约 5% 的顶点具体比例要根据你的最大缩放级别调先从小比例试叠到底图上检查形状有没有明显变形keep-shapes保证简化后不会出现空几何或消失的小区域clean用来修自相交和零面积的几何precision0.0001就是把坐标精度压到 4 位小数。6.3 加载策略比压缩更管用即使压缩到极致全国范围的精细边界还是不小。这时候要靠加载策略静态资源一定要开 gzip。GeoJSON 是纯文本重复的字段名和数字结构压缩比通常能到 5:1 甚至更高这一步零成本效果最明显。按需加载不要一次拉全量。省市区联动的正确姿势在前面说过逐级请求。考虑 TopoJSON。它是 GeoJSON 的拓扑化版本把共享边界只存一次体积能再小一大截代价是前端需要额外的解码库多一层依赖。超大数据考虑后端出图。如果数据量到了几十兆的级别就别硬塞前端了用服务端渲染成图片瓦片或者矢量瓦片前端只负责展示。7. 那些绕不开的坑几何、编码、属性丢失7.1 环方向和自相交是两个静默杀手GeoJSON 规范里面的外环要求逆时针、内环要求顺时针。现实中大量数据并不遵守多数渲染库也能容忍但只要你的下游有一个严格的工具就会出现某个区域填充变成整个地图或者洞没被挖掉这类诡异现象。自相交更常见手绘的时候顶点连错顺序、或者从别的格式转换过来带进了重叠边都会造成自相交。表现是有的渲染器填充异常有的直接报错。修复办法就是用上一节提到的-clean或者导入 QGIS 后跑一次几何有效性检查。7.2 属性字段丢失八成是 Shapefile 干的Shapefile 的 .dbf 表对字段名有长度限制经典格式是 10 个字符中文属性还有编码问题需要配套一个 .cpg 文件声明编码。所以你从 GeoJSON 转到 Shapefile 再转回来字段名可能被截断、中文可能变问号。如果链路上必须经过 Shapefile字段名提前按 10 字符以内规划并确保 .cpg 里写的是 UTF-8。另一个高频问题是 CSV 的编码。前面提过Excel 存的 CSV 在国内默认是 GBK导入工具后中文变乱码。这个问题的麻烦在于它不报错只是静默地把内容改掉你不逐条核对根本发现不了。7.3 坐标偏了用一条排查链路搞定偏移问题最忌讳的是瞎猜。我现在的固定流程是三步第一步量数值。看坐标是否在合理的经纬度区间内这一步能排除掉投影坐标和顺序反了两种情况。第二步找一个锚点。挑一个你百分百确定位置的要素把它的坐标单独拎出来分别按三种坐标系换算成另外两种得到三个候选点逐个叠到底图上比对。哪个对上数据就是哪个坐标系。第三步换底图验证。如果三个候选点都对不上说明问题不在坐标系而在底图本身——可能是底图缓存了旧版本、可能是底图服务本身的偏移、也可能是你在缩放级别很低的时候看误差被视觉放大了。换成另一种底图再叠一次基本就能定位。这套流程我走了很多次最慢的一次也就十几分钟。比起挨个试工具、试参数效率高太多。7.4 一个容易被忽略的细节null 几何FeatureCollection 里是允许存在 geometry 为 null 的 Feature 的规范里这是合法的。但很多渲染库遇到 null 几何会直接抛异常导致整张地图加载失败。数据清洗阶段就应该把这些空要素过滤掉而不是等到前端报错再去查。批量处理的时候一行过滤代码就能解决。说个我自己的习惯任何一份手头要用的 GeoJSON在正式接入之前我都会先做一件事——把它丢进地图随机放大三个不同区域逐个看一眼。看一眼边界跟底图对不对齐、看一眼有没有哪个区域渲染成黑块、看一眼文件大小。这三眼花不到五分钟但它拦下的问题比我写过的所有数据校验脚本加起来还多。数据这东西肉眼过一遍永远比自动化检查更早发现问题。