使用 Databricks 进行数据工程——使用 Apache Spark 进行数据导入和数据提取

517 阅读22分钟

Apache Spark 是一个强大的分布式计算框架,能够处理大规模数据处理任务。在处理数据时,最常见的任务之一是从各种来源加载数据并将其写入各种格式。在本实践章节中,你将学习如何使用 Python 通过 Apache Spark 加载和写入数据文件。

在本章中,我们将涵盖以下内容:

  • 使用 Apache Spark 读取 CSV 数据
  • 使用 Apache Spark 读取 JSON 数据
  • 使用 Apache Spark 读取 Parquet 数据
  • 使用 Apache Spark 解析 XML 数据
  • 在 Apache Spark 中处理嵌套数据结构
  • 在 Apache Spark 中处理文本数据
  • 使用 Apache Spark 写入数据

在本章结束时,你将学会如何读取、写入、解析和操作 CSV、JSON、Parquet 和 XML 格式的数据。你还将学会如何使用自然语言处理(NLP)分析文本数据,以及如何通过缓冲、压缩和分区优化数据写入。

技术要求

在开始之前,确保你的 Docker Compose 镜像已启动并运行,并在本地主机上打开运行的 JupyterLab 服务器(http://127.0.0.1:8888/lab)。同时,确保你已经克隆了本书的Git 仓库,并能访问本章使用的笔记本和数据。

在运行完代码示例后,请记得停止 Docker Compose 文件中定义的所有服务。你可以通过执行以下命令来完成此操作:

$ docker-compose stop

你可以在以下网址找到本章的笔记本和数据:github.com/PacktPublis…

使用 Apache Spark 读取 CSV 数据

读取 CSV 数据是数据工程和分析中常见的任务,Apache Spark 提供了一种强大且高效的方法来处理这些数据。Apache Spark 支持多种文件格式,包括 CSV,并提供了许多选项来读取和处理这些数据。在本教程中,我们将学习如何使用 Python 通过 Apache Spark 读取 CSV 数据。

操作步骤:

  1. 导入库:导入所需的库并创建一个 SparkSession 对象:
from pyspark.sql import SparkSession

spark = (SparkSession.builder
    .appName("read-csv-data")
    .master("spark://spark-master:7077")
    .config("spark.executor.memory", "512m")
    .getOrCreate())

spark.sparkContext.setLogLevel("ERROR")
  1. 读取带推断模式的 CSV 数据:使用 SparkSession 的 read 方法读取 CSV 文件。在下面的代码中,我们使用 SparkSession 的 format 方法将文件格式指定为 csv。然后,我们将 header 选项设置为 true,以指示 CSV 文件的第一行包含列名。最后,我们指定 CSV 文件的路径,并使用 load 方法将其加载到 DataFrame 中:
df = (spark.read.format("csv")
    .option("header", "true")
    .load("../data/netflix_titles.csv"))

注意:如果你的 CSV 文件没有标题行,可以将 header 选项设置为 false,如下所示:

df = (spark.read.format("csv")
    .option("header", "false")
    .load("../data/netflix_titles.csv"))
  1. 显示 DataFrame 中的样本数据:可以使用 show() 方法显示 DataFrame 的内容。这将显示 DataFrame 的前 20 行。如果你想显示更多或更少的行,可以向 show() 方法传递一个整数参数:
# 显示 DataFrame 内容
df.show()

# 或者
# df.show(10, truncate=False)
  1. 读取带显式模式的 CSV 数据:首先,你需要为你的 CSV 文件定义模式。可以使用 Spark SQL 中的 StructType 和 StructField 类来完成。例如,如果你的 CSV 文件有三列 "name"、"age" 和 "gender",并且你想指定 "name" 是字符串,"age" 是整数,"gender" 是字符串,可以这样定义模式:
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, DateType

schema = StructType([
    StructField("show_id", StringType(), True),
    StructField("type", StringType(), True),
    StructField("title", StringType(), True),
    StructField("director", StringType(), True),
    StructField("cast", StringType(), True),
    StructField("country", StringType(), True),
    StructField("date_added", DateType(), True),
    StructField("release_year", IntegerType(), True),
    StructField("rating", StringType(), True),
    StructField("duration", StringType(), True),
    StructField("listed_in", StringType(), True),
    StructField("description", StringType(), True)
])
  1. 读取 CSV 文件:接下来,需要使用 spark.read.format("csv") 方法读取 CSV 文件,并将模式作为参数传递。在此示例中,我们读取位于 ../data/netflix_titles.csv 的 CSV 文件,第一行为标题,并使用之前定义的模式:
df = (spark.read.format("csv")
    .option("header", "true")
    .schema(schema)
    .load("../data/netflix_titles.csv"))

可以使用 show() 方法显示 DataFrame 的内容。这将显示 DataFrame 的前五行。如果你想显示更多或更少的行,可以更改 show() 方法的 n 参数:

df.show()
  1. 停止 Spark 会话:最后,我们需要停止 Spark 会话,以释放 Spark 使用的资源:
spark.stop()

常见问题及解决方案

  1. 问题:数据中包含分隔符值
    解决方案:可以在读取 CSV 文件时使用 option("escapeQuotes", "true") 方法来指定如何处理列值中的分隔符值。例如:
df = (spark.read.format("csv")
    .option("header", "true")
    .option("nullValue", "null")
    .option("escapeQuotes", "true")
    .schema(schema)
    .load("../data/netflix_titles.csv"))
  1. 问题:空值和缺失值处理不当
    解决方案:可以在读取 CSV 文件时使用 option("nullValue", "null") 方法来指定 CSV 文件中空值的表示方式。还可以使用 option("emptyValue", "") 方法来指定如何处理空值。例如:
df = (spark.read.format("csv")
    .option("header", "true")
    .option("nullValue", "null")
    .option("emptyValues", "")
    .schema(schema)
    .load("../data/netflix_titles.csv"))
  1. 问题:日期格式不同,处理不当
    解决方案:可以在读取 CSV 文件时使用 option("dateFormat", "LLLL d, y") 方法来指定 CSV 文件中日期列值的表示方式。例如:
df = (spark.read.format("csv")
    .option("header", "true")
    .option("nullValue", "null")
    .option("dateFormat", "LLLL d, y")
    .schema(schema)
    .load("../data/netflix_titles.csv"))

注意:当我们在步骤 2 中使用 read API 时,Apache Spark 并未执行任何作业。这是因为 Apache Spark 使用延迟求值技术,延迟执行转换操作直到调用某个动作。这允许 Spark 优化执行计划并从故障中恢复。然而,这也可能导致调试和故障排除中的一些挑战。要有效地使用延迟求值,了解转换和动作之间的区别,并考虑代码中转换和动作的顺序和时间是很重要的。

更多信息

以下是一些关于使用 Apache Spark 读取 CSV 数据的附加细节:

  • 指定读取 CSV 文件时的选项:除了指定 header 选项,还可以在读取 CSV 文件时指定其他选项。例如,可以使用 delimiter 选项指定 CSV 文件中使用的分隔符(例如,option("delimiter", "|") 表示管道分隔符文件)或使用 inferSchema 选项自动推断 DataFrame 列的数据类型(例如,option("inferSchema", "true"))。
  • 处理缺失或格式错误的数据:在使用 Spark 读取 CSV 文件时,可能会遇到导致错误的缺失或格式错误的数据。要处理缺失数据,可以使用 nullValue 选项指定 CSV 文件中表示空值的值(例如,option("nullValue", "NA") 表示文件中 "NA" 表示空值)。要处理格式错误的数据,可以使用 mode 选项指定如何处理解析错误(例如,option("mode", "PERMISSIVE") 忽略解析错误并继续处理文件)。
  • 处理大型 CSV 文件:在处理大型 CSV 文件时,如果尝试一次将整个文件加载到 DataFrame 中,可能会遇到内存和性能问题。为避免这种情况,可以使用 spark.read.csv() 方法,并使用 maxColumnsmaxCharsPerColumn 选项限制 Spark 一次读取的列数和每列字符数。还可以使用 spark.readStream.csv() 方法将大型 CSV 文件作为流读取,从而允许实时处理从磁盘读取的数据。

参考资料

使用 Apache Spark 读取 JSON 数据

在本教程中,我们将学习如何使用 Apache Spark 导入和加载 JSON 数据。最后,我们还将介绍一些处理 JSON 数据时常见的数据工程任务。

操作步骤:

  1. 导入库:导入所需的库并创建一个 SparkSession 对象:
from pyspark.sql import SparkSession
from pyspark.sql.functions import *

spark = (SparkSession.builder
    .appName("read-json-data")
    .master("spark://spark-master:7077")
    .config("spark.executor.memory", "512m")
    .getOrCreate())

spark.sparkContext.setLogLevel("ERROR")
  1. 将 JSON 数据加载到 Spark DataFrame:可以使用 SparkSession 对象的 read 方法从文件或目录加载 JSON 数据。将 multiLine 选项设置为 true 以解析跨多行的记录。我们需要传递 JSON 文件的路径作为参数:
df = (spark.read.format("json")
    .option("multiLine", "true")
    .load("../data/nobel_prizes.json"))
  1. 查看 DataFrame 的模式:可以使用 DataFrame 对象的 printSchema() 方法来显示 JSON 数据的模式。这将帮助我们了解数据的结构:
df.printSchema()
  1. 查看 DataFrame 中的数据:可以使用 DataFrame 对象的 show() 方法来显示 JSON 文件中的数据。这将帮助我们查看数据的外观和结构:
df.show()
  1. 扁平化 JSON 中的嵌套结构:如果 JSON 数据具有嵌套结构,我们可以使用 explode 函数来简化数据。在此示例中,我们将扁平化包含 id、firstname、surname/lastname、share 和 motivation 字段的 laureates 数组字段为单独的列。explode 函数会为数组或映射中的每个元素创建一个新行。我们使用 withColumn 方法替换现有的 laureates 列。该方法接受两个参数——新列的名称和定义新列值的表达式:
df_flattened = (df
    .withColumn("laureates", explode(col("laureates")))
    .select(col("category"),
            col("year"),
            col("overallMotivation"),
            col("laureates.id"),
            col("laureates.firstname"),
            col("laureates.surname"),
            col("laureates.share"),
            col("laureates.motivation"))
)

df_flattened.show(truncate=False)
  1. 使用模式在读取数据时强制执行数据类型:如果我们想在 JSON 数据上强制执行数据类型,可以使用 pyspark.sql.types 模块中的 StructType、StructField 和数据类型类来定义模式。然后,我们可以使用 schema 参数将模式传递给 read 方法:
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, DateType, ArrayType

json_schema = StructType([
    StructField('category', StringType(), True),
    StructField('laureates', ArrayType(StructType([
        StructField('firstname', StringType(), True),
        StructField('id', StringType(), True),
        StructField('motivation', StringType(), True),
        StructField('share', StringType(), True),
        StructField('surname', StringType(), True)
    ]), True), True),
    StructField('overallMotivation', StringType(), True),
    StructField('year', IntegerType(), True)
])

json_df_with_schema = (
    spark.read.format("json")
    .schema(json_schema)
    .option("multiLine", "true")
    .option("mode", "PERMISSIVE")
    .option("columnNameOfCorruptRecord", "corrupt_record")
    .load("../data/nobel_prizes.json")
)
  1. 停止 SparkSession 对象:可以使用 SparkSession 对象的 stop() 方法来停止 Spark 应用程序并释放资源:
spark.stop()

更多信息

你还可以使用选项来指定如何读取 JSON 数据。例如,你可以指定模式来强制执行 JSON 数据或选项来处理损坏的数据。让我们详细看看一些内容。

get_json_object() 和 json_tuple() 函数

在 Apache Spark 中,get_json_object()json_tuple() 函数用于从 JSON 字符串中提取值。

  • get_json_object() 函数根据指定的 JSON 路径表达式从 JSON 字符串中提取 JSON 对象,并返回提取对象的字符串表示形式。
from pyspark.sql.functions import get_json_object
from pyspark.sql.types import StringType

# 创建一个带有 JSON 字符串列的 DataFrame
df = spark.createDataFrame([
    (1, '{"name": "Alice", "age": 25}'),
    (2, '{"name": "Bob", "age": 30}')
], ["id", "json_data"])

# 从 JSON 字符串列中提取 "name" 字段
name_df = df.select(get_json_object("json_data", "$.name").alias("name"))

# 将提取的值转换为字符串
name_str_df = name_df.withColumn("name_str", name_df["name"].cast(StringType()))
name_str_df.show()

输出:

+-----+--------+
| name|name_str|
+-----+--------+
|Alice|   Alice|
|  Bob|     Bob|
+-----+--------+
  • json_tuple() 函数根据指定的一组 JSON 路径表达式从 JSON 字符串中提取多个值,并返回提取值的元组。
from pyspark.sql.functions import json_tuple

# 创建一个带有 JSON 字符串列的 DataFrame
df = spark.createDataFrame([
    (1, '{"name": "Alice", "age": 25}'),
    (2, '{"name": "Bob", "age": 30}')
], ["id", "json_data"])

# 从 JSON 字符串列中提取 "name" 和 "age" 字段
name_age_df = df.select(json_tuple("json_data", "name", "age").alias("name", "age"))
name_age_df.show()

输出:

+-----+---+
| name|age|
+-----+---+
|Alice| 25|
|  Bob| 30|
+-----+---+

处理损坏的数据

如果 JSON 数据包含损坏的记录,我们可以使用 option 方法将 mode 参数设置为 "PERMISSIVE"。这将导致 Spark 将包含损坏数据的字段设置为 null,并将损坏的记录存储在名为 corrupt_record 的新列中,可用于调查和处理错误:

json_df_with_schema = (spark.read.format("json")
    .schema(json_schema)
    .option("multiLine", "true")
    .option("mode", "PERMISSIVE")
    .option("columnNameOfCorruptRecord", "corrupt_record")
    .load("../data/nobel_prizes.json"))

flatten() 和 collect_list() 函数

在处理 JSON 数据时,可能需要将数组的数组转换为单个数组,合并所有内部数组的元素。它返回一个包含合并数组的新 DataFrame。

from pyspark.sql.functions import flatten, collect_list

# 创建一个带有数组的数组列的 DataFrame
df = spark.createDataFrame([
    (1, [[1, 2], [3, 4], [5, 6]]),
    (2, [[7, 8], [9, 10], [11, 12]])
], ["id", "data"])

# 使用 collect_list() 函数按指定列分组
collect_df = df.select(collect_list("data").alias("data"))
collect_df.show(truncate=False)

输出:

+-------------------------------------------------------+
|data                                                   |
+-------------------------------------------------------+
|[[[7, 8], [9, 10], [11, 12]], [[1, 2], [3, 4], [5, 6]]]|
+-------------------------------------------------------+

现在,我们将合并内部数组元素:

# 使用 flatten() 函数合并所有内部数组的元素
flattened_df = collect_df.select(flatten("data").alias("merged_data"))
flattened_df.show(truncate=False)

输出:

+---------------------------------------------------+
|merged_data                                        |
+---------------------------------------------------+
|[[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]]|
+---------------------------------------------------+

注意:flatten() 函数仅适用于数组列。如果你有多个级别的数组嵌套结构,可以在使用 flatten() 函数之前使用 explode() 函数来扁平化结构。

参考资料

使用 Apache Spark 读取 Parquet 数据

Apache Parquet 是一种列式存储格式,设计用于处理大规模数据集。它针对复杂数据类型的高效压缩和编码进行了优化。而 Apache Spark 是一个快速且通用的集群计算系统,专为大规模数据处理而设计。

在本教程中,我们将探索如何使用 Python 通过 Apache Spark 读取 Parquet 数据。

操作步骤:

  1. 导入库:导入所需的库并创建一个 SparkSession 对象:
from pyspark.sql import SparkSession

spark = (SparkSession.builder
    .appName("read-parquet-data")
    .master("spark://spark-master:7077")
    .config("spark.executor.memory", "512m")
    .getOrCreate())

spark.sparkContext.setLogLevel("ERROR")
  1. 加载 Parquet 数据:我们使用 spark.read.format("parquet") 方法将 Parquet 数据加载到 Spark DataFrame 中,如下所示:
df = (spark.read.format("parquet")
    .load("../data/recipes.parquet"))
  1. 查看 DataFrame 的模式:可以使用 DataFrame 对象的 printSchema() 方法来显示 JSON 数据的模式。这将帮助我们了解数据的结构:
df.printSchema()
  1. 查看 DataFrame 中的数据:可以使用 DataFrame 对象的 show() 方法来显示数据。这将帮助我们查看数据的外观和结构:
df.show()
  1. 停止 SparkSession 对象:可以使用 SparkSession 对象的 stop() 方法来停止 Spark 应用程序并释放资源:
spark.stop()

处理 Parquet 数据时常见的场景

让我们快速回顾一下处理 Parquet 数据时可能遇到的一些挑战。

读取分区数据

要使用 Apache Spark 加载分区的 Parquet 文件,可以使用 spark.read API 并指定包含 Parquet 文件的目录路径。Spark 将自动识别该目录为分区数据集并相应地加载数据。例如,data/partitioned_recipes 路径包含按食谱类别分区的食谱数据,我们可以如下加载数据:

df_partitioned = (spark.read.format("parquet")
    .load("../data/partitioned_recipes"))
df_partitioned.printSchema()

你也可以通过加载通配符文件路径来读取部分分区,如下所示:

df_partitioned = (spark.read.format("parquet")
    .load("../data/partitioned_recipes/DatePublished=2020-01*"))
df_partitioned.printSchema()

模式合并

默认情况下,如果你尝试读取具有不同模式的 Parquet 文件,Spark 不会合并这些模式,因为 spark.sql.parquet.mergeSchema 设置为 false。然而,如果你将 mergeSchema 选项设置为 true,Spark 将合并文件的模式并尝试创建一个统一的模式来读取所有文件。

在此示例中,mergeSchema 设置为 true 以启用模式合并。Spark 将尝试合并路径指定的目录中所有文件的模式。如果模式兼容,Spark 将成功创建所有文件的统一模式。当我们读取所有分区时,Spark 推断了 ReviewCount 列,但在推断的模式中未包含 Images 列。当我们只读取一些分区(即 2020-01*)时,Spark 推断模式包含 Images 列,但未包含 ReviewCount 列。这是因为默认情况下 mergeSchema 选项设置为 false。将 mergeSchema 设置为 true 后,推断的模式中包含了 ReviewCountImages 列。

下面是一个示例代码片段:

df_merged_schema = (spark.read.format("parquet")
    .option("mergeSchema", "true")
    .load("../data/partitioned_recipes"))

注意:模式合并可能是一个昂贵的操作,尤其是当文件具有不同且复杂的模式时。在某些情况下,模式合并可能无法完成,在这种情况下,Spark 仍然会抛出错误。此外,模式合并可能不会总是生成适合你用例的最佳模式。在这种情况下,你可能需要手动处理模式演化,以确保模式适合你的需求。

参考资料

使用 Apache Spark 解析 XML 数据

读取 XML 数据是大数据处理中常见的任务,Apache Spark 提供了多种选项来读取和处理 XML 数据。在本教程中,我们将探索如何使用 Apache Spark 内置的 XML 数据源读取 XML 数据。我们还将介绍一些处理 JSON 数据时常见的问题以及如何解决它们。最后,我们将介绍一些数据工程中常见的任务。

注意:我们还需要在集群上安装 spark-xml 包。spark-xml 包是 Databricks 发布的一个用于 Apache Spark 的第三方库。该包使得在 Spark 应用程序中处理 XML 数据变得可能,并提供了使用 Spark DataFrame API 读取和写入 XML 文件的能力,使其易于与其他 Spark 组件集成并执行复杂的数据分析任务。我们可以通过运行以下命令来安装该包:

$SPARK_HOME/bin/spark-shell --packages com.databricks:spark-xml_2.12:0.16.0

我们已经将此包安装到我们本地运行的 Docker 镜像中。

操作步骤:

  1. 导入库:导入所需的库并创建一个 SparkSession 对象。通过添加 .config('spark.jars.packages', 'com.databricks:spark-xml_2.12:0.16.0'),我们添加了处理 XML 数据所需的 JAR 文件:
from pyspark.sql import SparkSession
from pyspark.sql.functions import *

spark = (SparkSession.builder
    .appName("read-xml-data")
    .config('spark.jars.packages', 'com.databricks:spark-xml_2.12:0.16.0')
    .master("spark://spark-master:7077")
    .config("spark.executor.memory", "512m")
    .getOrCreate())

spark.sparkContext.setLogLevel("ERROR")
  1. 定义 XML 文件路径并创建 DataFrame:我们定义要读取的 XML 文件路径,并使用 spark.read.format() 方法创建 DataFrame,参数为 "xml"。我们还将 rowTag 选项设置为我们希望用作行标签的 XML 元素的名称。要处理包含多个嵌套元素的更复杂的 XML 文件,我们可以使用 option("rootTag", "tagname") 函数来指定应视为 DataFrame 根的 XML 元素:
# 读取 XML 文件到 DataFrame
df = (spark.read.format("com.databricks.spark.xml")
    .option("rowTag", "row")
    .load("../data/nobel_prizes.xml"))
  1. 显示 DataFrame:我们可以使用 show() 方法以表格格式显示 DataFrame 的内容:
df.show()
  1. 从 DataFrame 访问数据:我们可以使用 select() 方法从 DataFrame 中选择特定列。我们将列名作为参数传递给该方法,用逗号分隔:
df.select("category", "year").show()

如果要访问数组、映射和结构等复杂类型中的元素,可以使用 getItem 方法提取单个元素或值。它接受一个指定要从复杂类型中提取的元素索引的参数。例如,在我们的 DataFrame 中,laureates 列是对象数组,我们可以使用 getItem 提取数组的第一个元素:

df.select("category", "year", col("laureates").getItem(0).alias("first_laureate")).show()

此外,如果我们想从 XML 文件中的嵌套元素访问数据,可以使用 .(点)操作符来访问子元素,如下示例:

df.select("category", "year", col("laureates").getItem(0).id).show()

这将从 laureates 数组元素中选择 categoryyear 字段,并获取第一个项目的 id 值。

  1. 扁平化嵌套结构:如果 XML 数据具有嵌套结构,我们可以使用 explode 函数简化数据。在此示例中,我们将包含 idfirstnamesurnamesharemotivation 字段的 laureates 字段(一个数组)扁平化为单独的列。explode 函数会为数组或映射中的每个元素创建一个新行。我们使用 withColumn 方法替换现有的 laureates 列。该方法接受两个参数——新列的名称和定义新列值的表达式:
df_flattened = (df
    .withColumn("laureates", explode(col("laureates")))
    .select(col("category"),
            col("year"),
            col("overallMotivation"),
            col("laureates.id"),
            col("laureates.firstname"),
            col("laureates.surname"),
            col("laureates.share"),
            col("laureates.motivation"))
)

df_flattened.show(truncate=False)
  1. 使用模式强制数据类型:如果我们想在 XML 数据上强制执行数据类型,可以使用 pyspark.sql.types 模块中的 StructTypeStructField 和数据类型类定义模式。然后,我们可以使用 schema 参数将模式传递给 read 方法:
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, ArrayType

schema = StructType([
    StructField('category', StringType(), True),
    StructField('laureates', ArrayType(StructType([
        StructField('firstname', StringType(), True),
        StructField('id', StringType(), True),
        StructField('motivation', StringType(), True),
        StructField('share', StringType(), True),
        StructField('surname', StringType(), True)
    ]), True), True),
    StructField('overallMotivation', StringType(), True),
    StructField('year', IntegerType(), True)
])

# 读取 XML 文件到 DataFrame
df_with_schema = (spark.read.format("com.databricks.spark.xml")
    .schema(schema)
    .option("rowTag", "row")
    .load("../data/nobel_prizes.xml"))

df_with_schema.show()
  1. 停止 SparkSession 对象:可以使用 SparkSession 对象的 stop() 方法来停止 Spark 应用程序并释放资源:
spark.stop()

更多信息

Apache Spark 提供了各种其他选项,可以用来定制 XML 读取器的行为。这些选项包括:

  • excludeAttribute:一个逗号分隔的属性名列表,这些属性应从 DataFrame 中排除
  • inferSchema:指定是否应从数据中推断模式
  • ignoreSurroundingSpaces:指定是否应忽略周围的空格
  • mode:指定在遇到损坏记录或解析错误时读取器的行为

参考资料

使用 Apache Spark 处理嵌套数据结构

在本教程中,我们将逐步介绍如何使用 Apache Spark 处理数组、映射等嵌套数据结构。通过本教程,你将获得使用 Apache Spark 分布式计算功能处理复杂数据类型的基本知识和实际技能。

操作步骤:

  1. 导入库:导入所需的库并创建一个 SparkSession 对象。SparkSession 是 Spark 应用程序的统一入口。它提供了一种简化的方式与各种 Spark 功能交互,如弹性分布式数据集(RDD)、DataFrame、数据集、SQL 查询、流处理等。可以使用 builder 方法创建一个 SparkSession 对象,该方法允许你配置应用程序名称、主 URL 和其他选项。我们还将定义 SparkContext,它是 Spark 功能的入口。它表示与 Spark 集群的连接,并负责协调和分发集群上的操作:
from pyspark.sql import SparkSession
from pyspark.sql.functions import *

spark = (SparkSession.builder
    .appName("nested-dataframe")
    .master("spark://spark-master:7077")
    .config("spark.executor.memory", "512m")
    .getOrCreate())

spark.sparkContext.setLogLevel("ERROR")
  1. 加载数据:我们使用 spark.read.format("json") 方法将 JSON 数据加载到 Spark DataFrame 中,如下示例所示:
df = (spark.read.format("json")
    .option("multiLine", "true")
    .load("../data/Stanford Question Answering Dataset.json"))
  1. 使用 explode 函数展开嵌套数据:如果我们想展开 paragraphs 列并为每个段落创建一个新行,可以使用 explode 函数。此外,为了从嵌套数据结构中提取数据,我们可以使用点表示法。例如,要提取每个标题的 contextqas,我们可以使用以下代码:
df_exploded = (
    df.select("title",
        explode("paragraphs").alias("paragraphs"))
    .select("title",
        col("paragraphs.context").alias("context"),
        explode(col("paragraphs.qas")).alias("questions"))
)

df_exploded.show()
  1. 使用数组获取唯一值:我们还可以使用高阶函数(HOFs)来就地操作嵌套列。例如,可以使用 array_distinct 函数对 qas 列中的答案数组进行去重,以使数组中只有不同的答案,如下面的代码所示:
df_array_distinct = (
    df_exploded.select("title", "context",
        col("questions.id").alias("question_id"),
        col("questions.question").alias("question_text"),
        array_distinct("questions.answers").alias("answers"))
)

df_array_distinct.show()
  1. 停止 SparkSession 对象:可以使用 SparkSession 对象的 stop() 方法来停止 Spark 应用程序并释放资源:
spark.stop()

常见问题和注意事项

在处理嵌套数据结构时,你可能会遇到以下常见问题:

使用 explode 时出现大量行

explode() 函数可能会导致大量行,这可能会使处理效率降低。如果可能,尽量避免使用 explode 函数。如果确实需要使用 explode,请考虑对数据的子集使用,或在展开前对数据进行聚合以减少结果行数。

使用点表示法处理深度嵌套数据结构

对于深度嵌套的数据结构,点表示法可能难以使用。使用 getItem 函数代替点表示法来提取嵌套字段。getItem 函数接受一个索引或键作为参数,并返回数组或映射中的相应元素。例如,使用 getItem 提取每个问题的第一个答案的值,可以使用以下代码。在此示例中,我们使用 getItem 函数提取 answers 数组中的第一个元素,然后使用 getField 函数提取结果结构中的 text 字段:

(df_array_distinct
    .select("title", "context", "question_text",
        col("answers").getItem(0).getField("text"))
    .show())

处理嵌套数据中的空值

嵌套数据可能包含缺失或空值,这可能会在尝试提取数据时导致错误。使用 isNullisNotNull 函数来过滤掉具有缺失或空值的行。例如,要过滤掉 answers 数组中第一个答案的 text 字段为空的所有行,可以使用以下代码:

(df_array_distinct
    .filter(col("answers").getItem(0).getField("text").isNotNull())
    .show())

更多信息

除了 explodefilter 函数,PySpark 还提供了许多可以用于处理嵌套数据结构的函数,包括 array_containsmap_keysmap_valuesexplode_outer 等。让我们来回顾一下这些函数。

array_contains 函数

array_contains 函数是 Spark 中的一个内置函数,允许你检查数组是否包含指定的元素。该函数接受两个参数:要检查的数组和要搜索的元素。它返回一个布尔值,指示数组是否包含指定的元素。

from pyspark.sql.functions import array_contains

df = spark.createDataFrame(
    [(["apple", "orange", "banana"],),
     (["grape", "kiwi", "melon"],),
     (["pear", "apple", "pineapple"],)],
    ["fruits"]
)

(df.select("fruits", 
    array_contains("fruits", "apple").alias("contains_apple"))
 .show(truncate=False))

输出:

+-------------------------------+---------------------+
|fruits                         |contains_apple       |
+-------------------------------+---------------------+
|[apple, orange, banana]        |true                 |
|[grape, kiwi, melon]           |false                |
|[pear, apple, pineapple]       |true                 |
+-------------------------------+---------------------+

map_keys 和 map_values 函数

map_keys 函数用于从 DataFrame 中的映射列提取键。它接受一个参数,即要从中提取键的映射列的名称。该函数返回映射列中所有键的数组。

map_values 函数用于从 DataFrame 中的映射列提取值。它接受一个参数,即要从中提取值的映射列的名称。该函数返回映射列中所有值的数组。

from pyspark.sql.functions import map_keys, map_values
from pyspark.sql import SparkSession

spark = SparkSession.builder.appName("map_keys_example").getOrCreate()

data = [
    {"user_info": {"name": "Alice", "age": 28, "email": "alice@example.com"}},
    {"user_info": {"name": "Bob", "age": 35, "email": "bob@example.com"}},
    {"user_info": {"name": "Charlie", "age": 42, "email": "charlie@example.com"}}
]

df = spark.createDataFrame(data)
df.show(truncate=False)

(df.select("user_info",
    map_keys("user_info").alias("user_info_keys"),
    map_values("user_info").alias("user_info_values"))
 .show(truncate=False))

输出:

+----------------------------------------------------------+------------------+----------------------------------+
|user_info                                                 |user_info_keys    |user_info_values                  |
+----------------------------------------------------------+------------------+----------------------------------+
|{name -> Alice, email -> alice@example.com, age -> 28}    |[name, email, age]|[Alice, alice@example.com, 28]    |
|{name -> Bob, email -> bob@example.com, age -> 35}        |[name, email, age]|[Bob, bob@example.com, 35]        |
|{name -> Charlie, email -> charlie@example.com, age -> 42}|[name, email, age]|[Charlie, charlie@example.com, 42]|
+----------------------------------------------------------+------------------+----------------------------------+

explode_outer 函数

explode_outer 函数类似于 explode 函数,但有一个重要区别:如果数组或映射列为空,explode_outer 函数仍然会为该空列返回一行,而 explode 函数不会。

data = [
    {"words": ["hello", "world"]},
    {"words": ["foo", "bar", "baz"]},
    {"words": None}
]

df = spark.createDataFrame(data)
(df.select(explode_outer("words").alias("word")).show(truncate=False))

输出:

+-----+
|word |
+-----+
|hello|
|world|
|foo  |
|bar  |
|baz  |
|null |
+-----+

参考资料

在 Apache Spark 中处理文本数据

在本教程中,我们将逐步介绍如何利用 Spark 的强大功能高效地处理和操作文本信息。本教程将为你提供使用 Apache Spark 的分布式计算能力来解决基于文本的挑战所需的基本知识和实际技能。

操作步骤:

  1. 导入库:导入所需的库并创建一个 SparkSession 对象:
from pyspark.sql import SparkSession
from pyspark.sql.functions import *

spark = (SparkSession.builder
    .appName("text-processing")
    .master("spark://spark-master:7077")
    .config("spark.executor.memory", "512m")
    .getOrCreate())

spark.sparkContext.setLogLevel("ERROR")
  1. 加载数据:我们使用 spark.read.format("csv") 方法将 CSV 数据加载到 Spark DataFrame 中,如下示例所示:
df = (spark.read.format("csv")
    .option("header", True)
    .option("multiLine", "true")
    .load("../data/Reviews.csv"))
  1. 探索 DataFrame:加载文本数据到 DataFrame 后,可以使用 show() 方法探索数据。以下代码显示 DataFrame 的前 10 行:
df.show(10, truncate=False)
  1. 使用正则表达式清理文本数据:可以使用 regexp_replace() 函数匹配和提取文本数据中的模式。在这个例子中,[^a-zA-Z] 正则表达式匹配所有非字母字符,regexp_replace() 函数将它们替换为空字符串。我们还使用正则表达式 + 匹配多个空格并将其替换为一个空格。清理后的 DataFrame 覆盖了原始 Text 列:
# 使用正则表达式删除所有非字母字符
df_clean = (df
    .withColumn("Text", regexp_replace("Text", "[^a-zA-Z ]", ""))
    .withColumn("Text", regexp_replace("Text", " +", " ")))

df_clean.show()
  1. 对文本数据进行分词:分词是将文本数据分解为较小单元(如单词或短语)的过程。我们可以使用 split() 函数将文本分解为单词。也可以使用 Apache Spark MLlib 库中的内置分词器函数。以下代码对文本数据进行分词:
# 使用 split 函数分词
df_with_words = (df_clean.withColumn("words", split(df_clean.Text, "\s+")))
df_with_words.show()

# 或者使用 MLlib 的分词器
from pyspark.ml.feature import Tokenizer

tokenizer = Tokenizer(inputCol='Text', outputCol='words')
df_with_words = tokenizer.transform(df_clean)
df_with_words.show()
  1. 移除停用词:停用词是一些不带太多意义的常用词,如 “the”、“and” 和 “in”。我们可以使用 Apache Spark 中的 StopWordsRemover 变换器移除这些停用词。以下代码从 words 列中移除停用词:
from pyspark.ml.feature import StopWordsRemover

remover = StopWordsRemover(inputCol="words", outputCol="filtered_words")
df_stop_words_removed = remover.transform(df_with_words)
df_stop_words_removed.show()
  1. 计算词频:我们可以使用 Apache Spark 中的 explode()groupBy() 函数来计算词频。此代码首先使用 explode() 函数将 filtered_words 列转换为新列 word,每行包含一个单词。然后使用 groupBy() 函数根据 word 列对行进行分组,并计算每组的计数,存储在新列 count 中。最后按 count 降序排序并显示前 10 行:
df_exploded = (df_stop_words_removed
    .select(explode(df_stop_words_removed.filtered_words).alias("word")))

word_count = (df_exploded
    .groupBy("word")
    .count()
    .orderBy("count", ascending=False))

word_count.show(n=100)
  1. 将文本数据转换为数值特征:机器学习算法处理的是数值数据,因此我们需要将文本数据转换为数值特征。MLlib 是 Apache Spark 的可扩展机器学习库,提供了分类、回归、聚类、协同过滤、特征提取和管道等常见的学习算法和实用程序。可以使用 MLlib 库中的 CountVectorizer 函数,将文本数据创建为词袋(BoW)表示:
from pyspark.ml.feature import CountVectorizer

vectorizer = CountVectorizer(inputCol='filtered_words', outputCol='features')
vectorized_data = vectorizer.fit(df_stop_words_removed).transform(df_stop_words_removed)
vectorized_data.show(10, truncate=False)
  1. 保存处理后的数据:最后,可以将处理后的数据保存为所需的任何格式,如 JSON 或 Parquet:
(vectorized_data.repartition(1)
    .write.mode("overwrite")
    .json("../data/data_lake/reviews_vectorized.json"))
  1. 停止 SparkSession 对象:可以使用 SparkSession 对象的 stop() 方法来停止 Spark 应用程序并释放资源:
spark.stop()

更多信息

Apache Spark 提供了许多其他处理文本数据的功能,以下是一些常见的用法:

使用 regexp_extract() 函数

该函数用于从匹配指定正则表达式模式的字符串中提取子字符串。在这个例子中,正则表达式 \bq\w* 匹配所有以字母 q 开头的单词,regexp_extract 函数从文本数据中提取这些单词。生成的 DataFrame 将包含一个新列 q_words,其中包含提取的单词:

from pyspark.sql.functions import regexp_extract

df_q_words = (vectorized_data
    .withColumn("q_words", regexp_extract("text", "\\bq\\w*", 0)))
df_q_words.show()

使用 rlike() 函数

该函数用于测试字符串是否匹配指定的正则表达式模式。在这个例子中,rlike() 函数用于测试文本数据是否包含单词 "quick"。生成的 DataFrame 将包含一个新列 contains_quick,其中包含一个布尔值,指示文本数据是否包含单词 "quick":

df_quick_word = (vectorized_data
    .withColumn("contains_quick", expr("text rlike 'quick'")))
df_quick_word.show()

自定义停用词

StopWordsRemover() 函数中的内置停用词列表可能不适用于所有类型的文本数据。我们可能需要根据具体需求自定义停用词列表。

custom_stopwords =  ["/><br", "-", "/>I","/>The"]

stopwords_remover = StopWordsRemover(inputCol="words",
    outputCol="filtered_words",
    stopWords=custom_stopwords)

df_stop_words_removed = stopwords_remover.transform(df_with_words)
df_stop_words_removed.show()

可以使用 setStopWords() 方法在实例化后修改停用词列表:

custom_stopwords = ["br", "get", "im","ive"]
stopwords_remover = StopWordsRemover(inputCol="words", outputCol="filtered_words")
stopwords_remover.setStopWords(custom_stopwords)
df_stop_words_removed = stopwords_remover.transform(df_with_words)
df_stop_words_removed.show()

参考资料

使用 Apache Spark 写入数据

在本教程中,我们将逐步介绍如何利用 Spark 的强大功能以多种格式写入数据。本教程将为你提供使用 Apache Spark 的分布式计算能力写入数据所需的基本知识和实际技能。

操作步骤:

  1. 导入库:导入所需的库并创建一个 SparkSession 对象:
from pyspark.sql import SparkSession

spark = (SparkSession.builder
    .appName("write-data")
    .master("spark://spark-master:7077")
    .config("spark.executor.memory", "512m")
    .getOrCreate())

spark.sparkContext.setLogLevel("ERROR")
  1. 读取 CSV 文件
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, DateType

df = (spark.read.format("csv")
    .option("header", "true")
    .option("nullValue", "null")
    .option("dateFormat", "LLLL d, y")
    .load("../data/netflix_titles.csv"))
  1. 写入 CSV 数据:一旦数据在 Spark DataFrame 中,我们可以使用 DataFrame 对象的 write 方法将其写入 CSV 文件。需要指定 CSV 文件的路径和要使用的分隔符:
(df.write.format("csv")
    .option("header", "true")
    .mode("overwrite")
    .option("delimiter", ",")
    .save("../data/data_lake/netflix_csv_data"))

在上述代码中,我们指定了 headerdelimiter 选项,因为我们希望将头部和逗号分隔符写入 CSV 文件。

注意mode 参数控制在数据或表存在时发生的情况,有四种模式:

  • overwrite:用新数据替换旧数据,但会删除索引和约束
  • append:将新行添加到旧数据中,不更改或删除旧数据
  • ignore:如果数据或表存在,则跳过写入,避免重复
  • errorerrorifexists:如果数据或表存在,则写入失败,防止覆盖或追加
  1. 将 DataFrame 写入 JSON 格式
(df.write.format("json")
    .mode("overwrite")
    .save("../data/data_lake/netflix_json_data"))
  1. 将 DataFrame 写入 Parquet 格式
(df.write.format("parquet")
    .mode("overwrite")
    .save("../data/data_lake/netflix_parquet_data"))
  1. 停止 Spark 会话:最后,我们需要停止 Spark 会话以释放 Spark 使用的资源:
spark.stop()

其他功能

Apache Spark 提供了许多其他写入数据的功能,以下是一些常见用法:

写入压缩数据

我们也可以将数据写入压缩格式,例如 GZIP 或 BZIP2。要以压缩格式写入数据,需要在 DataFrame 对象的 save 方法中指定压缩编解码器:

(df.write.format("csv")
    .mode("overwrite")
    .option("header", "true")
    .option("delimiter", ",")
    .option("codec", "org.apache.hadoop.io.compress.GzipCodec")
    .save("../data/data_lake/netflix_csv_data.gz"))

在上述代码中,我们指定了压缩编解码器为 org.apache.hadoop.io.compress.GzipCodec 以 GZIP 压缩格式写入 CSV 数据。同样,我们可以使用 org.apache.hadoop.io.compress.BZip2Codec 以 BZIP2 压缩格式写入 CSV 数据。

指定分区数

在 Apache Spark 中,分区是一种将数据拆分成多个部分的方式,这样每个部分可以由不同的节点并行处理。分区对于优化 Spark 应用程序的性能非常重要,因为它影响数据混洗的数量、节点间的负载均衡和容错级别。我们也可以在写入数据时指定要使用的分区数。分区数决定了写入数据时创建的文件数。可以在 DataFrame 对象的 repartition 方法中指定分区数:

(df.repartition(4)
    .write.format("csv")
    .mode("overwrite")
    .option("header", "true")
    .option("delimiter", ",")
    .save("../data/data_lake/netflix_csv_data_4_part"))

在上述代码中,我们指定分区数为 4,以在写入数据时创建四个文件。

使用 coalesce() 减少分区数

如果你有一个高分区数的大 DataFrame 对象,这可能会减慢写入过程。你可以在将数据写入文件之前使用 coalesce() 方法减少分区数。以下是如何使用 coalesce 方法的示例:

(df.coalesce(1)
    .write.format("csv")
    .mode("overwrite")
    .option("header", "true")
    .option("delimiter", ",")
    .save("../data/data_lake/netflix_csv_data_whole"))

使用 partitionBy() 基于列写入分区

使用 DataFrameWriter 类的 partitionBy() 属性,可以在写入 CSV 数据时基于列对数据进行分区。如果你想基于特定列对输出的 CSV 数据进行分区,可以使用 DataFrameWriter 类的 partitionBy() 属性。以下是如何使用 partitionBy() 属性的示例:

# 按 'release_year' 列分区 CSV 数据
(df.write.format('csv')
    .option('header', 'true')
    .option('delimiter', ',')
    .mode('overwrite')
    .partitionBy('release_year')
    .save("../data/data_lake/netflix_csv_data_partitioned"))

参考资料