做地图和位置相关开发这几年,我养成了一个习惯:凡是遇到空间数据格式的交接,先问对方三个问题——你這数据是给人看还是给程序吃?要不要带业务属性?坐标系统一了没有?这三个问题一出来,WKT、WKB和GeoJSON的选择基本就定下来了。
这三者本质上描述的是同一个东西:一个点、一条线、一个面,或者一堆几何对象。但它们的“性格”完全不同。WKT像手写的坐标清单,人一眼就能看懂;WKB像二进制的机器码,紧凑、快速、但极其不友好;GeoJSON则像带备注的JSON合同,和Web生态完美融合。很多刚接触GIS的开发者会在这三者之间反复横跳,其实只要理解了它们各自的编码逻辑和适用场景,选型根本不会纠结。
这篇文章我就把这三兄弟摊开来讲一遍:它们各自的标准出身、底层编码结构、性能差异、在真实项目里怎么组合使用,以及我自己踩过的那些坑。
1. 三种格式的前世今生:它们分别解决什么问题
1.1 WKT:给人看的空间几何文本
WKT全称Well-Known Text,是开放地理空间联盟(OGC)在1999年前后推出的空间几何编码规范,后来被ISO 19125采纳。它的设计目标非常明确:让一个几何对象能够用纯文本形式表达,既方便人眼阅读,也方便跨系统交换。
看几个例子就明白了:
POINT(120.5 30.2) LINESTRING(0 0, 10 0, 10 10) POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))第一行是一个点,经纬度坐标分别是120.5和30.2;第二行是一条线,由三个顶点构成;第三行是一个矩形面,四个顶点加上最后回到起点的闭合点。没有任何额外修饰,坐标用空格隔开、点与点之间用逗号隔开,学习成本几乎为零。
WKT能表达的空间类型也很完整:点、线、面、多点、多线、多面,以及上面这些类型的任意集合。标准里还有带SRID的扩展写法,比如SRID=4326;POINT(120.5 30.2),这样几何对象和坐标系信息就绑定到了一起,不会出现“数据有了但不知道坐标含义”的尴尬。
在数据库领域,WKT是绝对的主流交换格式。你用PostGIS查数据,执行SELECT ST_AsText(geom),返回的就是WKT;你用MySQL或者SQLite的Spatialite,导入导出空间字段时也大量使用WKT。如果你只是在排查数据问题,想用SQL直接看一眼某个面长什么样,WKT是绕不开的。
1.2 WKB:给机器吃的二进制压缩包
WKB全称Well-Known Binary,和WKT是同一套规范体系下的孪生兄弟。它把同样的几何信息编码成二进制字节流,没有空格、没有括号、没有逗号,一切靠固定的字节位置和长度来约束。
举个例子,POINT(1 2)的WKB用十六进制展开是这样一串:
0101000000000000000000F03F0000000000000040如果你拿这个字符串去肉眼比对坐标,大概率直接放弃。但程序读起来很爽:第一个字节是字节序标记,后面四个字节是几何类型码,再往后每八个字节就是一个double精度的坐标值。定长、顺序、无歧义,这就是WKB的设计哲学。
同样是表示一个几何对象,WKB的体积最小、解析速度最快,特别适合在数据库内部做存储、在软件组件之间做二进制传输,或者在高吞吐量的后端链路里传递空间数据。很多GIS中间件和空间数据引擎,底层实际上都在用WKB交换数据,只是在上层给你包装成了WKT或者GeoJSON。
1.3 GeoJSON:Web世界的“普通话”
GeoJSON诞生于2000年代后期,最初由一批GIS开发者、地图服务商共同推动,2016年正式成为RFC 7946标准。它基于JSON构建,因此天然继承了JSON的跨语言、易解析、结构灵活等优点。
它的核心结构分两层:外层是FeatureCollection,里面装了一堆Feature;每个Feature由geometry和properties两部分组成。geometry存几何形状,properties存业务属性。这一点很关键——WKT和WKB只能表达“这个东西在哪”,而GeoJSON还能表达“这个东西是什么、它有哪些属性”。
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "name": "某地块", "area": 1280.5 }, "geometry": { "type": "Polygon", "coordinates": [ [[120.5, 30.2], [120.6, 30.2], [120.6, 30.3], [120.5, 30.3], [120.5, 30.2]] ] } } ] }前端地图框架(Leaflet、Mapbox GL、OpenLayers)基本都把GeoJSON作为一等公民来支持,JSON.parse之后直接塞给渲染层,连额外解析库都不用装。这就是它能在Web领域快速普及的根本原因。
2. 格式细节拆解:从语法到语义的对比分析
2.1 WKT的语法细节与隐藏约束
WKT的语法表面上很简单,但真用起来有几个细节需要注意。
第一个是“闭合”问题。表示一个面时,外环的第一个点和最后一个点必须相同,否则解析器直接报错。我见过很多新人写POLYGON((0 0, 10 0, 10 10, 0 10)),觉得四条边够了,实际上必须补上结尾的0 0才是一个合法面。
第二个是环的方向。OGC标准规定,外环顶点按逆时针排列,内环(也就是洞)按顺时针排列。这个要求表面上看无所谓,但某些空间运算对环方向非常敏感。比如在PostGIS里计算面积,环方向反了会导致面积为负值,后续判断可能得出完全错误的结论。
第三个是数值格式。WKT坐标可以带小数,也可以写成科学计数法,比如POINT(1.2e3 -4.5e-2)。大部分解析器都支持,但有些老旧的第三方库对科学计数法的支持并不好,稳妥起见,日常交换数据时尽量用普通十进制小数。
WKT还有一个限制很多人没意识到:它只能表达几何本身,无法附带属性信息。你可以在POINT(120.5 30.2)旁边扩展任何你喜欢的额外字段,但那不是WKT标准的一部分。所以当业务需要同时传几何和属性时,WKT必须搭配外部字段一起走,这就在格式对接上埋下了隐患。
2.2 WKB的字节级结构解析
WKB是二进制格式,理解它的关键是字节序(endianness)。整个结构第一步就是读第一个字节,如果是0x00表示大端序,如果是0x01表示小端序,后续所有整数和浮点数都按这个字节序来解析。这个设计既保证了程序能自动识别数据来源的字节序,又避免了跨平台时的乱码问题。
接下来是一个四字节整数,表示几何类型。类型码低两位是几何类型编号:1是Point,2是LineString,3是Polygon,4是MultiPoint,5是MultiLineString,6是MultiPolygon,7是GeometryCollection。高两位如果出现1000、2000、3000这样的值,分别代表带Z坐标、带M测量值、同时带ZM。比如一个带高程的POINT,类型码就是1001。
再往后就是坐标本体。坐标统一用IEEE 754双精度浮点数存储,也就是C语言里的double,每个数8字节。以POINT(1 2)为例:小端序标记01,类型码01000000,X坐标000000000000F03F(对应1.0),Y坐标0000000000000040(对应2.0)。整个对象只有21个字节,比同等内容的WKT文本小一半以上。
多边形看起来就复杂一些:类型码之后先是一个四字节整数“环的数量”,然后每个环先写“顶点数量”,再依次写各个顶点坐标。这种层层嵌套的计数结构,让解析器可以无脑循环读取,不需要像文本解析那样逐个扫描括号和逗号,性能差距就是这么拉开的。
2.3 GeoJSON的内层结构与属性设计
GeoJSON的坐标系有点“轴顺序”问题,这个必须单独说。RFC 7946明确规定,坐标数组的顺序是经度在前、纬度在后,也就是[longitude, latitude]。这个规定和日常口语里常说的“经纬度”正好相反,很多人栽在这上面——写成了[latitude, longitude],结果点跑到海沟里去了。
GeoJSON的坐标嵌套层级也是个容易蒙的点。Point是两层数组[120.5, 30.2],LineString是三层数组[[120.5, 30.2], [120.6, 30.2]],Polygon是四层数组[[[120.5, 30.2], [120.6, 30.2], ...]],到了MultiPolygon就成了五层数组。每一层包的是什么含义,不熟练的时候特别容易写多一层、少一层,前端一渲染整个图形就混沌了。
属性部分是GeoJSON真正的优势区。properties可以是任意JSON对象,可以嵌套、可以是数组、也可以是布尔值或者数字。这在业务场景里极其好用:地块的名称、面积、用途、审批状态,全部可以塞进去,一份数据同时满足了空间展示和业务查询两个需求。
不过这里也要泼一盆冷水:GeoJSON默认就是WGS84经纬度坐标系,RFC 7946直接移除了自定义CRS(坐标系参考)的推荐做法。如果你的数据是高斯投影或者Web墨卡托坐标,直接按GeoJSON输出出去,别人拿到的就是一堆莫名其妙的数字。解决办法是在输出前先做坐标转换,把数据统一到EPSG:4326。
3. 核心对比:可读性、体积、性能与适用场景
3.1 横向对比速查表
为了方便决策,我把三种格式的关键属性放在一起对比:
| 对比维度 | WKT | WKB | GeoJSON |
|---|---|---|---|
| 可读性 | 高 | 差 | 极高 |
| 体积大小 | 中 | 最小 | 较大 |
| 解析性能 | 快 | 极快 | 中等 |
| 属性承载 | 无 | 无 | 有 |
| 坐标系支持 | 可嵌SRID | 可嵌SRID | 默认WGS84 |
| 标准化 | OGC/ISO 19125 | OGC/ISO 19125 | RFC 7946 |
| 前端直接渲染 | 需解析库 | 基本不行 | 直接JSON.parse |
| 数据库支持 | 极好 | 极好 | 好,但空间索引要转几何 |
3.2 坐标维度与SRID处理差异
坐标维度的处理上,三者有明显区别。WKT和WKB都支持三维坐标,WKT直接在坐标后加一个Z值,比如POINT(120.5 30.2 100);WKB则通过类型码的高位标识来判断是否包含Z。GeoJSON也可以表达三维坐标,在坐标数组里加第三个元素即可,但要注意很多前端渲染库默认就把第三维忽略了。
SRID(空间参考标识符)的处理是另一大差异点。PostGIS里,WKT可以通过SRID=4326;前缀携带坐标系信息,WKB写入时需要单独指定SRID参数,GeoJSON则完全不带SRID——它默认就是4326。这带来一个很现实的问题:如果你的系统用的是其他坐标系,从GeoJSON拿到前端展示时前端没问题,但一旦你要把这份GeoJSON重新存回数据库并且参与空间计算,就必须在导入时ST_SetSRID(ST_GeomFromGeoJSON(...), 目标SRID),或者先ST_Transform,否则几何对象和坐标系就对不上。
3.3 属性数据承载能力对比
这三种格式最本质的差异,其实不在于几何编码方式,而在于能不能带属性。WKT和WKB是纯粹的几何描述格式——数据里只有形状和位置,没有名称、没有面积、没有类型,任何业务信息都得靠外部字段手工关联。GeoJSON则把几何和属性打包在一起,一个Feature既告诉系统“这个多边形在哪”,又告诉它“这个多边形是哪块地、属于谁、状态是什么”。
所以你会看到,在空间数据库内部,WKB是王者;在SQL查询和人工排查场景,WKT是好帮手;而在前后端API、数据交换、Web展示这些场景,GeoJSON几乎是唯一稳妥的选择。选型千万不要“一招鲜”,要跟着数据的使用方式走。
4. 实战转换:从数据库到前端地图的完整链路
4.1 在PostGIS中完成SQL级格式互转
如果你的空间数据存在PostGIS里,格式互转就是几条SQL的事,根本不用写代码。
-- WKT方式读取几何 SELECT ST_AsText(geom) FROM my_spatial_table WHERE id = 1; -- GeoJSON方式读取,第二参数控制坐标小数位数为6位 SELECT ST_AsGeoJSON(geom, 6) FROM my_spatial_table WHERE id = 1; -- 从WKT写入 INSERT INTO my_spatial_table (geom) VALUES (ST_GeomFromText('POINT(120.5 30.2)', 4326)); -- 从GeoJSON写入,注意要显式指定SRID INSERT INTO my_spatial_table (geom) VALUES (ST_SetSRID(ST_GeomFromGeoJSON('{"type":"Point","coordinates":[120.5,30.2]}'), 4326));这里有几个容易踩的坑。第一,ST_AsGeoJSON如果不给精度参数,默认最多输出15位小数,一条几百万个点的折线体积能膨胀好几倍。实际使用建议给个6位或7位小数,换算下来精确到厘米级甚至毫米级,对绝大多数地图展示场景完全够用,体积却能砍掉一大截。
第二,从GeoJSON导入时千万不要忘了ST_SetSRID。ST_GeomFromGeoJSON解析出来的几何对象SRID是0,也就是未知坐标系,直接参与空间运算会报错,或者结果完全不可用。这个坑我掉进去过不止一次,排查到最后往往就是SRID缺失。
第三,数据库端做坐标系转换用ST_Transform,不是直接改数字。比如你把GeoJSON导入的是4326,但业务库要求3857,正确的做法是:
UPDATE my_spatial_table SET geom = ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON('{"type":"Point","coordinates":[120.5,30.2]}'), 4326), 3857) WHERE id = 2;4.2 用Shapely在Python里玩转三种格式
如果你更习惯在Python里处理空间数据,Shapely库是绕不开的工具。它的设计非常顺手,加载WKT、导出WKB、转GeoJSON,都在几个函数之间完成。
from shapely import wkt, wkb import json # WKT -> 几何对象 geom = wkt.loads("POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))") # 几何对象 -> WKB,默认输出字节串,hex()方便打印和存储 wkb_bytes = wkb.dumps(geom) print(wkb_bytes.hex()) # 几何对象 -> GeoJSON格式的字典,再转成JSON字符串 from shapely.geometry import mapping geojson_dict = mapping(geom) print(json.dumps(geojson_dict))Shapely对WKB特别友好,可以直接从十六进制字符串还原:
from shapely import wkb wkb_hex = "0103000000010000000500000000000000000000000000000000000000000000000000244000000000000000000000000000002440000000000000244000000000000000000000000000000000000000000000000000" geom = wkb.loads(bytes.fromhex(wkb_hex)) print(geom.wkt)这里要提醒一个版本差异。Shapely 1.x里面geojson模块是独立的,2.x之后模块结构有调整,直接from shapely.geometry import mapping再配合标准库json是最稳的做法,尽量少依赖内部耦合的接口。
4.3 一个完整案例:某跨平台系统的空间数据导出
我之前处理过一个跨平台系统的数据导出需求:库里存的是PostGIS,前端需要能在网页地图上查看一批地块边界,同时要显示每个地块的名称、面积和审批状态。当时有三种候选方案。
方案一:后端直接吐WKT字符串,前端装一个空间解析库解析后自己画。能做到,但属性数据得单独包一层JSON,接口结构复杂,前端还要绕一道解析,放弃。
方案二:后端吐WKB,前端解析二进制。性能虽然好,但Javascript解析WKB需要额外写一堆字节序处理逻辑,纯属自找麻烦,放弃。
方案三:后端用ST_AsGeoJSON(geom, 6)直接转GeoJSON,properties里拼上地块属性,前端拿到之后JSON.parse,然后直接交给Leaflet或者Mapbox GL渲染。三秒钟搞定。
实测数据量一上来,方案三也有瓶颈。几千条数据时毫无压力,到了几万条要素、每条要素还有几千个坐标点的时候,前端渲染直接卡得喘不过气。我的解决方案是双层优化:第一层,后端在SQL里做视口范围裁剪,只返回用户当前能看到的那部分要素;第二层,对返回的GeoJSON做坐标精度控制,限制到6位小数,同时用空间简化算法去掉肉眼看不出来的冗余顶点。这两步做完,前端渲染效率翻了几倍不止。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| GeoJSON里的点跑到大海里 | 经纬度写反了,用了[lat, lon] | 统一按RFC 7946的[lon, lat]顺序 |
| WKT多边形解析报错 | 外环未闭合,首尾点不一致 | 补上闭合点 |
| PostGIS面积计算为负数 | 环方向反了 | 外环逆时针,内环顺时针 |
| WKB导入后坐标完全错乱 | 字节序标记被忽略或写错 | 严格读取第一个字节,大小端按标记解析 |
| GeoJSON导入数据库后空间查询无效 | 没设SRID或没转geometry类型 | ST_SetSRID + 显式geometry字段 |
| 大数据量GeoJSON拖垮前端 | 小数位数过多或要素超量 | 限制精度、空间裁剪、必要时后端简化 |
| 点位显示偏了几百米 | 坐标系没对齐 | 统一到4326,跨系必须ST_Transform |
5.2 坐标顺序与精度防呆清单
坐标顺序问题是这个领域最高发的事故源。我见过不止一次,一个看起来很正常的Point,画到地图上落在了完全不对的位置,最后发现是[30.2, 120.5]和[120.5, 30.2]的差别。建议在项目里定一条铁规矩:凡是进入GeoJSON的坐标,一律采用[经度, 纬度]的顺序写入;凡是手写测试坐标,都用一个已知的城市坐标来验证,比如[116.4, 39.9]这种、看一眼就知道合不合理的数据。
精度问题则是性能隐患。坐标精度不是越高越好,float的7位有效数字约等于1.1米误差,double的15位有效数字能到小数点后原子级别。对于地图展示和空间查询,GeoJSON保留7位小数已经绰绰有余。我在实战里把接口的GeoJSON输出精度统一限制在6到7位,返回值体积至少缩小三分之一,肉眼根本看不出差异。
5.3 性能优化的几个实际经验
第一,能用数据库算的,绝不在应用层算。空间过滤、坐标系转换、甚至GeoJSON生成,PostGIS都能在SQL里完成,减少一次网络传输就快一大截。
第二,大文件优先考虑逐行流式解析。GeoJSON可以改成GeoJSONSeq格式(也叫JSON Lines),每一行是一个Feature。前端用流式解析,边读边渲染,不会因为一个几百MB的JSON卡死浏览器。
第三,WKB依然是内部传输的最优选择。如果你的系统有多个后端服务在频繁交换空间数据,内部链路用WKB,只在对外接口处转GeoJSON,能省下大量的网络带宽和序列化CPU时间。我在一个实时轨迹接收服务里就是这么做,整体吞吐能力提升了大约40%。
6. 选型建议与团队实践心得
6.1 不同场景下的格式选型
到这里,格式选型的规律已经很清晰了。数据库内部存储和空间索引,首选geometry原生字段,底层就是WKB;日常排查、SQL调数据看一眼,用WKT最方便;对外API、Web前端渲染、跨系统数据交换,直接给GeoJSON;后端内部高吞吐传输,可以考虑WKB裸格式。
坐标系情况单独拎出来对齐。所有对外输出统一成WGS84经纬度(EPSG:4326),前端地图也基于4326或者由前端自动转换投影到Web墨卡托(EPSG:3857)显示。后端内部如果业务需要投影坐标,则维持自己的投影仓库,输出前统一转换,避免每个系统各自为政。
6.2 我个人踩过的坑与经验总结
做空间数据这几年,我最大的体会是:格式本身都是好东西,出问题的大多数时候出在“人在中间乱传坐标”。有一次我排查了整整一个下午的数据错乱,最后发现是某个中间步骤里,前端把GeoJSON传回后端时,代码用Object.keys遍历坐标,把数字数组顺序搞反了。从那以后,我给自己定了一个规则:所有空间GeoJSON的坐标都必须用数组字面量直接构造,不允许用map或者遍历“重组”坐标数组,因为重组过程最容易引入顺序错误。
另一个经验是,一定要在系统里留一个WKT的统一出口。很多团队只关注GeoJSON接口,排查数据问题时一片抓瞎。实际上,只要在调试工具里能方便地刷出某个农业地块、某条道路、某个行政区边界的WKT,很多空间数据异常一眼就能看出来。WKT就像这个领域里的“明文日志”,值得永远保留一席之地。
最后分享一个小技巧:如果你要在PostGIS和前端之间高频传输GeoJSON,建议同时把坐标精度小数位调到6位,再在Nginx层开启gzip压缩。GeoJSON文本里有大量重复键名,压缩比通常能到5:1以上。这样一套组合拳打下来,既保留了GeoJSON的生态优势,又能把网络延迟压到接近二进制格式的水平。我自己现在做空间数据项目,凡是落地到线上环境的接口,基本都是这套配置。