Python技术迷

clevercsv,一个有趣的 Python 库!

CSV 一乱,pandas 第一反应就是甩锅。CleverCSV 这库有点意思

供应商丢过来的 CSV,最烦的不是大。

是你一眼看过去像 CSV,程序一读,全歪了。

逗号不像逗号,分号不像分号,有的字段还夹着反斜杠转义。运营同事拿 Excel 打开说“没问题啊”,我一般不太信 Excel。Excel 太会装了,它帮你猜完格式,顺手还把一些值改掉。

比如这种文件:

sku;name;price;tags
A001;Apple iPhone 15;5999;"phone\;ios"
A002;ThinkPad X1;8999;"notebook\;business"
A003;Logitech Mouse;129;"mouse\;office"

你直接上 pandas:

import pandas as pd

df = pd.read_csv("supplier.csv")
print(df.head())

大概率就开始不对劲。

不是列数不对,就是某一列被吃掉。这个时候我第一反应不是调业务逻辑,而是先把 CSV 的“方言”查清楚。

CSV 这个东西名字叫 comma separated values,但实际现场根本不只逗号。分隔符可能是 ;、\t、|,引号可能没有,转义符可能乱用。Python 标准库里有 csv.Sniffer,但遇到脏文件,它猜错也不稀奇。

CleverCSV 干的就是这件事:更靠谱地猜 CSV 的 dialect。官方文档也把它定位成 Python csv 包的 drop-in replacement,重点就是改进脏 CSV 的 dialect detection,还带命令行工具。

安装没什么花活:

pip install clevercsv

要用命令行那套工具,可以装完整版本:

pip install "clevercsv[full]"

我平时不会一上来就 read_dataframe,先探一下格式。因为线上导入脚本出问题,第一条日志要能告诉我:这个文件到底被当成什么格式读了。

from pathlib import Path
import clevercsv

defsniff_csv(path: str, encoding: str = "utf-8"):
    file_path = Path(path)

    dialect = clevercsv.detect_dialect(
        file_path,
        num_chars=20_000,
        encoding=encoding,
        method="auto",
    )

if dialect isNone:
raise RuntimeError(f"没识别出 CSV 格式: {file_path}")

    print(
"[csv-dialect]",
f"delimiter={dialect.delimiter!r}",
f"quotechar={dialect.quotechar!r}",
f"escapechar={dialect.escapechar!r}",
    )

return dialect

dialect = sniff_csv("supplier.csv")

这里打印出来的东西,比你在代码里猜半天强。

可能是这样:

[csv-dialect] delimiter=';' quotechar='"' escapechar='\\'

看到这个结果,后面处理就稳多了。

import clevercsv

dialect = clevercsv.detect_dialect("supplier.csv", encoding="utf-8")

with open("supplier.csv", "r", newline="", encoding="utf-8") as fp:
    reader = clevercsv.DictReader(fp, dialect=dialect)

for row_no, row in enumerate(reader, start=2):
        sku = (row.get("sku") or"").strip()
        price_text = (row.get("price") or"").strip()

ifnot sku:
            print(f"[bad-row] line={row_no}, reason=empty sku, row={row}")
continue

try:
            price = int(price_text)
except ValueError:
            print(f"[bad-row] line={row_no}, reason=bad price, value={price_text!r}")
continue

# 这里才进入真正的业务处理
        print(sku, price)

这段代码故意没写得很“框架化”。

导入脚本最怕什么?怕一条脏数据把整批卡死,还不给你留现场。我的习惯是:格式识别打一条日志,行号保住,坏行先捞出来。后面要不要中断,看业务要求,不要上来就吞异常。

CleverCSV 还有一个我觉得挺顺手的用法,直接读成表:

import clevercsv

rows = clevercsv.read_table("supplier.csv", encoding="utf-8")

for row in rows[:5]:
    print(row)

要进 pandas,也可以:

import clevercsv

df = clevercsv.read_dataframe("supplier.csv", encoding="utf-8")
print(df.head())

官方 quick start 里也给了 read_table、read_dataframe、Sniffer().sniff() 这几种入口;其中 read_dataframe 就是给 pandas 用户少踩一步坑用的。

不过我不建议所有 CSV 都无脑换 CleverCSV。

文件特别大时,格式检测这一步别拿全文件硬扫。文档里 Detector.detect 也提到,如果时间允许,用完整内容效果更好;反过来讲,生产脚本里就得控制 sample 大小。它不是魔法,还是要在准确率和耗时之间取舍。

我一般这么写:

import clevercsv
from pathlib import Path

defopen_dirty_csv(path: str, encoding: str = "utf-8"):
    p = Path(path)

    dialect = clevercsv.detect_dialect(
        p,
        num_chars=50_000,
        encoding=encoding,
        method="auto",
    )

if dialect isNone:
# 兜底不要太聪明,宁可明确报错,也别静悄悄读歪
raise ValueError(f"CSV dialect detect failed: {p.name}")

    fp = p.open("r", newline="", encoding=encoding)
return fp, clevercsv.DictReader(fp, dialect=dialect)

fp, reader = open_dirty_csv("supplier.csv")

try:
for row in reader:
pass
finally:
    fp.close()

这个库有意思的地方不在于 API 多,而在于它专门咬住了一个很小但很烦的问题:CSV 到底该怎么切。

CleverCSV 背后用的是行长度模式和单元格数据类型一致性这类思路来判断格式,作者在文档里也提到,他们在大量真实 CSV 文件上做过研究,dialect detection 准确率做到过 97%,在非标准脏 CSV 上比 Python 标准库有明显提升。

这类库平时不起眼。

真到供应商文件、历史导出文件、运营手改文件一起塞进系统的时候,它就能少救你几次火。

我的建议很简单:标准、干净、自己系统导出来的 CSV,继续 pandas。来源复杂、格式不可信、老系统导出的 CSV,先让 CleverCSV 探一下。

别急着写清洗逻辑。

先确认你读进去的,真的是那张表。