从 Pandas 到 Polars 四十六:使用Polars读取和写入S3数据

在本文中,我们将看到如何使用Polars从S3中的CSV或Parquet文件读取和写入数据。同时,我们还将了解如何在下载前对S3上的文件进行过滤,以减少跨网络传输的数据量。

写入文件到S3

我们将创建一个包含3列的简单DataFrame。我们将使用s3fs库将其写入S3中的CSV和Parquet文件。s3fs库允许您以类似于在本地文件系统上工作的语法来读取和写入S3中的文件。

bucket_name = "my_bucket"
csv_key = "test_write.csv"
parquet_key = "test_write.parquet"
fs = s3fs.S3FileSystem()  
df = pl.DataFrame(
    {
        "foo": [1, 2, 3, 4, 5],
        "bar": [6, 7, 8, 9, 10],
        "ham": ["a", "b", "c", "d", "e"],
    }
)
with fs.open(f"{bucket_name}/{csv_key}", mode="wb") as f:
    df.write_csv(f)
with fs.open(f"{bucket_name}/{parquet_key}", mode="wb") as f:
    df.write_parquet(f)

如果你可以选择的话,我推荐使用Parquet格式,因为它具有更小的文件大小,可以保留数据类型(dtypes),并且使后续读取更快。

从S3读取文件

我们可以使用Polars的pl.read_csv函数从S3中读取文件。

df_csv = pl.read_csv(f"s3://{bucket}/{csv_key}")
df_parquet = pl.read_parquet(f"s3://{bucket}/{parquet_key}")

Polars内部使用ffspec将远程文件读取到内存缓冲区中,然后将缓冲区中的数据读入DataFrame。这是一种快速的方法,但它确实意味着整个文件都被读入内存。对于小文件来说这没问题,但对于大文件来说可能会很慢并且占用大量内存。

然而,当我们只想读取行的一个子集时,读取整个文件是浪费的。对于Parquet文件,我们可以在S3上扫描文件,并且只读取我们需要的行。

在S3上使用查询优化扫描文件

对于Parquet文件,我们可以在S3上扫描文件并构建一个延迟查询。Polars的查询优化器会应用以下优化:

  1. 谓词下推(predicate pushdown),这意味着任何用于过滤行的条件都在S3上应用
  2. 投影下推(projection pushdown),这意味着如果只需要列的一个子集,那么只有这些列才会从S3中读取。

我们可以使用pl.scan_parquet来执行这些操作。这也可能需要传递一些特定于云存储提供商的选项。首先,Polars会尝试从环境变量中获取这些选项,但我们可以使用storage_options参数来覆盖它们。

import polars as pl

source = "s3://bucket/*.parquet"

storage_options = {
    "aws_access_key_id": "<secret>",
    "aws_secret_access_key": "<secret>",
    "aws_region": "eu-west-1",
}
df = (
    # Scan the file on S3
    pl.scan_parquet(source, storage_options=storage_options)
    # Apply a filter condition
    .filter(pl.col("id") > 100)
    # Select only the columns we need
    .select("id","value")
    # Collect the data
    .collect()
)

使用scan_parquet,Polars会在底层使用Rust的object_store库对Parquet文件进行异步读取。

对CSV文件应用过滤器

目前(2023年10月),Polars不支持在S3上扫描CSV文件。在这种情况下,我们可以在返回文件之前使用boto3库在S3上应用过滤器条件。

在boto3中,关键的方法是select_object_content。这允许我们在下载文件之前对S3上的文件应用SQL过滤器。此外,它还需要我们传递一些关于文件序列化方式(是CSV还是Parquet文件,是否压缩等)以及下载数据应该如何序列化的更多信息。

在这个例子中,我们过滤S3上的CSV文件,只返回“ham”列等于“a”的行。下面我将展示完整的代码,然后逐一解释每个部分。

import boto3import polars as pl

bucket_name = "my_bucket"
key = "test_write.csv"

# Create a boto3 client to interface with S3
s3 = boto3.client("s3")

# Define the SQL statement to filter the CSV 
datasql_expression = "SELECT * FROM s3object s WHERE ham = 'a'"

# Use SelectObjectContent to filter the CSV data before downloading 
its3_object = s3.select_object_content(
    Bucket=bucket_name,
    Key=key,
    ExpressionType="SQL",
    Expression=sql_expression,
    InputSerialization={"CSV": {"FileHeaderInfo": "USE"}, "CompressionType": "NONE"},
    OutputSerialization={"CSV": {}},
)

# Create a reusable StringIO 
objectoutput = io.StringIO()

# Iterate over the filtered CSV data and write it to the StringIO object
for event in s3_object["Payload"]:
    if "Records" in event:
        records = event["Records"]["Payload"].decode("utf-8")
        output.write(records)

# Rewind the StringIO object to the beginning
output.seek(0)
df = pl.read_csv(output)

我们首先创建SQL查询字符串:

sql_expression = "SELECT * FROM s3object s WHERE ham = 'a'"

这使用了S3 Select SQL语法,它与您在PostgreSQL等数据库中编写查询的方式有一些差异。

  1. s3object 关键字指的是S3上的对象。
  2. s 是该对象的别名。
  3. WHERE 子句是过滤条件。

请注意,您当然需要为筛选数据所消耗的计算资源付费。

然后,我们通过调用 s3.select_object_content 创建了 s3_object。这需要多个参数:

s3_object = s3.select_object_content(
    Bucket=bucket_name,
    Key=key,
    ExpressionType="SQL",
    Expression=sql_expression,
    InputSerialization={"CSV": {"FileHeaderInfo": "USE"}, "CompressionType": "NONE"},
    OutputSerialization={"CSV": {}},)

在InputSerialization参数中,我们传递了一个包含一些参数的字典,以告诉它如何读取我们的CSV文件。例如,我们告诉它文件有一个标题行,并且没有压缩。

在OutputSerialization参数中,我们传递了一个包含一些参数的字典,以告诉它如何序列化返回的数据——在这种情况下是CSV文件。不幸的是,在编写本文时,唯一的输出序列化选项是CSV和JSON,因此即使您输入的是Parquet文件,您也不能返回Parquet文件。

然后,我们使用Python内置的io库创建了一个StringIO对象,用于在从S3提取数据之前保存s3.select_object_content方法返回的数据。

# Iterate over the filtered CSV data and write it to the StringIO object
for event in s3_object["Payload"]:
    if "Records" in event:
        # Decode the bytes for each line to a string        
        records = event["Records"]["Payload"].decode("utf-8")

        # Write the string to the StringIO object        
        output.write(records)

# Rewind the StringIO object to the beginning
output.seek(0)

有了这个StringIO对象,我们就可以创建一个Polars DataFrame了。

df = pl.read_csv(output)

如果你只返回数据的一个子集,这种方法可能比从S3读取整个文件要快得多。

总结

在本文中,我们了解了如何使用Polars从S3读取和写入文件。这是一个快速发展的领域,所以我确信未来我会再次更新这篇文章(再次强调!),因为Polars将更多地原生支持这些功能。

在S3上管理数据有更复杂的方法。例如,你可以使用像Delta Lake这样的数据湖工具来管理你在S3上的数据。这些工具允许你以更结构化的方式管理数据,并执行如upsert和删除等操作。请参阅Matthew Powers的这篇文章,了解如何使用Delta Lake与Polars的入门介绍。

        往期热门文章:

从 Pandas 到 Polars 二十六:在Polars中,不要遍历列

从 Pandas 到 Polars 二十三:如果你的数据已经排序,Polars可以为你提供助力

从 Pandas 到 Polars 十八:数据科学 2025,对未来几年内数据科学领域发展的预测或展望

从 Pandas 到 Polars 十三:流式处理的关键参数

从 Pandas 到 Polars 十:“Polars 表达式“是什么?

从 Pandas 到 Polars 六:在 Polars 中流式处理大型数据集

从 Pandas 到 Polars 0:理解Polars嵌套列类型

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值