解密 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。




本文参考自:


相关文章
|
5月前
|
数据可视化 Linux iOS开发
Python脚本转EXE文件实战指南:从原理到操作全解析
本教程详解如何将Python脚本打包为EXE文件,涵盖PyInstaller、auto-py-to-exe和cx_Freeze三种工具,包含实战案例与常见问题解决方案,助你轻松发布独立运行的Python程序。
1428 2
|
4月前
|
监控 机器人 编译器
如何将python代码打包成exe文件---PyInstaller打包之神
PyInstaller可将Python程序打包为独立可执行文件,无需用户安装Python环境。它自动分析代码依赖,整合解释器、库及资源,支持一键生成exe,方便分发。使用pip安装后,通过简单命令即可完成打包,适合各类项目部署。
905 68
|
6月前
|
缓存 数据可视化 Linux
Python文件/目录比较实战:排除特定类型的实用技巧
本文通过四个实战案例,详解如何使用Python比较目录差异并灵活排除特定文件,涵盖基础比较、大文件处理、跨平台适配与可视化报告生成,助力开发者高效完成目录同步与数据校验任务。
226 0
|
7月前
|
编译器 Python
如何利用Python批量重命名PDF文件
本文介绍了如何使用Python提取PDF内容并用于文件重命名。通过安装Python环境、PyCharm编译器及Jupyter Notebook,结合tabula库实现PDF数据读取与处理,并提供代码示例与参考文献。
|
7月前
|
编译器 Python
如何利用Python批量重命名文件
本文介绍了如何使用Python和PyCharm对文件进行批量重命名,包括文件名前后互换、按特定字符调整顺序等实用技巧,并提供了完整代码示例。同时推荐了第三方工具Bulk Rename Utility,便于无需编程实现高效重命名。适用于需要处理大量文件命名的场景,提升工作效率。
|
8月前
|
编解码 Prometheus Java
当Python同时操作1000个文件时,为什么你的CPU只用了10%?
本文介绍如何构建一个高效的文件处理系统,解决单线程效率低、多线程易崩溃的矛盾。通过异步队列与多线程池结合,实现任务调度优化,提升I/O密集型操作的性能。
195 4
|
7月前
|
安全 Linux 网络安全
Python极速搭建局域网文件共享服务器:一行命令实现HTTPS安全传输
本文介绍如何利用Python的http.server模块,通过一行命令快速搭建支持HTTPS的安全文件下载服务器,无需第三方工具,3分钟部署,保障局域网文件共享的隐私与安全。
1741 0
|
7月前
|
数据管理 开发工具 索引
在Python中借助Everything工具实现高效文件搜索的方法
使用上述方法,你就能在Python中利用Everything的强大搜索能力实现快速的文件搜索,这对于需要在大量文件中进行快速查找的场景尤其有用。此外,利用Python脚本可以灵活地将这一功能集成到更复杂的应用程序中,增强了自动化处理和数据管理的能力。
624 0
|
7月前
|
数据采集 监控 算法
Python文件与目录比较全攻略:从基础操作到性能优化
文件比较的核心在于数据指纹校验,通过逐字节比对生成唯一标识,确保内容一致性。从标准库的os与filecmp到高性能第三方库如pydiffx,再到分布式与量子加密技术的未来趋势,文件比较广泛应用于数据备份、代码审查与系统监控等领域,是保障数据完整性的关键技术手段。
163 0
|
8月前
|
数据采集 存储 API
Python爬虫结合API接口批量获取PDF文件
Python爬虫结合API接口批量获取PDF文件

推荐镜像

更多