那天晚上我下班前脑子一抽,说要给测试环境跑个全量报表,结果就被CSV教育了一晚上…
你们有没有遇到过那种,data_2024_01_01.csv、data_2024_01_02.csv 一路排到 data_2024_12_31.csv 的? 运维同事一脸无辜:”也不大啊,就几十G而已。“ 我电脑风扇直接起飞,VSCode 卡到像远程桌面套远程桌面。
我当时代码还挺老实的,大概这样:
import pandas as pddf_list = []for day in range(1, 32): file_name = f"data_2024_01_{day:02d}.csv" tmp = pd.read_csv(file_name) df_list.append(tmp)df = pd.concat(df_list, ignore_index=True)print(df.shape)
结果嘛,大家也能想象,内存直接爆掉,笔记本开始“呼——”地狂转,我那一瞬间甚至怀疑人生: “我不就想算个UV吗,至于搞成离线大数据项目吗?”
后来同事路过,看了眼说:“哥,你这年头还玩CSV呢?用 Parquet 不香吗?” 我当时心里还有点不服:“不就一个文件格式嘛,至于吹成这样?” 然后我真换了一下…就,再见了兄弟CSV,你好我亲爱的Parquet。
先说个最直观的感受:同样那批数据,CSV差不多 10G,转成Parquet之后不到 2G。 我当时还有点不信,以为是我少写了几列,来回 check schema 半天。 结果发现不是数据少,是它列式存储+压缩太狠了。
大概就这么一段小脚本,跑完我心情都变好了:
import pandas as pdcsv_path = "data_2024_01_full.csv"parquet_path = "data_2024_01_full.parquet"# 读一次痛苦,换来以后一直舒服df = pd.read_csv(csv_path)# 注意:engine=“pyarrow” 要装 pyarrow,不要问怎么知道的…df.to_parquet(parquet_path, engine="pyarrow", compression="snappy")
压完以后我做了个小实验,同一台破电脑上:
read_csv 首次加载:十几秒起步,CPU 飙满read_parquet:眨眼功夫就出来了,CPU 也是动一下就结束那种
那一刻我有种“哦,原来以前一直在用错姿势”的羞耻感…
更骚的是,Parquet 是列式的嘛,你要几列,它只给你几列,不像CSV那种:“兄弟,先把整行读进来咱再说”。
以前用CSV算个简单的统计,比如只要 user_id 和 event_time,你也得乖乖把几十列全读进来:
df = pd.read_csv("data.csv") # 啥都得读
换 Parquet 之后,我的代码变成这样:
import pandas as pddf = pd.read_parquet("data.parquet", columns=["user_id", "event_time"], # 只要这两列,其他列一个字节都不看 engine="pyarrow")
这个 columns 参数一加,你会发现: 以前那个读数据要喝口水等一等的脚本,现在“咔咔”一下就跑完了。
最惊喜的是,我那台8G内存的旧本子,终于不用天天被我骂了——问题根本不在它,是我当初选了CSV这个“前女友”。
说完快和轻一点,再说个“安全”的事。 安全不是那种加密啥的安全哈,是“数据别给我乱来”的那种安全感。
CSV 最大的问题之一,就是啥类型都能写进去——你说它是 int,它说自己是 string,你俩吵去吧。 有一次业务给了我一个CSV,理论上 amount 那列都是数字,我直接:
df = pd.read_csv("order.csv")print(df["amount"].mean())
结果平均值直接给我算出一坨NaN。 一看数据,中间混进来几个 "--"、"未知" 这种鬼玩意儿。 我真是…想顺着网线去敲业务脑袋。
换 Parquet 之后呢,文件本身是带 schema 的,像这种感觉:
from pyarrow import schema, int64, stringorder_schema = schema([ ("order_id", string()), ("user_id", string()), ("amount", int64()), ("status", string()),])
然后你往里塞数据,如果 amount 列来了个 "未知", pyarrow 会当场给你翻脸:兄弟,你这类型对不上啊。
虽然一开始看着很烦,但你会发现, ——数据在写入阶段就帮你把脏东西挡在门外了, 比那种跑到线上才发现“诶怎么这个字段是字符串”的崩溃好太多了。
还有个特别爽的点,我当时是在一个埋点系统里用 Parquet 的。 埋点大家知道吧,事件一堆字段,什么 page, button, os_version,加着加着就几十列上百列了。
我们那会儿经常会遇到这种需求: 产品突然说:“能不能看一下上个月安卓用户,在订单页点了某个按钮的点击分布?”
如果用CSV,大概操作是这样的:
df[df['os'] == 'android' & (df['page'] == 'order') & ...]
换成 Parquet + 一点点 predicate pushdown(别怕这个词),操作就简单多了:
import pyarrow.dataset as dsdataset = ds.dataset("events_parquet_dir", format="parquet")android_order = dataset.to_table( filter=( (ds.field("os") == "android") & (ds.field("page") == "order") ), columns=["user_id", "button_id", "event_time"])
这里有个小细节特别关键: filter 这个条件,会尽量在“扫磁盘之前”就生效,也就是所谓的“往数据源那边推条件”。 翻译成人话: 以前是“先把整仓库里的箱子都扛出来,再数里面有几个苹果”; 现在是“进库前跟管理员说:我只要苹果箱子”。
这就是为什么同样是筛选数据,Parquet 会比 CSV 快得多的一个重要原因。
再说个很接地气的场景:你本地调试。
以前我为了在本地复现线上Bug,只能让运维帮我切一份“缩小版CSV”下来, 什么“抽样 1%”啊、“只要最近7天”啊,搞完还得验证这玩意儿是不是有代表性。
现在我直接怼线上Hive/对象存储拉 Parquet 到本地, 十几G压完也就几G甚至更小,然后我在本地随手整两句:
import pandas as pddf = pd.read_parquet("events_7d.parquet", engine="pyarrow")sample = ( df.query("os == 'android' and page == 'order'") .sample(n=5000, random_state=42))sample.to_parquet("debug_sample.parquet", index=False)
以后谁再让我用 CSV 当调试样本,我会非常真诚地劝他:“哥,要不你试试 Parquet,真不是我安利,这是从血泪里总结出来的。”
当然 Parquet 也不是没有坑,简单提两句,免得你们以为这是万能银弹:
- 你得装
pyarrow 或 fastparquet,有时编译环境会比较折腾 - 文本编辑器打不开“看一眼”的那种爽感没了(毕竟是二进制)
- 和一些古早系统联动的时候,别人只认 CSV,不认你这新玩意儿
但只要你的场景是:数据量不小 + Python 处理为主 + 经常做分析/统计, 那我真的是,双手合十拜托你试一次,人生苦短,别再和 CSV 硬刚大数据了。
我现在新项目的习惯基本就是:
那天我清理老项目的时候,看到一堆 data_xxx.csv, 手一抖,写了个小脚本全给它们洗白:
import pandas as pdfrom pathlib import Pathdata_dir = Path("legacy_csv")for csv_file in data_dir.glob("*.csv"): df = pd.read_csv(csv_file) parquet_file = csv_file.with_suffix(".parquet") df.to_parquet(parquet_file, engine="pyarrow", compression="snappy") print(f"migrate {csv_file.name} -> {parquet_file.name}")
命令敲完,看着终端里一行行 migrate xxx.csv -> xxx.parquet, 那种感觉就像给老旧系统做完一次“换心手术”, 心里只有一个声音:
——行了兄弟,CSV 就让它安静地待在历史里吧, 后面的性能、稳定、安全,就交给 Parquet 了。
我这边先去泡杯咖啡,你要是哪天真从坑里爬出来,记得回来跟我说一句: “东哥,你说的那个 Parquet,真还行。”