解密 parquet 文件,以及如何用 Python 去处理它(二)

简介: 解密 parquet 文件,以及如何用 Python 去处理它(二)

接上篇解密 parquet 文件,以及如何用 Python 去处理它(一):https://developer.aliyun.com/article/1617387?spm=a2c6h.13148508.setting.21.72964f0eREsVbB



记录的边界要如何确定




了解完 Parquet 文件的结构之后,再来回顾之前遗留的问题,那就是记录的边界该怎么确定呢?


3758d1be82ba7f061679749a7e0c1f01.png


以里面的 contacts.name 为例,前两个属于第一条 record,后两个属于第二条,那么问题来了,Parquet 是如何区分的呢?答案是通过 repetition level。


数组的每个元素都有一个 repetition level,如果只考虑每个数组的第一个元素,那么它们的 repetition level 就是从 0 开始依次递增的。至于其它元素的 repetition level 则直接和数组的个数保持一致,可能用文字描述的话不太好理解,我们画一张图就清晰了。


2756c4038774a5438a74e5db16a6b997.png


因为有 5 条记录,所以数组的个数就是 5。每个数组的第一个元素的 repetition level 从 0 开始依次递增,所以 a1、b1、c1、d1、e1 的 repetition level 分别对应 0、1、2、3、4,至于其它元素的 repetition level 则直接和数组的个数保持一致,因为这里有 5 条记录,所以就都是 5。


说完了 repetition level,再来看看 definition level,后者主要是表示 null 值。因为 Parquet 文件不会显式地存储 null,所以需要用 definition level 来判断某个值是否为 null。

message TestDefinitionLevel {
  optional group a {
    optional group b {
      optional string c;
    }
  }
}

该结构如果转换成列式,那么它只有一列 a.b.c,由于所有 field 都是 optional 的,都可能是 null。如果 c 有定义,那么 a 和 b 作为它的上层,也一定是有定义的。但当 c 为 null 时,可能是因为它的某一级 parent 为 null 才导致 c 是 null 的。


这时为了记录嵌套结构的状况,我们就需要保存最先出现 null 的那一层的深度了。这里一共嵌套三层,所以 definition level 最大是 3。

ee3f1e5371fba2dc9ca15bad332913b4.png

这里 definition level 不会大于3,等于 3 的时候,表示所有层级都有定义;等于 0、1、2 的时候,指明了 null 出现的层级.

需要说明的是,对于 required 字段是不需要 definition level 的,只有那些 optional 字段才需要,举个例子:

message TestDefinitionLevel {
  optional group a {
    required group b {
      optional string c;
    }
  }
}

如果将 b 改成 required,那么 definition level 最大就是 2,因为 b 不需要 definition level。


c82077d386321858a96e8dec19e09828.png


以上就是 repetition level 和 definition level,还是不难理解的。但可能有人觉得每个值都保存这俩玩意,是不是太浪费空间了。所以 Parquet 又做了优化,对非数组类型的字段不保存 repetition level,对 required 字段不保存 definition level。


并且实际存储这两个字段时,还会通过 bit-packing + RLE 来进行压缩。



Python 操作 Parquet 文件




Parquet 文件的原理我们稍微了解一下就好,重点是如何操作它。需要说明的是,不光是嵌套结构,二维表结构也一样可以用 Parquet 文件存储。

import pandas as pd
df = pd.DataFrame({
    "name": ["satori", "koishi", "marisa", "cirno"],
    "age": [17, 16, 18, 40],
    "gender": ["female"] * 4
})
df.to_parquet(
    "girl.parquet.gz",
    # 需要 pip install pyarrow
    engine="pyarrow",
    # 压缩方式,可选择:'snappy', 'gzip', 'brotli', None
    # 默认是 'snappy'
    compression="gzip",
    # 是否把 DataFrame 自带的索引写进去,默认写入
    # 但要注意的是,索引会以 range 对象的形式写入到元数据中
    # 因此不会占用太多空间,并且速度还更快
    index=False
)

写完之后,在我本地就会生成一个 gz 文件,那么要如何读取它呢?

import pandas as pd
df = pd.read_parquet("girl.parquet.gz",
                     engine="pyarrow")
print(df)
"""
     name  age  gender
0  satori   17  female
1  koishi   16  female
2  marisa   18  female
3   cirno   40  female
"""

结果没有问题,如果你只想要部分字段,那么通过 columns 参数指定想要的字段即可。


根据列进行分区


然后我们还可以根据指定的列进行分区,举个例子:

import pandas as pd
df = pd.DataFrame({
    "name": ["satori", "koishi", "marisa", "cirno"] * 2,
    "age": [17, 16, 18, 40] * 2,
    "gender": ["female"] * 8
})
df.to_parquet(
    "girl.parquet.gz",
    engine="pyarrow",
    compression="gzip",
    # 按照 "name" 字段分区
    partition_cols=["name"]
)

执行之前先将刚才生成的文件删掉,执行之后会发现生成了一个目录:


067d5720dbe62c1122e17b789a70858a.png


此时的 girl.parquet.gz 就不再是文件了,而是一个目录,然后目录里面会出现 4 个子目录。因为我们是按照 name 分区的,而 name 有 4 个不同的值。


所以从这里可以看出,只有那些专门用于分类、元素重复率非常高的字段,才适合做分区字段,最典型的就是日期。


然后我们来读取它。

import pandas as pd
df = pd.read_parquet("girl.parquet.gz",
                     engine="pyarrow")
print(df)
"""
   age  gender    name
0   40  female   cirno
1   40  female   cirno
2   16  female  koishi
3   16  female  koishi
4   18  female  marisa
5   18  female  marisa
6   17  female  satori
7   17  female  satori
"""
# 默认全部读出来了
# 但也可以选择读取部分记录,比如只读取 name='satori' 的记录
df = pd.read_parquet("girl.parquet.gz/name=satori",
                     engine="pyarrow")
print(df)
"""
   age  gender
0   17  female
1   17  female
"""

在读取 name='satori' 的记录时,我们看到居然没有 name 字段,原因也很简单,我们是按照 name 字段分区的。那么每个分区的 name 字段的值一定是相同的,所以读取出来之后手动添加即可。

df = pd.read_parquet("girl.parquet.gz/name=satori",
                     engine="pyarrow")
df["name"] = "satori"
print(df)
"""
   age  gender    name
0   17  female  satori
1   17  female  satori
"""

当然啦,如果你觉得一个分区字段不够,那么也可以指定多个分区字段。

import pandas as pd
import numpy as np
df = pd.DataFrame({
    "p1": ["a"] * 4 + ["b"] * 4 + ["c"] * 4,
    "p2": ["X", "X", "Y", "Y"] * 3,
    "p3": np.random.randint(1, 100, size=(12,))
})
df.to_parquet(
    "test.parquet.gz",
    engine="pyarrow",
    compression="gzip",
    # 按照 "p1" 和 "p2" 字段分区
    partition_cols=["p1", "p2"]
)

先按照 p1 分区,在 p1 内部再按照 p2 分区,执行之后目录结构如下:


e61534adc5b8e84c4e1ee13ec6ab0363.png


非常清晰,所以 pandas 已经封装的非常好了,你根本不需要理解 Parquet 文件的原理,直接用就完事了。


然后我们来读取:

import pandas as pd
df = pd.read_parquet("test.parquet.gz",
                     engine="pyarrow")
print(df)
"""
    p3 p1 p2
0   86  a  X
1   72  a  X
2   64  a  Y
3   53  a  Y
4   90  b  X
5   60  b  X
6   58  b  Y
7   30  b  Y
8   17  c  X
9   74  c  X
10  81  c  Y
11  24  c  Y
"""
df = pd.read_parquet("test.parquet.gz/p1=b",
                     engine="pyarrow")
print(df)
"""
   p3 p2
0  90  X
1  60  X
2  58  Y
3  30  Y
"""
df = pd.read_parquet("test.parquet.gz/p1=b/p2=X",
                     engine="pyarrow")
print(df)
"""
   p3
0  90
1  60
"""

结果也没有问题,然后 pandas 的一个强大之处就在于,它不仅可以读取本地的 Parquet 文件,还可以读 s3 上面的,甚至是 http、ftp 也支持。



小结




以上我们就介绍了 Parquet 文件的原理以及如何用 Python 去操作它,这里需要再补充的一点是 pyarrow,它是 Apache Arrow 的 Python 实现。Apache Arrow 是一个高效的列式数据格式,用于加快数据的处理速度,并且是跨语言的。

而 pandas 在 2.0 的时候,可以采用 pyarrow 作为后端。在此之前,Pandas 的数据在内存中基本都是以 Numpy 数组的形式存在,每一列数据都以向量的形式存储,内部用 BlockManager 去管理这些向量。

但 Numpy 本身并不是为 DataFrame 而设计,对于一些数据类型的支持并不是很好。最尴尬就是缺失值,结果就搞出来 NaN,NaT,pd.NA 等等,让人头皮发麻,甚至一些公司在某些场景下都禁用 pandas。

但 Arrow 的引入可以完美地解决问题,不需要 Pandas 内部对每一种类型都设计一套实现,更契合的内存数据结构省了很多麻烦。不仅速度更快,也更省内存。

import pandas as pd
import pyarrow
# Pyarrow 是 Apache Arrow 的 Python 实现
# 通过 pyarrow.array() 即可生成 Arrow 格式的数据
arr1 = pyarrow.array([1, 2, 3])
print(arr1.__class__)
"""
<class 'pyarrow.lib.Int64Array'>
"""
arr2 = pyarrow.array([1, 2, 3], type=pyarrow.int32())
print(arr2.__class__)
"""
<class 'pyarrow.lib.Int32Array'>
"""
# pandas 在存储数据时默认使用 Numpy Array
s1 = pd.Series([1, 2, 3], dtype="int64")
print(s1)
"""
0    1
1    2
2    3
dtype: int64
"""
# 指定 Pyarrow,表示使用 Arrow 格式来存储数据
s2 = pd.Series([1, 2, 3], dtype="int64[pyarrow]")
print(s2)
"""
0   1
1   2
2   3
dtype: int64[pyarrow]
"""
# 在 2.0 的时候,通过设置 dtype_backend
# 可以让 pandas 默认选择 pyarrow 作为后端
# pd.options.mode.dtype_backend = 'pyarrow'

pandas 还有一个让人诟病的地方,就是它的字符串处理效率不高,因为 Numpy 主要是用于数值计算的,字符串并不擅长。所以在 pandas 里面存储字符串的时候,本质上存储的还是泛型指针,那这样就和 Python 动态处理字符串没什么两样了。

而使用 Arrow 之后,字符串就不再需要通过指针来查找了,它们是连续的一段内存,这样在内存占用和处理速度上都有非常大的提升。

所以我们可以得出如下结论:

1)Apache Arrow 是一种跨语言的高效的列式数据格式,用于加速数据处理;

2)pyarrow 库是 Apache Arrow 的 Python 实现,调用 pyarrow 的 array 函数,即可创建 Arrow 格式的数据。至于它的一些 API 以后有机会再说,不过有 pandas,这些 API 你完全可以不用了解,在使用 pandas 的时候,让它在背后为你默默工作就行;

3)pandas 在创建 Series 或 DataFrame 的时候,在类型后面加上 [pyarrow],即可将数据格式从 Numpy 的数组换成 Arrow,然后在处理数据的时候会以 pyarrow 作为后端;


但 pyarrow 除了可以创建 Arrow 格式的数据之外,它还提供了一系列读取文件的方法,比如读取 ORC 文件、Parquet 文件、CSV 文件、本地文件、HDFS 文件等等。但这些方法你同样不需要了解,因为我们有 pandas,只需要在使用 pandas 读取文件的时候,将参数 engine 指定为 pyarrow 即可。


274d812388d562cec93a7fbddafb7dac.png


非常方便,并且速度快了不止一点半点。

注:将 engine 指定为 pyarrow,表示用 pyarrow 去读取 CSV 文件,但数据格式仍然使用 Numpy Array。而在 pandas2.0 的时候新增一个参数 dtype_backend,也要指定为 pyarrow,表示数据读取进来之后使用 Arrow 格式。

以上就是本文的内容,我们聊了 Parquet 文件,然后聊到了如何用 Python 去处理它,最后又介绍了 pyarrow。




本文参考自:


相关文章
|
3天前
|
安全 Python
Python 高级编程:高效读取 txt 文件的技巧与实践
在 Python 中,读取 txt 文件是常见操作。本文介绍了使用 `with` 语句自动管理文件资源、逐行读取文件、读取特定字节范围内容、处理编码问题以及使用缓冲读取提高性能等高级方法,确保代码高效且安全。通过这些技巧,你可以更灵活地处理文件内容,并避免资源泄漏等问题。原文链接:https://www.wodianping.com/app/2024-10/44183.html
33 18
|
4天前
|
数据处理 Python
Python 高级技巧:深入解析读取 Excel 文件的多种方法
在数据分析中,从 Excel 文件读取数据是常见需求。本文介绍了使用 Python 的三个库:`pandas`、`openpyxl` 和 `xlrd` 来高效处理 Excel 文件的方法。`pandas` 提供了简洁的接口,而 `openpyxl` 和 `xlrd` 则针对不同版本的 Excel 文件格式提供了详细的数据读取和处理功能。此外,还介绍了如何处理复杂格式(如合并单元格)和进行性能优化(如分块读取)。通过这些技巧,可以轻松应对各种 Excel 数据处理任务。
31 16
|
9天前
|
存储 Python
一文让你搞懂 Python 的 pyc 文件
一文让你搞懂 Python 的 pyc 文件
29 15
|
1天前
|
存储 索引 Python
一文让你搞懂 Python 的 pyc 文件
一文让你搞懂 Python 的 pyc 文件
29 3
|
10天前
|
人工智能 IDE 开发工具
Python实行任意文件的加密—解密
Python实行任意文件的加密—解密
21 2
|
11天前
|
人工智能 IDE 开发工具
Python实行任意文件的加密—解密
Python实行任意文件的加密—解密
23 1
|
1天前
|
存储 JSON 数据格式
解密 parquet 文件,以及如何用 Python 去处理它(一)
解密 parquet 文件,以及如何用 Python 去处理它
12 0
|
10天前
|
UED Python
Python requests库下载文件时展示进度条的实现方法
以上就是使用Python `requests`库下载文件时展示进度条的一种实现方法,它不仅简洁易懂,而且在实际应用中非常实用。
25 0
|
11天前
|
数据处理 Python
python遍历文件夹所有文件按什么排序
python遍历文件夹所有文件按什么排序
10 0
|
2月前
|
SQL JSON 关系型数据库
n种方式教你用python读写excel等数据文件
n种方式教你用python读写excel等数据文件