
1. 先说这个痛点30GB的CSV把人逼疯前阵子接手一个数据分析任务数据团队给过来的是一份30GB的CSV当时人就有点麻。这个文件有多大呢放在机械盘里双击打开Excel基本等于自杀用 pandas 的read_csv直接读哪怕是16G内存的机器也会读到把内存吃满然后报MemoryError就连想看一眼前几行head -n 5 data.csv也要等好几秒因为30GB的纯文本即使只读头部也要先让文件系统吐数据。这其实是很多做数据处理的人都会撞上的场景不是没有分析工具而是数据本身在一个“不适合直接干活”的格式里。CSV的好处是人能看懂坏处是机器要处理的时候处处掣肘。更要命的是一个30GB的CSV往往是从数据库导出、日志聚合、或者设备采集之后直接落地的里面大量重复文本、冗余符号真正有价值的信息可能只有很小一部分。我当时第一反应是能不能先把它换一种存储格式于是有了这篇东西的标题——30GB的CSV换个格式只要3GB。整个过程做完文件从30GB降到3GB压缩了近10倍。这个经验不挑行业只要你在跟“大CSV”打交道尤其是经常下载开源数据集、分析用户行为日志、处理设备采样数据、做机器学习特征工程的人基本都能直接用上。先说结论这个案例里我最终把CSV转成了Parquet格式。Parquet对这类大数据量、列很多、重复值高的表格数据压缩效果非常明显后续再读数据做分析速度也比直接啃CSV快得多。下面我把整个思考过程、技术选型、实操命令和踩坑记录都铺开讲希望能帮你少走几步弯路。2. 为什么30GB的CSV能瘦身成3GB2.1 CSV瘦不下来的三个硬伤要理解为什么换个格式能少9成体积得先搞清楚CSV为什么这么大。CSV的本质就是“逗号分隔的纯文本”每一行是一条记录每个字段用分隔符隔开每条记录结尾有换行符。听起来很直白但它的体积膨胀主要来自三块。第一块是重复的文本值。举个例子一张用户行为表里省份字段可能只有“广东、北京、上海”等几十个取值但在CSV里每一条记录都会原原本本地写一遍“广东省”。假设一亿行数据里这个字段平均出现3次汉字光这一个字段就要占掉一大块原始文本空间。第二块是类型信息和分隔符。CSV本身区分不了字符串和数字读者只能靠猜为了让机器能读每个值之间还要加上逗号、引号、换行这类结构性字符这些字符对业务没有信息量却每一个字节都占用磁盘。第三块是压缩率太差。虽然可以把CSV放进zip或者gz里压缩但那是“整块压缩”一旦你用pandas或数据库去读还是要把整个文件解压成原始文本流内存和IO压力一点没少。2.2 Parquet和ORC为什么能把空间省回来Parquet是属于“列式存储压缩编码”的格式。列式存储的意思是同一个字段的所有值在文件里是连续存放的而不是像CSV那样一行一行地摊开。假设刚才那张表有一列是“是否付费”取值只有0和1那么在Parquet文件里这一列可以放进一个连续的内存块里再用位图或字典编码去表示一个布尔值压缩之后平均不到1位而CSV里每个值至少要占1个字节还要加逗号和换行。更关键的是Parquet支持字典编码和压缩算法叠加。遇到重复度高的列它会先建一个字典把“广东省、北京市、上海市”这些值只存一份后面的记录用整数下标引用遇到时间戳、数值这类可以预测序列模式的列还会用Delta编码把相邻差值存下来最后还能对整个数据块做zstd或者snappy压缩。这几层编码叠下来30GB的CSV变成3GB的Parquet就是这个数据本身的重复度决定的并不夸张。2.3 10倍压缩是普遍情况还是运气好有人会问是不是所有CSV都能压缩10倍答案是看数据。如果你手里的CSV主要是随机数、UUID、加密串、高熵文本那压缩率可能只有2-3倍但如果是业务表、日志表、用户行为数据、传感器时序数据里面有大量分类值、时间戳、数字型指标那么10倍或者更高是常见现象。我这边的数据就是一个多列的用户行为日志含中文字段和重复设备型号实际压缩比到了10.2左右。压缩率高还有一个隐藏好处文件小了磁盘和传输成本都降了后续做分析时列式格式还能跳过无关列只读取需要的列所以查询速度往往也能快一个数量级。这就是“换个格式”最值钱的地方。3. 工具选型与转换方案设计3.1 这批数据到底要不要上Spark看到30GB这个量级有人第一反应是“上Spark”。我的建议是没必要。Spark也不是不行只是对一个单机就能处理的文件引入一套分布式计算集群光是调度和资源分配的时间都够转换好几遍了。30GB的CSV只要不是内存小于4GB或者需要做特别复杂的全量JOIN单机工具完全吃得下。真实项目里数据量到几百GB甚至TB级别再去考虑Spark、Doris这类分布式方案也不迟。退一步讲即使以后数据量涨到100GB、200GBDuckDB和PyArrow这类工具依然能在单机上处理只是耗时会更长它们的底层都是向量化执行配合列式格式能撑住的量级比很多人想象中大得多。3.2 DuckDB、Pandas、PyArrow三套方案的取舍转换大CSV主流方案有三条路。一是用DuckDB。这是一个嵌入式数据库单文件、免安装SQL直接跑CSV和Parquet。优点是语法简单可以一条SQL完成读取和写入内部是流式执行内存占用比较可控缺点是需要单独装一下它不过一条命令就能装好。我的首选方案就是用DuckDB。二是用Pandas。优点是人人都熟缺点是大文件直接read_csv容易爆内存。所以Pandas路线必须配合chunksize或者PyArrow的底层流式读取。适合那种团队里只有Python、不想再引入SQL工具的场景。三是用PyArrow。它是Arrow内存格式和Parquet格式的官方实现读取速度比Pandas的纯C解析还要快配合pyarrow.csv.open_csv可以流式读取然后用ParquetWriter写入。这是最底层、最可控的方案灵活性最高。三条路线我都试过结论是如果是临时一次性转换优先DuckDB如果要写进自动化管道或者转换前后有清洗逻辑优先PyArrow如果只是小批量的边缘文件Pandas也能凑合。3.3 我选的这套流程长什么样我实际采用的流程是先用DuckDB对CSV做个快速体检编码、列数、行数、类型然后直接用DuckDB把整份CSV转成Parquet压缩算法选zstd转换完成后再跑几个SQL对比源文件和目标文件的行数、唯一键数量、关键字段聚合值最后写一个小的Python脚本抽样做MD5级校验。整套流程下来手动操作不超过10分钟主要时间都花在让DuckDB扫描并写出文件上。这个流程的好处是每个步骤都能验证不是黑盒“转完了就完事”。数据转换最怕的就是悄悄丢行、丢列、类型错乱所以校验环节绝对不能省。4. 从30GB CSV到3GB Parquet的完整实操4.1 动手前先给CSV做个“体检”第一步不是急着写转换命令而是先搞清楚手里的CSV长什么样。在Linux环境下我会依次跑这几个命令# 文件大小和行数 ls -lh data.csv wc -l data.csv # 前几行内容重点看分隔符、表头、编码 head -n 5 data.csv # 用file命令看编码 file -bi data.csv如果是Windows环境可以用小工具或者直接拿Python读一小段一样的效果。这一步要确认三件事第一表头是否存在列名有没有重复第二分隔符到底是逗号、制表符还是分号第三文件编码是UTF-8、UTF-8带BOM还是GBK。尤其是编码后面很多坑都是从这儿埋下的。4.2 优先推荐DuckDB一行命令直接转环境没问题之后直接用DuckDB转换。以Windows为例假设数据在D:/data/raw.csv输出到D:/data/raw.parquet命令行执行duckdb -c COPY (SELECT * FROM read_csv_auto(D:/data/raw.csv)) TO D:/data/raw.parquet (FORMAT PARQUET, COMPRESSION ZSTD);执行完你会看到DuckDB边扫描边写文件最后生成一个3GB左右的Parquet文件。整个过程最需要注意的就是read_csv_auto的类型推断。如果CSV里有整数列但某些行出现了空值DuckDB可能会把整列推断成VARCHAR转换出来的Parquet在存储空间和查询效率上会打折扣。所以我一般会在SQL里手动指定关键列的类型或者先用下面这个SQL看推断结果DESCRIBE SELECT * FROM read_csv_auto(D:/data/raw.csv);如果推断结果不对就改成显式指定COPY ( SELECT id::BIGINT AS id, user_id::VARCHAR AS user_id, event_time::TIMESTAMP AS event_time, province::VARCHAR AS province FROM read_csv_auto(D:/data/raw.csv) ) TO D:/data/raw.parquet (FORMAT PARQUET, COMPRESSION ZSTD);给各位一个参考我转换30GB的CSV机器配置是8核16G内存DuckDB版本1.1左右整个过程大约跑了9分钟峰值内存用了4GB多一点比预期稳很多。4.3 备选方案Pandas PyArrow分块转换如果你的环境不方便装DuckDB或者转换前后想在Python里做清洗那就用分块方案。这里不建议直接用pd.read_csv一次性读而是在读取端用迭代器写出端用pyarrow.parquet.ParquetWriter。示例代码如下import pyarrow as pa import pyarrow.csv as pacsv import pyarrow.parquet as pq reader pacsv.open_csv( data.csv, read_optionspacsv.ReadOptions(block_size1 26), # 64MB一个block parse_optionspacsv.ParseOptions(delimiter,) ) writer None for batch in reader: table pa.Table.from_batches([batch]) if writer is None: writer pq.ParquetWriter( data.parquet, table.schema, compressionzstd, use_dictionaryTrue ) writer.write_table(table) if writer: writer.close()这段代码的关键点是读取端是按批处理的不会把整个文件读进内存写出端把每一批转成Arrow Table再交给ParquetWriter。这样即使文件再大几倍内存也基本稳定。如果你确实还想用Pandas也可以把上面的batch转成pandas.DataFrame处理后再转回Arrow但注意中间转换也要分批别一口气全给内存扛。4.4 转换后的数据验证与MD5校验思路文件转换完不能直接当成功。很多同行栽过跟头转换过程没报错但读出来的数据和源CSV对不上要么少了几列要么有几千行因为解析失败被跳过。所以验证环节我建议至少做两层。第一层行数和唯一键校验。-- 源CSV SELECT count(*), count(DISTINCT id) FROM read_csv_auto(D:/data/raw.csv); -- Parquet SELECT count(*), count(DISTINCT id) FROM D:/data/raw.parquet;两边行数一致、唯一键数量一致基本可以确定数据没有整行丢失。如果担心列级差异再挑几个数值列算sum和min/max对比。数值求和如果一致说明这些列的值没被悄悄改掉。第二层内容级MD5校验。需要说明的是CSV和Parquet的二进制内容完全不同所以不能直接对两个文件跑md5sum然后比较哈希值。这样比永远不一样没有意义。正确的做法是“按内容计算哈希”先对源CSV的每一行按统一规则拼成一个字符串然后逐行计算MD5最后再对所有这些MD5结果再取一次哈希对Parquet也做同样的处理两个结果如果一致说明内容完全一致。下面是一个示例脚本对指定主键列做逐行MD5聚合import hashlib def rows_md5(iterable_rows, key_cols): line_hasher hashlib.md5() for row in iterable_rows: raw b\x01.join(str(row[c]).encode(utf-8, errorsreplace) for c in key_cols) line_hasher.update(raw b\x02) return line_hasher.hexdigest()不过说实话30GB的CSV逐行算MD5在纯Python里会很慢跑一两个小时也正常。实际生产里我更建议如果只是想验证文件在拷贝过程中没有损坏直接用md5sum data.csv记录原文件哈希如果想验证转换后内容一致用“行数唯一键关键列聚合值”的组合判断已经足够。如果必须做全量级校验用Spark或者DuckDB的xxhash64这类原生函数去算比Python逐行快得多。4.5 新格式日常读取和查询转换完的Parquet日常读取非常快。DuckDB可以直接像查表一样读SELECT province, count(*) FROM D:/data/raw.parquet GROUP BY province ORDER BY count(*) DESC LIMIT 10;Pandas也可以直接读import pandas as pd df pd.read_parquet(data.parquet, enginepyarrow)30GB的CSV转成Parquet后查询单列聚合的速度提升是肉眼可见的。因为列式存储只要读对应的列块不需要把整行文本拖进内存。比如我只统计省份分布CSV格式可能要读完整30GB再解析Parquet可能只要读几百MB的列数据差别就是这么明显。5. 换格式过程里最容易踩的坑5.1 编码问题GBK、UTF-8、BOM到底谁说了算CSV文件编码是最常见的坑没有之一。本地用Excel另存出来的CSV默认很可能是GBK或者带BOM的UTF-8用PyCharm写脚本生成的CSV如果没有显式指定编码默认跟随项目编码从Linux服务器上下载的CSV又大概率是纯UTF-8。一旦DuckDB或Pandas用UTF-8去读GBK文件中文直接乱码甚至整个文件解析失败。我的习惯是转换之前先看一眼file输出或者在读的时候直接指定编码。DuckDB读CSV时可以通过read_csv_auto(data.csv, encodinggbk)来指定编码Pandas则用pd.read_csv(data.csv, encodinggbk)。另外如果CSV是给Excel用户准备的建议保存为utf-8-sig也就是带BOM的UTF-8这样Excel双击打开不会显示乱码但如果是给程序解析的建议直接用普通UTF-8BOM反而可能干扰首列列名解析。5.2 分隔符和引号错位字段里有逗号和换行CSV文件里最隐蔽的坑是字段内容本身包含逗号、换行、引号。比如用户留言字段里可能有逗号和换行如果导出时没有加引号保护读出来就是错位的。DuckDB的read_csv_auto一般会自动识别引号包裹的字段但遇到“字段里有引号引号又被重复一次”的情况还是容易解析错位。遇到这种情况我建议转换前先抽查几行数据确认字段是否正确。也可以用read_csv_auto的quote、escape、header等参数手动指定规则。更稳妥的做法是把这类“脏CSV”先用更严谨的解析器清洗一遍再转Parquet。数据质量不解决格式换得再勤也没用。5.3 类型推断翻车日期、大整数、空值自动类型推断在大多数时候挺好用但有些列会翻车。最典型的是日期时间列2024-01-01 00:00:00在不同读取器里可能被推断成字符串、日期、时间戳三种不同结果。还有大整数列超过int32范围时容易被推断成float64精度直接丢失。空值多的列也容易被误判成字符串。所以我在正式转换前一定跑一次DESCRIBE把关键列的类型确认清楚。宁可在转换SQL里多写几行CAST也不能让类型推断悄悄把精度吃掉。特别是ID列一旦变成浮点型后面做关联、去重都容易出问题。5.4 元数据陷阱转换后“看起来”没问题但缺数据最后一个坑比较隐性DuckDB在读取CSV时如果遇到某些格式错误有的配置下会返回一个包含错误信息的“错误列”而不是直接报错。这时候行数和总列数看起来都对但部分数据被塞进了一个错误字段里。这种问题靠行数校验发现不了必须抽查几行原始数据对比。我的建议是转换后随机抽几行直接对比CSV源文件和Parquet输出。可以用ORDER BY random() LIMIT 5来抽也可以拿几个明显有特征的ID去查。数据转换这种事校验做得越细后面填坑的成本越低。6. 这套思路还能顺手解决哪些CSV难题6.1 大CSV拆分、DBeaver导入、Kettle输出的替代方案如果你手里的CSV虽然没有30GB那么大但也到了几个GBDBeaver这类GUI的CSV导入功能往往会卡死因为界面工具大多数是一次性把文件读进内存。DBeaver导入CSV之前可以先用脚本把CSV拆成小文件再导入更省事的办法是先转成Parquet再用其他能读Parquet的分析工具去查体验会好很多。KettlePDI里常见的一个场景是两个表合并后输出CSV输出文件经常几个GB起步。Kettle本身有Parquet输出步骤可以把输出目标从CSV改成Parquet既减小体积后面接入分析工具也更快。这是很多人没注意到但非常实用的替代方案。至于大CSV拆分最简单的方式是用Linux的split命令按行数拆如果想按逻辑维度拆分比如按省份拆成多个文件直接用DuckDBCOPY (SELECT * FROM read_csv_auto(data.csv) WHERE province广东) TO guangdong.parquet (FORMAT PARQUET, COMPRESSION ZSTD);6.2 JMeter参数化大文件的分块读取JMeter做压测时参数化文件如果是一个超大的CSVCSV Data Set Config把所有数据一次性读进内存很容易把JMeter本身压垮。我见过有人硬跑一个2GB的参数化文件最后JMeter内存爆掉压测结果失真。我的建议是把一个大的参数化CSV先按线程数拆成多个小文件比如10个线程就拆成10个分片每个线程组用CSV Data Set Config指向自己的分片共享模式选择“Current thread”或者“Current thread group”。这样JMeter的内存占用会低很多而且每个线程拿到的数据互不重叠符合“每个线程分块取值”的需求。6.3 PyCharm生成CSV打不开、Matlab读取慢、示波器波形打不开这几个问题其实是同一类CSV文件本身没坏但被“错误打开方式”坑了。PyCharm里用df.to_csv()生成的CSV默认编码是UTF-8没有带BOMExcel在中文Windows下会用本地ANSI编码去解析所以中文全乱甚至看起来“不是表格文件”。解决办法很简单df.to_csv(out.csv, encodingutf-8-sig)或者在PyCharm里生成CSV时显式指定编码。Matlab读取CSV做FFT如果是几个GB的大文件用readmatrix直接读基本会卡到怀疑人生。我建议先用Python把CSV转成Parquet或MAT文件Matlab只需要按需读取列如果数据量实在太大先降采样再分析更合理。示波器导出的波形CSV是一个更典型的场景动不动几个GB记录频率很高普通文本编辑器根本打不开。这种文件不一定需要转Parquet但至少应该用脚本分段读取、做降采样或只截取异常窗口而不是指望一个软件把它们全显示在屏幕上。示波器报错提示“在电脑上可以看吗”本质上不是文件坏了是打开方式不对。6.4 航迹数据、坐标转换这类专业CSV怎么处理再往专业场景看航空领域的航迹CSV、坐标转换用的CSV/TXT文件往往记录量很大字段含有经纬度、高度、速度、时间戳。这类数据很适合转成Parquet或GeoParquet既能压缩体积又能配合空间索引做范围查询。很多坐标转换小工具在主界面上都有导入CSV或TXT的入口但超大文件传进去同样容易卡死先拆分成小体积文件再导入是更稳妥的思路。比如做坐标转换时如果源数据是一个几千万点的CSV直接全量读进内存做投影转换会非常吃力先转Parquet然后按区域分块处理内存压力会小很多。DuckDB本身也支持空间扩展可以直接读Parquet做筛选再配合计算不同坐标系的转换逻辑整套流程比硬啃一个超大CSV轻松得多。7. 最后分享一点实操体会做数据这行我越来越觉得“格式管理”比很多人想象的重要。30GB的CSV压缩到3GB的Parquet省的不只是磁盘还有每次读取、传输、清洗和建模的时间成本。一个项目里数据格式选得好不好后面所有环节的效率都会被放大或拖累。我现在凡是拿到大的表格数据第一步不会再是read_csv一把梭而是先问自己接下来要做什么这个数据的最佳存储形态是什么。如果答案只是“先看看”那我会用DuckDB直接连CSV预览不急着转换如果确定后续要反复分析、跑模型我会毫不犹豫地转成Parquet并在转换前把类型、编码、唯一键都检查一遍。如果你手里也有一个快把磁盘撑爆的CSV可以从DuckDB那一行COPY命令开始试。第一次跑通之后你就再也不想回到“拿着CSV硬扛”的日子了。