跳转至
Apache Sedona 1.9.0 已正式发布,新增 Spark 4.1 支持、proj4sedona 坐标系转换、Bing Tile 函数等众多特性!

在 Spark 上使用 Apache Sedona 处理 GeoJSON

本文介绍如何使用 Apache Sedona 与 Spark 读写单行 GeoJSON 与多行 GeoJSON 文件。

末尾给出 GeoJSON 在空间分析中的优缺点小结。

GeoJSON 基于 JSON,支持以下类型:

  • Point
  • LineString
  • Polygon
  • MultiPoint
  • MultiLineString
  • MultiPolygon

更多关于 GeoJSON 格式的规范 见此处。

使用 Sedona 与 Spark 读取多行 GeoJSON

读取多行 GeoJSON 的方式如下:

df = (
    sedona.read.format("geojson")
    .option("multiLine", "true")
    .load("data/multiline_geojson.json")
    .selectExpr("explode(features) as features")
    .select("features.*")
    .withColumn("prop0", expr("properties['prop0']"))
    .drop("properties")
    .drop("type")
)
df.show(truncate=False)

输出如下:

+---------------------------------------------+------+
|geometry                                     |prop0 |
+---------------------------------------------+------+
|POINT (102 0.5)                              |value0|
|LINESTRING (102 0, 103 1, 104 0, 105 1)      |value1|
|POLYGON ((100 0, 101 0, 101 1, 100 1, 100 0))|value2|
+---------------------------------------------+------+

该多行 GeoJSON 文件包含一个点、一个折线和一个多边形。原始文件内容如下:

{ "type": "FeatureCollection",
    "features": [
      { "type": "Feature",
        "geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
        "properties": {"prop0": "value0"}
        },
      { "type": "Feature",
        "geometry": {
          "type": "LineString",
          "coordinates": [
            [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]
            ]
          },
        "properties": {
          "prop0": "value1",
          "prop1": 0.0
          }
        },
      { "type": "Feature",
         "geometry": {
           "type": "Polygon",
           "coordinates": [
             [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],
               [100.0, 1.0], [100.0, 0.0] ]
             ]
         },
         "properties": {
           "prop0": "value2",
           "prop1": {"this": "that"}
           }
         }
       ]
}

注意整体结构是一个 FeatureCollection,每个 feature 都有几何类型、几何坐标与属性字段。

也可以一次读取多个多行 GeoJSON 文件。假设有如下目录:

many_geojsons/
  file1.json
  file2.json

读取方法如下:

df = (
    sedona.read.format("geojson").option("multiLine", "true").load("data/many_geojsons")
)

只需把包含 JSON 文件的目录传入即可。

多行 GeoJSON 对人类阅读友好,但对机器低效。建议把 JSON 数据写在单行上。

使用 Sedona 与 Spark 读取单行 GeoJSON

读取单行 GeoJSON 的方式如下:

df = (
    sedona.read.format("geojson")
    .load("data/singleline_geojson.json")
    .withColumn("prop0", expr("properties['prop0']"))
    .drop("properties")
    .drop("type")
)
df.show(truncate=False)

结果如下:

+---------------------------------------------+------+
|geometry                                     |prop0 |
+---------------------------------------------+------+
|POINT (102 0.5)                              |value0|
|LINESTRING (102 0, 103 1, 104 0, 105 1)      |value1|
|POLYGON ((100 0, 101 0, 101 1, 100 1, 100 0))|value2|
+---------------------------------------------+------+

数据如下:

{"type":"Feature","geometry":{"type":"Point","coordinates":[102.0,0.5]},"properties":{"prop0":"value0"}}
{"type":"Feature","geometry":{"type":"LineString","coordinates":[[102.0,0.0],[103.0,1.0],[104.0,0.0],[105.0,1.0]]},"properties":{"prop0":"value1"}}
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[100.0,0.0],[101.0,0.0],[101.0,1.0],[100.0,1.0],[100.0,0.0]]]},"properties":{"prop0":"value2"}}

可以看出:多行 GeoJSON 使用一个 FeatureCollection,而单行 GeoJSON 中每行是独立的 Feature

单行 GeoJSON 文件更优——查询引擎可以对它进行 split。

下面看如何用 Sedona 通过 DataFrame 写出 GeoJSON。

使用 Sedona 与 Spark 写出 GeoJSON

创建一个 Sedona DataFrame,再写出为 GeoJSON:

df = sedona.createDataFrame([
    ("a", 'LINESTRING(2.0 5.0,6.0 1.0)'),
    ("b", 'LINESTRING(7.0 4.0,9.0 2.0)'),
    ("c", 'LINESTRING(1.0 3.0,3.0 1.0)'),
], ["id", "geometry"])
actual = df.withColumn("geometry", ST_GeomFromText(col("geometry")))
actual.write.format("geojson").mode("overwrite").save("/tmp/a_thing")

写出的文件如下:

a_thing/
  _SUCCESS
  part-00000-856044c5-ae35-4306-bf7a-ae9c3cb25434-c000.json
  part-00003-856044c5-ae35-4306-bf7a-ae9c3cb25434-c000.json
  part-00007-856044c5-ae35-4306-bf7a-ae9c3cb25434-c000.json
  part-00011-856044c5-ae35-4306-bf7a-ae9c3cb25434-c000.json

Sedona 会并行写出多个 GeoJSON 文件,比写单一文件更快。

注意:写出操作要求 DataFrame 至少包含一个几何类型的列。Sedona 会按以下规则确定使用哪一列作为几何列:

  1. 如果存在名为 “geometry” 且类型为 geometry 的列,则使用该列;
  2. 否则使用根 schema 中找到的第一个几何列。

也可以通过 geometry.column 选项手动指定使用哪一列:

df.write.format("geojson").option("geometry.column", "geometry").save("/tmp/a_thing")

将这些 GeoJSON 文件再读回 DataFrame:

df = sedona.read.format("geojson").load("/tmp/a_thing")
df.show(truncate=False)
+---------------------+----------+-------+
|geometry             |properties|type   |
+---------------------+----------+-------+
|LINESTRING (1 3, 3 1)|{c}       |Feature|
|LINESTRING (2 5, 6 1)|{a}       |Feature|
|LINESTRING (7 4, 9 2)|{b}       |Feature|
+---------------------+----------+-------+

GeoJSON 的优势

GeoJSON 格式有以下优点:

  • 人工可读
  • 可以并行写出多个文件,便于并行处理引擎获得更快的 I/O。
  • 大量引擎都支持 GeoJSON / JSON 文件。

不过 GeoJSON 也有不少缺点,使其在存储地理空间数据时并非最佳选择。

GeoJSON 的局限

GeoJSON 在空间数据湖场景下可能存在以下性能问题:

  • GeoJSON 对象虽然可以包含 CRS,但 CRS 是可选的,这一关键信息可能丢失。
  • 行式存储,无法享受 GeoParquet 等列式格式的列裁剪等性能优化。
  • 不存储 row group 元数据,无法进行 row-group 过滤(这是 Parquet 的一项性能优化)。
  • 文件尾部不携带 schema,需手动提供或自动推断。
  • GeoJSON 规范要求固定的结构,对某些数据集而言比较僵硬。
  • 只能用于构建数据湖,无法用于构建数据湖仓(lakehouse)。

结论

GeoJSON 在空间数据分析中很常见,Apache Sedona 提供完整的读写支持是非常方便的。

GeoJSON 受到广泛支持且可读性好,但相比 GeoParquet 等格式速度较慢。一般而言,进行空间数据分析时建议优先使用 GeoParquet 或 Iceberg 以获得更好的性能。