如何在Spark SQL中选择仅存在于我查询的数据子集中的列?

时间:2019-12-05 18:56:15

标签: apache-spark pyspark apache-spark-sql pyspark-sql aws-glue

我有一个配置为在AWS Glue中运行的Spark作业,该作业从Athena读取数据源,而该数据源又从许多JSON文件中进行了爬网。这些JSON文件大多是 一致的;但是,有些属性具有其他属性则没有。在我的Spark作业中,我正在创建一个数据框,然后使用该数据框转换为Parquet。麻烦在于,由于我选择的数据可能存在或可能不存在,具体取决于单个记录,这是错误条件。

工作的相关部分看起来像这样:

from awsglue.job import Job
from awsglue.context import GlueContext, SQLContext
from pyspark.context import SparkContext
from pyspark.sql.functions import col

sc = SparkContext()
sqlContext = SQLContext(sc)
glueContext = GlueContext(sc)
job = Job(glueContext)

# ...

datasource0 = glueContext.create_dynamic_frame.from_catalog(
    database="mynamespace",
    table_name="my_crawled_table_of_json",
    transformation_ctx="datasource0",
)
df = datasource0.toDF()
result = df.select(
    col("nested.always.present.field"), # this one is always present,
    col("nested.maybe.present.field"), # this one is only sometimes present
    # ...
    col("nested.another.value"),
)

result.write.mode("overwrite").format("parquet").save("s3://my-bucket/path/to/output")
job.commit()

当我运行作业时,我在日志中看到的错误是对此的一种变化:

  

org.apache.spark.sql.AnalysisException:此类结构字段可能不会总是出现在另一个字段中,等等;       在org.apache.spark.sql.catalyst.expressions.ExtractValue $ .findField(complexTypeExtractors.scala:85)

同样,问题是每个记录上都不存在maybe嵌套字段。当我定义要选择的列时,是否可以通过某种方式表示“在存在时选择此列 否则选择null”?

5 个答案:

答案 0 :(得分:1)

一种解决方案是使用df.schema获取所有字段,然后使用一些递归函数构建嵌套的字段路径。 通过这种方式,您可以确定可以选择的列名,从而仅选择数据集中存在的列名。

这是此类功能的一个示例:

def list_fields(field: str, dt: DataType):
    fields = []
    if isinstance(dt, StructType):
        for f in dt.fields:
            path = f"{field}.{f.name}" if field else f.name
            fields.extend(list_fields(path, f.dataType))
    else:
        fields.append(field)

    return fields

示例:

json_string = '{"nested":{"always": {"present": {"field": "val1"}}, "another": {"value": "val2"}, ' \
                  '"single":"value"}}'
df = spark.read.json(sc.parallelize([json_string]))
available_columns = list_fields(None, df.schema)

print(available_columns)

# output
['nested.always.present.field', 'nested.another.value', 'nested.single']

现在,您可以使用该列表构建选择表达式。像这样:

columns_to_select = ["nested.always.present.field", "nested.another.value",
                     "nested.maybe.present.field", "nested.single"]

# filter your columns using the precedent list    
select_expr = [col(c).alias(f"`{c}`") if c in available_columns else lit(None).alias(f"`{c}`") for c in columns_to_select]
df.select(*select_expr).show()

输出:

+-----------------------------+----------------------+----------------------------+---------------+
|`nested.always.present.field`|`nested.another.value`|`nested.maybe.present.field`|`nested.single`|
+-----------------------------+----------------------+----------------------------+---------------+
|                         val1|                  val2|                        null|          value|
+-----------------------------+----------------------+----------------------------+---------------+

编辑:

也可以使用@ user10938362评论中的解决方案linked

select_expr = [col(c).alias(f"`{c}`") if has_column(df, c) else lit(None).alias(f"`{c}`") for c in columns_to_select]
df.select(*select_expr).show()

虽然它要短得多,但是您需要在DF上检查每一列的选择,而在上述解决方案中,您只需要循环遍历该模式以首先提取列名,然后根据它检查您的选择。

答案 1 :(得分:1)

因此,在尝试调试此问题时遇到了许多问题。最终,一些较早的评论者是对的,我可以使用定义为in this question's answer并复制到此处的hasColumn函数来获得:

def has_column(df, col):
    try:
        df[col]
        return True
    except AnalysisException:
        return False

我最终定义了一个我想选择的(嵌套的)列名的列表,然后使用列表理解来选择它们,如@jxc所建议的:


cols = [
    "nested.always.present.field",
    "nested.maybe.present.field",
    # ...
    "nested.another.value"
]
result = df.select(
    [lit(None).alias(c) if not has_column(df, c) else col(c).alias(c) for c in cols]
)

但是后来我遇到了另一个问题。没有在我上面的原始问题中列出;我一直在对数据帧进行其他转换,然后再将输出保存为可利用Spark SQL的withColumn函数的木地板。这也带来了问题,因为除非您使用反引号将字符转义,否则点号不能在该函数(实际上是col函数)中发挥很好的作用。所以我必须做这样的事情:

result = df.withColumn("my_id", monotonically_increasing_id())
for c in cols:
    result = result.withColumn(
        c, regexp_replace(col("`" + c + "`"), "oldvalue", "newvalue")
    )

在没有反引号的情况下,它试图遍历已被扁平化的列,因此引发了另一个异常。最后,通过AWS Glue控制台进行调试是完全不切实际的,因为更改的周转时间太长了。因此,我尝试在没有GlueContext的情况下尽最大可能在本地计算机上重新创建内容,并学到了重要的一课:

glueContext.create_dynamic_frame.from_catalog创建一个RDD,然后需要将其强制转换为数据框。 spark.read.json没有。后者直接创建一个数据框。这一混乱点使我头痛,可以轻易避免。我很感激我能为我工作,尽管我正在为自己的问题输入答案,但我的确欠了多个评论者的答案,所以我将功劳归于其他人。

答案 2 :(得分:0)

根据以下代码,我对awsglue不太熟悉

df = datasource0.toDF()

我假设datasource0是一个RDD,每行都有nested个json对象。

使用选择语法而不是转换为ToDF

为什么不将JSON转换为字典的字典,然后使用dict.get(“ key”),即使该键未保留在dict中,get方法也将返回None,然后将RDD转换为DF。 / p>

答案 3 :(得分:0)

您可以使用select + case / when函数。类似于:pyspark replace multiple values with null in dataframe


更新示例:

这是使用when-otherwise的上述情况的示例:

import json
from pyspark.sql import functions as F

a=[
  json.dumps({'a':"1", 'b':2, 'c':3}),
  json.dumps({'a':"4", 'b':5, 'inner_node': {'inner_a': 2}})
]
jsonRDD = sc.parallelize(a)
df = spark.read.json(jsonRDD)
df.printSchema()
df.select(F.when(df["inner_node.inner_a"].isNotNull(), df.inner_node.inner_a).otherwise("your_placeholder_value").alias("column_validation") ).show()

以上代码将输出:

root
 |-- a: string (nullable = true)
 |-- b: long (nullable = true)
 |-- c: long (nullable = true)
 |-- inner_node: struct (nullable = true)
 |    |-- inner_a: long (nullable = true)

+--------------------+
|   column_validation|
+--------------------+
|your_placeholder_...|
|                   2|
+--------------------+

答案 4 :(得分:0)

好吧,您始终可以使用null用伪值(主要是withColumn)创建该列,然后选择它。

  1. 使用df.columns获取数据框的列

  2. 使用If语句,检查是否存在可选列。如果存在,则按原样传递数据帧,不存在时调用withColumn函数并创建列。

  3. 将数据框传递给select语句。

df = datasource.toDF()
if 'optional column' in data df.columns:
    pass
else:
    df=df.withColumn('optional column', lit(''))

result = df.select(...)

但是,尽管源文件中缺少此列,您仍将在输出文件中看到此列。