CSV解析实战:从RFC标准到生产级健壮读取
1. 为什么一个看似“过时”的文本格式,至今仍是数据科学 workflow 的隐形脊柱?
在数据科学项目里,你可能花三天调参优化一个 XGBoost 模型,花两天写 PySpark 作业处理十亿级日志,但真正卡住整个 pipeline 的,往往不是算法,而是一个 30 年前设计的、连压缩都不支持的纯文本文件——CSV。它没有 schema 定义,不记录数据类型,不保存时区信息,甚至对换行符和引号的处理都依赖“双方默契”。可现实是:90% 以上的 Kaggle 数据集以 CSV 分发;85% 的 BI 工具默认导出为 CSV;几乎所有数据库的COPY FROM和EXPORT TO命令都把 CSV 当作第一公民;就连你用 pandas 读取 Excel 文件时,底层也常先转成 CSV-like 的内存结构再解析。这不是技术惯性,而是经过二十年高强度实战验证后的理性选择:CSV 的不可替代性,恰恰源于它的极度简单与极致脆弱——简单到任何语言三行代码就能读,脆弱到一旦出错,错误信号会立刻、明确、无遮掩地暴露出来。它不帮你掩盖问题,它逼你直面数据质量本身。本文面向三类人:刚学 pandas 的新手(为什么pd.read_csv()有 47 个参数?)、正在调试 ETL 流水线的工程师(为什么生产环境凌晨三点报警说“unquoted fields”?)、以及负责制定企业数据规范的数据治理人员(为什么强制要求 UTF-8 BOM 是反模式?)。我们不讲定义,只拆解真实场景中每一个逗号、每一组引号、每一处换行背后的技术权衡与血泪教训。
2. CSV 格式设计逻辑与核心约束:从 RFC 4180 到工业级实践的鸿沟
2.1 RFC 4180 是什么?它为什么几乎没人真正遵守?
RFC 4180 是 IETF 在 2005 年发布的 CSV “标准”文档,共 8 条规则。但请注意:它本身不是强制协议,而是一份“最佳实践建议”。现实中,它被广泛引用,却更常被当作“免责说明书”——当两个系统对接失败时,一方会甩出 RFC 4180 第 7 条:“字段内换行必须用双引号包裹”,另一方则回敬:“我们按 Excel 实现,Excel 就不加引号”。这种撕扯的根源,在于 RFC 4180 试图用“理想语法”框定一个本就诞生于“实用主义混沌”的格式。我们逐条拆解其工业落地现状:
规则1:每行一条记录,字段用逗号分隔
理论上干净,但实际中:MySQL 的LOAD DATA INFILE允许自定义分隔符(\t,|),pandas 的sep参数支持正则(r',(?=(?:[^"]*"[^"]*")*[^"]*$)'用于跳过引号内逗号),而某些金融数据源甚至用~作为分隔符并声称“这是 CSV 变体”。关键认知:CSV 的“C”代表 Comma-Separated,但工业界早已默认它代表 Character-Separated —— 分隔符只是约定,不是契约。规则2:所有字段必须用双引号包围
这是 RFC 最常被违反的条款。Excel 导出默认仅对含逗号、换行、引号的字段加引号;pandas 默认quoting=csv.QUOTE_MINIMAL(最小化引号);而 PostgreSQL 的COPY命令则要求QUOTE '"'显式指定。实测对比:一个含 10 万行的销售数据,若强制全字段加引号,文件体积增加 12%,解析耗时上升 8%(因需额外字符串扫描)。所以工程师的选择不是“要不要标准”,而是“在吞吐、体积、兼容性之间,哪条路的代价最小”。规则3:字段内双引号需转义为两个双引号("")
这是唯一被几乎所有主流工具严格遵守的规则。为什么?因为它是解决“字段边界模糊”问题的数学最优解。试想:若用反斜杠\转义,那么路径C:\data\file.csv中的\d就会被误解析。而""是自解释的——只有连续两个引号才表示转义,单个引号永远是字段边界。pandas 的escapechar参数在此规则下完全失效,强行设置会导致解析崩溃。这个细节暴露出一个本质:CSV 不是编程语言,它没有“转义字符”的概念,只有“字段定界符”和“字段内引号转义符”两个原语。规则4:首行应为列名(header)
现实中,Kaggle 数据集 63% 有 header,但 IoT 设备上传的传感器原始日志 100% 无 header,银行对账单 CSV 则常在 header 前插入 3 行元信息(如“生成时间:2024-03-15”)。pandas 的skiprows=3和header=0组合能解决,但 Spark 的option("header", "true")遇到多行 header 会直接报错。这迫使数据工程师必须在 pipeline 前置环节做“header 归一化”——用 Python 脚本预处理,或用 AWK 提取有效行。规则5:最后一行可选换行符
看似无害,却是 CI/CD 中最隐蔽的坑。Git 会将 LF(Unix)和 CRLF(Windows)视为不同内容,导致同一份 CSV 在不同开发机上 commit hash 不同。更致命的是:某些旧版 Hadoop 集群的 TextInputFormat 会把末尾 CRLF 解析为额外空行,引发下游聚合计算偏差。解决方案不是争论该不该加,而是用 pre-commit hook 强制标准化:dos2unix *.csv或 Git 的.gitattributes设置*.csv text eol=lf。规则6:MIME 类型应为
text/csv
HTTP 传输中,Content-Type: text/csv能帮助浏览器正确触发下载,但 API 返回 JSON 时若嵌套 CSV 字符串(如{ "data": "a,b\n1,2" }),此 MIME 类型毫无意义。真正的工业实践是:传输层用 JSON 包裹,内容层用 CSV 编码,二者职责分离。规则7:字段内换行必须用双引号包裹
这是 RFC 的“安全阀”,但也是性能杀手。pandas 默认启用此规则,但当遇到未加引号的换行字段时,会抛出ParserError: Error tokenizing data。而 Spark 的multiline=true选项虽能处理,却需将整块数据加载进内存再切分,1GB 文件可能触发 OOM。高阶技巧:用awk '/^"/{f=1;next} /^"/{f=0;next} !f' file.csv预过滤掉跨行字段,再交给主解析器——用流式文本处理规避内存爆炸。规则8:编码应为 US-ASCII
2024 年还在用 ASCII?显然不现实。但 RFC 未规定 UTF-8,导致历史遗留系统(如 COBOL 主机导出)仍输出 ISO-8859-1。pandas 的encoding='utf-8'遇到乱码会静默替换为 ``,而encoding='utf-8-sig'自动跳过 BOM。血泪教训:永远在读取前用file -i filename.csv检查真实编码,而非依赖文件后缀。
提示:RFC 4180 的价值不在执行,而在提供一套“错误归因坐标系”。当你看到
pandas.errors.ParserError: Expected 5 fields in line 1234, saw 6,立刻知道问题出在第 1234 行的引号配对或换行处理上,而不是去怀疑数据源逻辑——这是它给工程师最实在的礼物。
2.2 工业级 CSV 的三大隐性契约:比 RFC 更重要的生存法则
脱离 RFC 空谈标准毫无意义。真实世界中,数据团队靠三条不成文契约维系协作:
“UTF-8 without BOM” 是默认编码
Windows 记事本保存 CSV 时默认添加 BOM(Byte Order Mark,EF BB BF),导致 Linux 服务器上的head -n1 file.csv显示id,name。pandas 读取时若未指定encoding='utf-8-sig',id列名会变成id,后续所有df['id']操作全部报错。解决方案:在数据接入网关层部署统一清洗脚本,用sed -i '1s/^\xEF\xBB\xBF//' *.csv批量移除 BOM。这不是妥协,是建立基础设施级的编码共识。“缺失值统一用空字符串或
NULL字符串”
Excel 导出的 CSV 中,空单元格生成空字段,,;而数据库导出常用NULL字符串。pandas 的na_values=['', 'NULL', 'null', 'N/A']可覆盖,但 Spark 需显式option("nullValue", "NULL")。更危险的是:某电商订单表中,discount_code字段为空时写入"",而coupon_used布尔字段却写入"false"——此时""和"false"都是有效值,不能一概na_values。经验:在数据字典中明确定义每个字段的“空值语义”,并在 ETL 脚本开头用df.replace({'': pd.NA})统一转换,避免下游逻辑歧义。“日期时间字段必须 ISO 8601 格式(YYYY-MM-DD HH:MM:SS)”
03/15/2024是美式还是欧式?15-03-2024是日式还是欧式?CSV 不存类型,只存字符串。pandas 的parse_dates=['order_time']能自动推断,但遇到2024/03/15和15-MAR-2024混存时,会随机失败。硬性规定:所有上游系统导出前,必须用strftime('%Y-%m-%d %H:%M:%S')格式化时间字段。宁可让业务方改一行代码,也不在数据平台加 200 行容错逻辑。
这三条契约,没有写在任何 RFC 里,却每天支撑着万亿级数据流转。它们的本质是:用最小的格式约束,换取最大的解析确定性。
3. 核心解析技术点深度拆解:从pd.read_csv()到零拷贝解析
3.1 pandas 的 47 个参数,哪些真正决定生死?
pd.read_csv()文档列出 47 个参数,但 90% 的日常使用只涉及 7 个。真正影响生产环境稳定性的,是以下 5 个“核按钮”:
chunksize:流式处理的命脉
读取 10GB CSV 时,chunksize=10000生成TextFileReader对象,每次next()返回 1 万行 DataFrame。但注意:chunksize不是内存控制开关!pandas 仍需将整块磁盘数据读入内存,再切片。实测:chunksize=10000时内存峰值达 12GB(文件 10GB),而chunksize=1000峰值仅 1.5GB。原理:pandas 内部用StringIO缓冲,chunksize 越小,缓冲区越小。但过小(如 100)会导致 I/O 次数激增,CPU 耗时翻倍。黄金值 =max(1000, int(file_size_bytes / (10 * 1024 * 1024)))(即每 chunk 约 10MB 内存占用)。dtype:类型预设的防爆机制
默认infer_dtype=True会逐行扫描推断类型,100 万行数据可能耗时 47 秒。更糟的是:第 1 行age=25推断为int64,第 50 万行age="N/A"就触发TypeError。正确姿势:用dtype={'age': 'Int64', 'price': 'float32', 'category': 'category'}。其中'Int64'(首字母大写)是 pandas 的可空整型,完美容纳NaN;'float32'比默认'float64'节省 50% 内存;'category'对低基数字符串(如国家代码)压缩率达 90%。na_values&keep_default_na:空值识别的精准手术刀
默认keep_default_na=True会将['', '#N/A', 'NULL', 'NaN']视为空值。但某医疗数据集中,test_result字段用"N/A"表示“未检测”,用"NULL"表示“检测失败”,二者语义完全不同。此时必须keep_default_na=False,再手动na_values=['#N/A', 'NaN']。避坑:永远用df.isna().sum()验证空值识别是否符合预期,而非依赖文档描述。low_memory=False:解析器的“全知模式”开关
默认True时,pandas 分块推断 dtype,可能导致同一列前 10 万行是int64,后 10 万行是object(因出现字符串),最终合并时报DtypeWarning。设为False强制一次性扫描全文件推断,内存多用 15%,但 dtype 100% 稳定。生产环境铁律:ETL 任务必须low_memory=False,交互分析可True换速度。engine='c'vs'python':底层解析引擎的生死抉择'c'引擎(默认)用 C 实现,速度快 3-5 倍,但不支持正则分隔符和复杂 quoting;'python'引擎用纯 Python,支持sep=r',(?!(?:"[^"]*"(?:[^"]*"[^"]*)*[^"]*$))'这种高级逗号分割(跳过引号内逗号),但慢且吃内存。决策树:若数据源来自 Excel/DB 导出(标准 CSV),用'c';若来自日志拼接/爬虫(非标 CSV),用'python'并接受 3 倍耗时。
注意:
compression='infer'在读取.csv.gz时自动解压,但若文件是.csv.bz2,必须显式compression='bz2',否则报OSError: Not a gzipped file。这不是 bug,是设计——pandas 不愿为小众压缩格式增加维护成本。
3.2 Spark 的 CSV 解析:分布式下的新挑战
Spark 2.0+ 的spark.read.csv()表面参数精简,实则暗藏玄机:
inferSchema=True的陷阱
Spark 采样前 100 行推断 schema,若第 1 行amount=100.5推断为DoubleType,第 101 行amount="ERROR"就导致AnalysisException: Cannot parse。生产方案:永远inferSchema=False,用schema=StructType([...])显式定义,并在columnNameOfCorruptRecord中捕获脏数据。multiLine=True的内存诅咒
启用后,Spark 必须将整个文件加载进 Driver 内存,再分发到 Executor。1GB 文件在 4GB Driver 上必 OOM。破局之道:用spark.sql.files.maxPartitionBytes=128m强制小分区,配合multiLine=False,再用 UDF 处理跨行字段——用计算换内存。quote和escape的组合技
某广告日志 CSV 中,creative_content字段含双引号和逗号,且用~作转义符(如~"hello, world~")。Spark 需option("quote", "\"").option("escape", "~")。但注意:escape只对quote字符生效,对分隔符无效。验证方法:df.select("creative_content").show(truncate=False)直接看原始字符串,别信count()。
3.3 零拷贝解析:当性能成为唯一信仰
当单机 pandas 读取 100GB CSV 耗时超 1 小时,你需要超越传统解析器:
Apache Arrow 的
csv.read_csv()
Arrow 用 Rust 实现,内存映射(mmap)直接操作磁盘页,避免 Python 层数据拷贝。实测:读取 50GB CSV,pandas 耗时 38 分钟,Arrow 仅 4.2 分钟,内存占用低 60%。关键配置:use_threads=True启用多线程,block_size=64*1024*1024(64MB 块大小)匹配 SSD 页大小。Polars 的
read_csv()
Polars 基于 Arrow,但增加查询优化器。pl.read_csv('data.csv').filter(pl.col('sales') > 1000).select(['id','name'])会编译为单次磁盘扫描,而非 pandas 的“全读-过滤-投影”三步。适用场景:对超大 CSV 做简单聚合(sum/count),Polars 比 pandas 快 8-12 倍。自研流式解析器(Python + ctypes)
极端场景:实时解析 Kafka 流中的 CSV 消息。用 C 写核心解析(状态机处理引号/转义),Python 用ctypes调用。一个 100 行 C 函数可处理每秒 50 万行 CSV,而 pandas 仅 8 万行。代价:开发成本高,但若你的业务每秒处理百万事件,这笔投资 3 天回本。
实操心得:不要迷信“最新技术”。Arrow 在 2024 年已成熟,但 Polars 的
write_csv()在中文路径下仍有 bug(v0.20.19)。我的选择是:分析用 Polars,生产 ETL 用 Arrow,实时流用自研 C 解析器——技术选型不是比赛,是精准匹配。
4. 实操全流程:从原始 CSV 到可信数据资产的 7 步炼金术
4.1 步骤1:原始文件诊断(5分钟定生死)
在写任何代码前,先用命令行做三件事:
# 1. 查编码(Linux/macOS) file -i sales_2024.csv # 输出:sales_2024.csv: text/plain; charset=utf-8 # 2. 查分隔符频率(找出最常出现的非字母数字字符) sed '10q' sales_2024.csv | tr -cd ',;\t|' | fold -w1 | sort | uniq -c | sort -nr # 输出: 1234 , (逗号占绝对主导) # 3. 查异常行(定位换行/引号问题) awk 'NR==FNR && /"/{if (gsub(/"/,"&")==1) print "奇数引号行:", NR}' sales_2024.csv # 输出:奇数引号行: 12345 → 立刻知道第 12345 行引号不配对为什么这步不可跳过?我曾接手一个“无法解析”的客户数据,file -i显示charset=iso-8859-1,但iconv -f iso-8859-1 -t utf-8 sales.csv > sales_utf8.csv后,pandas 仍报错。最终发现:文件是GBK编码,file工具误判。教训:file是初筛,hexdump -C sales.csv | head -20看十六进制才是终审。
4.2 步骤2:编码清洗与 BOM 移除
用 Python 脚本批量处理:
import chardet from pathlib import Path def detect_and_convert(file_path: Path): # 1. 用 chardet 检测(比 file 命令准) with open(file_path, 'rb') as f: raw = f.read(10000) # 读前 10KB encoding = chardet.detect(raw)['encoding'] or 'utf-8' # 2. 读取并转为 UTF-8 without BOM with open(file_path, 'r', encoding=encoding) as f: content = f.read() # 3. 移除 BOM(如果存在) if content.startswith('\ufeff'): content = content[1:] # 4. 写回(覆盖原文件) with open(file_path, 'w', encoding='utf-8') as f: f.write(content) print(f"{file_path} → {encoding} → utf-8") # 批量处理 for csv_file in Path("raw_data/").glob("*.csv"): detect_and_convert(csv_file)关键细节:chardet.detect()对小文件(<1KB)准确率低,故读 10KB;content[1:]移除 BOM 是安全的,因 UTF-8 BOM 仅在文件开头;open(..., 'w', encoding='utf-8')默认不写 BOM,无需utf-8-sig。
4.3 步骤3:Schema 推断与验证
用 Pandas 做快速探查:
import pandas as pd # 用前 10 万行推断(平衡速度与准确性) sample_df = pd.read_csv("sales_2024.csv", nrows=100000, low_memory=False, encoding='utf-8') # 生成数据字典草案 schema_report = [] for col in sample_df.columns: dtype = str(sample_df[col].dtype) null_pct = sample_df[col].isna().mean() * 100 unique_pct = sample_df[col].nunique() / len(sample_df) * 100 # 智能类型建议 if dtype == 'object': if unique_pct < 0.1 and null_pct < 5: suggested = 'category' elif sample_df[col].str.match(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$').all(): suggested = 'datetime64[ns]' else: suggested = 'string' else: suggested = dtype schema_report.append({ 'column': col, 'sample_dtype': dtype, 'null_pct': round(null_pct, 2), 'unique_pct': round(unique_pct, 2), 'suggested_type': suggested }) pd.DataFrame(schema_report).to_csv("schema_draft.csv", index=False)输出schema_draft.csv后,人工审核:
order_date列suggested_type='string'?但业务要求是 datetime → 手动改为datetime64[ns]product_id列null_pct=0.0,但业务说“部分订单无产品” → 检查数据源,发现product_id=""被误读为NaN,需加na_values=['']
4.4 步骤4:健壮解析(生产级代码模板)
import pandas as pd import numpy as np def robust_read_csv( file_path: str, dtype: dict = None, na_values: list = None, parse_dates: list = None, chunksize: int = None ) -> pd.DataFrame: """ 生产环境 CSV 解析模板 """ # 默认参数(覆盖 95% 场景) default_params = { 'encoding': 'utf-8', 'sep': ',', 'quoting': 0, # csv.QUOTE_MINIMAL 'escapechar': None, 'low_memory': False, 'on_bad_lines': 'skip', # 跳过损坏行,而非报错 'na_filter': True, 'keep_default_na': True, } # 合并用户参数 params = {**default_params, **{ 'dtype': dtype or {}, 'na_values': na_values or ['NULL', 'null', 'N/A', ''], 'parse_dates': parse_dates or [], 'chunksize': chunksize, }} try: if chunksize: # 流式处理 chunks = [] for chunk in pd.read_csv(file_path, **params): # 每 chunk 做轻量清洗 chunk = chunk.dropna(how='all') # 删除全空行 chunks.append(chunk) return pd.concat(chunks, ignore_index=True) else: return pd.read_csv(file_path, **params) except pd.errors.EmptyDataError: print(f"警告:{file_path} 为空文件") return pd.DataFrame() except pd.errors.ParserError as e: print(f"解析错误:{file_path} - {e}") # 回退到 python 引擎 params['engine'] = 'python' return pd.read_csv(file_path, **params) # 使用示例 df = robust_read_csv( "sales_2024.csv", dtype={'order_id': 'string', 'amount': 'float32'}, parse_dates=['order_time'], chunksize=50000 )为什么on_bad_lines='skip'是生产必需?
某次线上事故:一个供应商上传的 CSV 中,第 88888 行末尾多了一个逗号(123,abc,456,),导致pandas报ParserError,整个 ETL 任务中断。启用skip后,该行被丢弃,任务继续,同时日志记录Skipped bad line 88888,运维可人工修复。数据质量不是“全有或全无”,而是“可控损失”。
4.5 步骤5:数据质量校验(DQ Rules)
用 Great Expectations 框架定义规则:
import great_expectations as ge # 创建上下文 context = ge.data_context.DataContext() # 加载数据 df_ge = ge.from_pandas(df) # 定义期望 df_ge.expect_column_values_to_not_be_null("order_id") df_ge.expect_column_values_to_be_between("amount", min_value=0, max_value=1000000) df_ge.expect_column_values_to_match_strftime_format("order_time", "%Y-%m-%d %H:%M:%S") df_ge.expect_column_values_to_be_in_set("status", value_set=["pending", "shipped", "delivered"]) # 生成报告 results = df_ge.validate() print(f"数据质量通过率:{results['statistics']['success_percent']:.1f}%")结果解读:若success_percent < 99.5%,立即告警;若status字段出现"cancelled"(不在 value_set 中),记录违规样本供业务确认——是数据错误,还是规则需更新?
4.6 步骤6:存储优化(Parquet 替代 CSV)
CSV 解析完,绝不直接存 CSV!用 Parquet:
# 写入 Parquet(分区 + 压缩) df.to_parquet( "cleaned_data/sales_2024.parquet", partition_cols=['order_year', 'order_month'], # 按年月分区 compression='snappy', # 比 gzip 快 3 倍,体积只大 15% use_dictionary=True, # 对字符串列启用字典编码 engine='pyarrow' ) # 读取时自动过滤分区 filtered_df = pd.read_parquet( "cleaned_data/sales_2024.parquet", filters=[('order_year', '==', 2024), ('order_month', '==', 3)] )性能对比(10GB 销售数据):
| 操作 | CSV (gzip) | Parquet (snappy) | 提升 |
|---|---|---|---|
| 全表读取 | 218 秒 | 32 秒 | 6.8x |
| 查询 2024 年 3 月数据 | 187 秒 | 1.2 秒 | 156x |
| 存储体积 | 2.1 GB | 1.3 GB | 节省 38% |
4.7 步骤7:元数据管理(让 CSV 有“身份证”)
创建data_catalog.yaml:
datasets: - name: "sales_2024" description: "2024年全量销售订单数据,来源:ERP系统每日导出" source: type: "csv" path: "raw_data/sales_2024.csv" last_modified: "2024-03-15T02:15:00Z" encoding: "utf-8" delimiter: "," quote_char: '"' schema: - name: "order_id" type: "string" nullable: false description: "订单唯一标识,业务主键" - name: "amount" type: "float32" nullable: true description: "订单金额,单位:元" dq_rules: - rule: "not_null" column: "order_id" - rule: "range_check" column: "amount" min: 0.01 max: 999999.99价值:当新人问“amount字段最大值是多少?”,不再需要df['amount'].max(),直接查 YAML;当审计要求“证明数据来源”,source.path和last_modified就是证据。
5. 常见问题与排查技巧实录:那些让你凌晨三点爬起来的 Bug
5.1 问题速查表:症状、根因、解法
| 症状 | 根因 | 解法 | 验证命令 |
|---|---|---|---|
ParserError: Expected 4 fields in line 123, saw 5 | 第 123 行有未转义的逗号(如"John,Doe",123,45.6) | 用sed -n '123p' file.csv查看,用csvkit工具修复 | in2csv file.csv > /dev/null 2>&1 |
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 | 文件是 Latin-1 编码,含é字符 | iconv -f latin1 -t utf-8 file.csv > file_utf8.csv | file -i file.csv |
DtypeWarning: Columns (1) have mixed types | 同一列前 10 万行是 int,后 10 万行是 string | dtype={'col1': 'string'}强制字符串 | df['col1'].apply(type).value_counts() |
MemoryError读取 5GB CSV | pandas 默认加载全文件到内存 | 改用chunksize=50000流式处理 | ps aux | grep python查内存 |
df.shape[0]比文件行数少 | on_bad_lines='skip'跳过了损坏行 | 查日志Skipped bad line XXX | wc -l file.csv对比 |
5.2 独家避坑技巧:教科书不会写的实战智慧
技巧1:用
csvkit做“CSV 体检医生”csvkit是命令行 CSV 工具集,比 pandas 更轻量:# 查看前 5 行(自动处理引号/换行) csvlook sales.csv \| head -20 # 统计每列空值率 csvstat sales.csv \| grep -A 10 "Missing" # 转换编码(比 iconv 更智能) in2csv --encoding latin1 sales.csv > sales_utf8.csv为什么不用 pandas?
csvkit是纯 Python + C 扩展,启动快,适合 CI/CD 中做前置检查,无需启动完整 Python 环境。技巧2:
pandas的memory_map=True是伪命题
文档说memory_map=True启用内存映射,但实测对 CSV 无效(仅对二进制格式如 HDF5 有效)。真相:pandas 的 CSV 解析器必须将整行读入内存才能解析,mmap 无意义。正确方案是chunksize或 Arrow。技巧3:Excel 导出的 CSV,永远用
dialect='excel'
Excel 用 `"\r\n
