Python QUOTE_NONNUMERIC:同一串长整数,加不加引号为什么决定能否保住末位

昨天 4阅读

CSV中两个字段写着完全相同的长整数,只有第二个带双引号。启用QUOTE_NONNUMERIC读取后,前者变成浮点数且末位已改变,后者仍是完整字符串。这个参数不只控制怎样识别字段边界,还主动参与类型转换,必须与列的数据契约一起决定。

下面在Linux、CPython 3.12.14实跑,仅使用StringIO,不读取或覆盖实际文件。保存为demo.py,用python3 demo.py执行。长整数固定为9007199254740993,旁边放入带引号与不带引号的3.5,便于同时观察值和类型。

Python QUOTE_NONNUMERIC:同一串长整数,加不加引号为什么决定能否保住末位

AI模型生成概念插图:受引号形状保护的细节保留,经过数值转换漏斗的末端细节丢失;不是表格软件截图。

完整程序与本地结果

完整可运行程序

import csv
from io import StringIO

source = '9007199254740993,"9007199254740993",3.5,"3.5"\n'
row = next(csv.reader(StringIO(source), quoting=csv.QUOTE_NONNUMERIC))
assert row == [9007199254740992.0, "9007199254740993", 3.5, "3.5"]
assert int(row[0]) != int(row[1])
print("automatic values:", row)
print("automatic types:", [type(value).__name__ for value in row])
print("integer recovered:", int(row[0]))

plain = next(csv.reader(StringIO(source)))
exact = int(plain[0])
assert exact == 9007199254740993
print("text-first integer:", exact)

buffer = StringIO(newline="")
writer = csv.writer(buffer, quoting=csv.QUOTE_NONNUMERIC, lineterminator="\n")
writer.writerow([9007199254740993, "9007199254740993"])
print("writer output:", repr(buffer.getvalue()))
reread = next(csv.reader(StringIO(buffer.getvalue()), quoting=csv.QUOTE_NONNUMERIC))
assert int(reread[0]) == 9007199254740992
print("round-trip exact:", int(reread[0]) == 9007199254740993)

try:
    next(csv.reader(StringIO("id,value\n"), quoting=csv.QUOTE_NONNUMERIC))
except ValueError:
    print("unquoted header: ValueError")
else:
    raise AssertionError("text headers are not floats")

本次实际输出(以下为结果,不是程序)

automatic values: [9007199254740992.0, '9007199254740993', 3.5, '3.5']
automatic types: ['float', 'str', 'float', 'str']
integer recovered: 9007199254740992
text-first integer: 9007199254740993
writer output: '9007199254740993,"9007199254740993"\n'
round-trip exact: False
unquoted header: ValueError

未加引号会先进入float

automatic types显示float、str、float、str。QUOTE_NONNUMERIC读取时把未引用字段转换成float,并不会先问它属于编号列还是数量列。带引号字段保留为字符串,因此第二项仍保存十进制原文。

本例的长整数超过二进制双精度能够连续表示每个整数的范围,转换结果成为9007199254740992.0。此后再调用int,只能得到已经舍入后的数,无法恢复原本末位。程序比较两条不同转换路径,直接证明信息损失发生在CSV读取阶段。

先读取文本,再按列选择类型

普通reader把四项都读成字符串,随后int直接处理第一项,得到精确的9007199254740993。若它本来是业务编号,还可以一直保留字符串,从而连前导零也不丢;是否需要算术应由字段用途决定,不能让外观像数字替你作决定。

金额、小数测量值和整数计数也未必适合同一种转换。正式导入应先检查列名、字段数量与允许的文本形式,再进入对应类型,并把错误关联到记录位置。示例只展示固定合法整数的精确转换,不把int成功当成完整业务校验。

读写双方选项一致,也不保证类型往返

writer输出中,整数对象没有引号,字符串对象带有引号。重新用同一种QUOTE_NONNUMERIC模式读取时,前者仍会转成float,round-trip exact因此为False。写端没有承诺把每一种Python数值类型编码成可以自动还原原类型的表示。

验收不能只比较写出的文字是否“看起来正确”。应把结果按消费者的实际选项重读,同时核对关键字段的值与类型。若上下游确实约定全部先按文本传输,就应使用相应读取方式;若依赖引用决定类型,生产者也必须遵守这一份更严格的协议。

表头同样会走自动转换路径

最后的id,value没有引号,读取时抛ValueError,因为它们不是可转换的浮点文本。可见参数没有自动识别“首行是标题”的业务概念;直接把此模式套在常见带表头文件上,可能连第一行都过不去。

本例只讨论CSV模块自身行为,不涉及表格软件打开文件后的再解释。修复之后应保留长整数、普通小数、引号差异与文本表头四种样本,并记录消费者实际配置。清楚划定文本解析和类型解释的先后,才能避免导入成功却已经改掉标识值。

把最小复现接到真实排查

在对账或记录关联中,末位改变可能使两条不同编号意外合并。最先观察到的症状却往往只是“找不到原记录”,因为浮点值仍然可以正常打印和参与比较。调查时应回到原始字段文字与读取选项,定位第一次改变值的边界。

不要尝试通过增加显示小数位修复这种情况。显示格式只决定已有数值怎样写成文字,不会补回此前舍去的整数位。同样,先转成浮点再转字符串,即使结果更整齐,也已经不再是生产者提供的那份原始编号。

如果导入流程需要保留逐行错误报告,可以在字段仍是字符串时记录受控的行序号、列名与错误类别,再执行转换。这样能够把结构不合法、数字文本不合法和业务范围不允许分别处理,避免所有问题最后都变成含糊的“数值异常”。

示例没有开启任何忽略错误的回退。真实数据里遇到不能转换的字段,应依据契约拒绝或标记待处理,而非随手置零后继续累计。保存几条已知结果的样本作为长期检查,比仅用一份当时能够导入的大文件更容易发现精度回归。

参考资料

资料核验日期:2026年10月2日。以上输出来自固定输入的本地实跑,程序退出码为0。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。