【spark sedona geospark】sedona1.5.1读取shp文件,字段类型都是string的问题

【spark sedona geospark】sedona1.5.1读取shp文件,字段类型都是string的问题

引言

最近在使用sedona读取shp文件中数据,但是遇到了两个问题,一个是中文乱码的问题,一个就是这个类型都是string的问题了,中文乱码的问题我解决了,但是这个类型的问题没有真正从根本上解决。

给一个我解决中文乱码的博客地址吧,点击跳转

一、问题重现

先贴出代码(Java):

SparkSession sedona = SedonaContext.builder()
                .master("local[*]")
                .appName("test")
                .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
                .config("spark.kryo.registrator", SedonaVizKryoRegistrator.class.getName())
                .getOrCreate();
SedonaContext.create(sedona);
SpatialRDD<Geometry> geomRDD = ShapefileReader.readToGeometryRDD(JavaSparkContext.fromSparkContext(
												sedona.sparkContext()),shpPath);
Dataset<Row> df = Adapter.toDf(geomRDD, sedona);
df.printSchema();

打印出的结果是:

在这里插入图片描述

类型都是string,可是我的数据中肯定不止string这个类型啊,OBJECTIDinteger 是我改过的,它原本也是 string

二、原因

我上 GitHub 提了一个 issue ,官方给的答案真是让人无奈(看代码分析直接跳转到目录第四),直接贴出 issue 的地址,点击跳转

官方回答:

在这里插入图片描述

**这个回答的上部分包含了解决中文乱码的办法。

官方的回答简单来说就是他们意识到了这个问题,但是现在暂时没有解决。他们在读取 shp 文件的时候,可能是因为某种原因,将所有的字段都作 string 处理了,官方也承认这是不足之处。我觉得后续可能会改进。

当下官方给出的解决办法是自己实现一个基于 Spark DataSourceV2shp 读取器,直接将 shp 加载为 DataFrame

我觉得按照我的水平应该是完成不了了,所以我选择另辟蹊径。

三、我的解决办法(可能有更好)

虽然官方在读取shp时丢失了字段的所有类型信息,但是官方有提供类型转换的接口,也就是说如果你知道某个字段是什么类型,那么其实你可以手动转它的类型,类似:

Dataset<Row> df = Adapter.toDf(geomRDD, sedona);
df = df.withColumn("OBJECTID", df.col("OBJECTID").cast("int"));

那么我们就可以手动转换类型了,可是类型怎么来呢?GeoTools,我们可以使用 Geotools 来获取字段的类型,再通过判断的形式,转换所有字段的类型不就可以了吗

引入gt-shapefile依赖:

<dependency>
    <groupId>org.geotools</groupId>
    <artifactId>gt-shapefile</artifactId>
    <version>25.2</version>
</dependency>

代码部分(仅示例,详细自己写):

FileDataStore dataStore = FileDataStoreFinder.getDataStore(new File(shpPath));
SimpleFeatureSource featureSource = dataStore.getFeatureSource();
SimpleFeatureType featureType = featureSource.getSchema();
List<AttributeDescriptor> attributeDescriptors = featureType.getAttributeDescriptors();
for (AttributeDescriptor descriptor : attributeDescriptors) {
    String fieldName = descriptor.getLocalName();
    String fieldType = descriptor.getType().getBinding().getSimpleName();
    System.out.println("字段名: " + fieldName + ", 字段类型: " + fieldType);
}

根究代码我们可以得到每个字段的类型的,后面如何处理就不多说了吧

四、导致问题的代码

扒了一下源代码,结合开发人员说的内容,我大概知道了为什么他明知不可为而为之了。

1、ShapefileReader

读取数据的入口他放在了 ShapefileReader 中,提供了一个静态方法,读取到数据后返回 SpatialRDD ,方法签名:

public class ShapefileReader {
	public static SpatialRDD<Geometry> readToGeometryRDD(JavaSparkContext sc, String inputPath);
}

跟着调用链我们往下跟踪。发现这个方法调用的是:

public static SpatialRDD<Geometry> readToGeometryRDD(JavaSparkContext sc, 
													 String inputPath, 
													 GeometryFactory geometryFactory)

这个方法内部主要读取数据的代码是:

在这里插入图片描述

顺着这个 readShapefile 方法往下,发现它是调用 SparkContextnewAPIHadoopFile 方法,让 Hadoop 给它处理并分配文件,它直接获取文件路径读取就行:

在这里插入图片描述

我们来看看方法签名,当然,这是 Scala 代码:

def newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]](
    path: String,
    fClass: Class[F],
    kClass: Class[K],
    vClass: Class[V],
    conf: Configuration): JavaPairRDD[K, V]

Scala 我也不熟,不过我们能从调用处窥探到一点门径:

JavaPairRDD<ShapeKey, PrimitiveShape> shapePrimitiveRdd = sc.newAPIHadoopFile(
															inputPath, 
															ShapeInputFormat.class, 
															ShapeKey.class, 
															PrimitiveShape.class, 
															sc.hadoopConfiguration());

* 第一个参数传了一个路径,它是数据文件的路径了。
* 第二个参数是一个转换类,它定义了如何分片数据以及如何读取数据的逻辑。
* 第三个参数是读取数据时的 ,如果不懂键值对的概念,可以先学学Hadoop
* 第四个当然就是 了。
* 第五个参数是 Hadoop 的配置,固定写法。

情况现在比较明了了,我们只要去看它是如何转换数据的就行了。跳转到 ShapeInputFormat 类。

2、ShapeInputFormat

ShapeInputFormat 继承了 CombineFileInputFormatCombineFileInputFormat 又继承了 FileInputFormat ,这个类是 Hadoop 定义来读取数据的。继承它并且自己实现的话,可以自定义数据的读取逻辑。

别的我们都可以暂时不用看,getSplits 是定义分片的,不管。createRecordReader 是真正定义数据如何读取的,也就是说数据读取的真实实现在这里面。

我们看看 Sedona 是如何定义的:

在这里插入图片描述

好嘛,接着跳过去。

3、CombineShapeReader

这个类就是真正读取数据的地方,也就是每个分区中真实干活的类,篇幅有限,我们直接跳到 getCurrentValue 方法:

public PrimitiveShape getCurrentValue() throws IOException, InterruptedException {
        PrimitiveShape value = new PrimitiveShape(this.shapeFileReader.getCurrentValue());
        if (this.hasDbf && this.hasNextDbf) {
            value.setAttributes(this.dbfFileReader.getCurrentValue());
        }

        return value;
    }

读取每行 Feature 字段值的代码就是 this.dbfFileReader.getCurrentValue() ,我们继续跳过去:

4、DbfFileReaderDbfParseUtil

跳过来后是一个获取值的方法:

public String getCurrentValue() throws IOException, InterruptedException {
    return this.value;
}

当然,这不是读取数据的方法,直接找到 nextKeyValue 方法:

public boolean nextKeyValue() throws IOException, InterruptedException {
    String curbytes = this.dbfParser.parsePrimitiveRecord(this.inputStream);
    if (curbytes == null) {
        this.value = null;
        return false;
    } else {
        this.value = curbytes;
        this.key = new ShapeKey();
        this.key.setIndex((long)(this.id++));
        return true;
    }
}

接着跳… :

在这里插入图片描述

跳过来后我也懒得说了,继续跳:

在这里插入图片描述

好家伙,跳了半天,终于找到真实实现了:

public String primitiveToAttributes(ByteBuffer buffer) throws IOException {
    byte[] delimiter = new byte[]{9};
    Text attributes = new Text();

    for(int i = 0; i < this.fieldDescriptors.size(); ++i) {
        FieldDescriptor descriptor = (FieldDescriptor)this.fieldDescriptors.get(i);
        byte[] fldBytes = new byte[descriptor.getFieldLength()];
        buffer.get(fldBytes, 0, fldBytes.length);
        String charset = System.getProperty("sedona.global.charset", "default");
        Boolean utf8flag = charset.equalsIgnoreCase("utf8");
        byte[] attr = utf8flag ? fldBytes : fastParse(fldBytes, 0, fldBytes.length).trim().getBytes();
        if (i > 0) {
            attributes.append(delimiter, 0, 1);
        }

        attributes.append(attr, 0, attr.length);
    }

    return attributes.toString();
}

*** 到这里我们就很清楚了,它用了字符串拼接的方式来直接读取的 dbf 文件,而不是使用 GeoTools 读取字段值的。也就是说它是直接读取 dbf 文件中的字符,一行一行的读,然后拼接字符串,因为直接读取的文本文件,所以它无法获取到字段的类型信息:

在这里插入图片描述

这点我们可以去 Sedona 底层 Scala 代码中去验证。

5、Adapter.toDf

要想验证,它是否真的无法获取到字段类型,我们得去找 RDD 是如何成为 DataFrame 的。

想将 ShapefileReader.readToGeometryRDD 读取到的 SpatialRDD 转为 DataFrame ,一般都是调方法:

SpatialRDD<Geometry> geomRDD = ShapefileReader
								.readToGeometryRDD(JavaSparkContext.fromSparkContext(
								        sedona.sparkContext()), shpPath);
Dataset<Row> df = Adapter.toDf(geomRDD, sedona);

Adapter.toDf 就是将 SpatialRDD 转为 DataFrame 的关键点,点进去后就是 Scala 代码了,我的编译器无法下载源代码,可能是没开启 Scala 的扩展插件,我上 GitHub 上看:

在这里插入图片描述

我们能看到,Adapter.toDf 需要一个 SpatialRDD 和一个 SparkSession ,而且,它做了一个判空,调用了下面的那个 toDf 方方法:

def toDf[T <: Geometry](spatialRDD: SpatialRDD[T], sparkSession: SparkSession): DataFrame = {
  import scala.jdk.CollectionConverters._
  if (spatialRDD.fieldNames != null)
    return toDf(spatialRDD, spatialRDD.fieldNames.asScala.toList, sparkSession)
  toDf(spatialRDD = spatialRDD, fieldNames = null, sparkSession = sparkSession);
}

看来下面这个 toDf 才是真正干活的,我们先看看它的代码:

def toDf[T <: Geometry](
    spatialRDD: SpatialRDD[T],
    fieldNames: Seq[String],
    sparkSession: SparkSession): DataFrame = {
  val rowRdd = spatialRDD.rawSpatialRDD.rdd.map[Row](geom => {
    val stringRow = extractUserData(geom)
    Row.fromSeq(stringRow)
  })
  var cols: Seq[StructField] = Seq(StructField("geometry", GeometryUDT))
  if (fieldNames != null && fieldNames.nonEmpty) {
    cols = cols ++ fieldNames.map(f => StructField(f, StringType))
  }
  val schema = StructType(cols)
  sparkSession.createDataFrame(rowRdd, schema)
}

结果已经显而易见了,破绽就是:

fieldNames.map(f => StructField(f, StringType))

它直接写死了,每个字段的类型就是 StringType

五、导致问题的原因

结合代码,还有之前开发人员说的:

一个更恰当的支持 Shapefile 的方式是实现一个基于 Spark DataSourceV2 的 Shapefile 读取器

我猜测将类型写死在代码中的原因是拿不到字段类型(这不废话嘛),拿不到字段类型的原因是没有使用 GeoTools 读取 shp 文件,而是直接读取的 dbf 文件,所以拿不到字段类型。

那为什么不使用 GeoTools 来读取数据呢,因为这涉及到 Hadoop分片 的问题,之前一个文本文件我们可以简单的拆分,但是现在是一个 shp 文件,我们无法通过简单的文本拆分来分片。

如果要达到分片的效果,我们只能拆分图层,一个分片读取图层的一部分。
例如一个图层中有10行要素,我们将它分为两片,那么第一片就是1 ~ 5行要素,用一个List存起来,第二片就是6 ~ 10行要素,也用一个List存起来。

但是如果数据量很大,那么我们就无法拆分数据放到List中并传输了,我们拆分的必须只是一个索引信息而非真实数据,每个分区根据索引读取整个图层中属于自己分区的部分。

但是这就涉及到一个问题,也就是说每个分区都得有一个完整 shp 的备份,这就又涉及到 dfs 了。

总结下来,我猜官方是因为不想引入更复杂的实现(还得引入dfs等),迫不得已选择了直接读取 dbf 文件而不是用 GeoTools 读取数据。换句话说就是 Sedona 当前使用的数据源获取实现不够好,导致获取数据源在某些地方存在掣肘的情况,所以官方开发人员才说:

实现一个基于 Spark DataSourceV2 的 Shapefile 读取器

如果你有实力自己实现一个,那么肯定能避免很多不必要的麻烦,我就算了,我没那个实力也没时间。

好了,以上

写在最后

如果你觉得对你有所帮助,请不吝点赞,Thanks!!!

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值