我有一个配置为在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”?
答案 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
)创建该列,然后选择它。
使用df.columns获取数据框的列
使用If
语句,检查是否存在可选列。如果存在,则按原样传递数据帧,不存在时调用withColumn
函数并创建列。
将数据框传递给select语句。
df = datasource.toDF()
if 'optional column' in data df.columns:
pass
else:
df=df.withColumn('optional column', lit(''))
result = df.select(...)
但是,尽管源文件中缺少此列,您仍将在输出文件中看到此列。