ARTICLE · 1055785
Excel 导入报类型转换异常,那个值你按 Ctrl+F 搜不到
写在开头
线上导入一个 Excel,控制台抛出来一串 NumberFormatException。
栈里带着那个转换失败的值,看着还挺贴心。
你把它复制出来,粘到 Excel 的搜索框里。
搜不到。。。
不是搜错列了,也不是那个值后来又被人改过。是真的搜不到,你把那一列从上到下翻三遍都找不着它。而下次导入还是会炸在同一个地方。
这篇想聊的就是这件事。为什么异常消息给了你值,你却找不到它在哪一格,以及在导入之前怎么把这批值提前揪出来。
先说一句,下面用的那个体检脚本是我让 Agent 写的,判据那部分我又让它拿真实的 JDK 对了一遍。所以里面的数字都能追溯,你自己跑一遍也能验。
一、异常给你的是值,不是位置
先看异常长什么样。
java.lang.NumberFormatException: For input string: " 12 "值,引号里那个 12 就是。位置,没有。
平时这不是问题,你拿着 12 去搜索,Excel 会直接跳到那一格。
但有一类值,这套动作会失灵。
我让 Agent 拿 JDK 1.8 把负号跑了一遍。一个负号,六种写法。
new BigDecimal("?12") | - 搜得到吗 | ||
|---|---|---|---|
-12 | |||
-12 | |||
–12 | |||
—12 | |||
−12 | |||
﹣12 |
六个在屏幕上几乎看不出区别,只有一个能被 Java 接受。你在 Excel 里按住 - 搜,另外五个它根本不认识。
Excel 是按字符的码位搜的,不是按它长什么样搜的。
这就是问题的形状。不在于你发现不了它,在于你定位不到它。
二、空格比负号更麻烦
负号好歹还算有个符号摆在那儿,空格是真的什么都没有。
我让 Agent 又跑了一组,把一个空白字符分别放在 12 两边,看 Java 的 String.trim() 之后还能不能转。
trim() | ||
|---|---|---|
原因在 Java 8 的 String.trim() 只干一件事,把码位小于等于 U+0020 的字符去掉。上面那几个后一半的码位都比它大,从它眼皮底下穿过去。
可能有朋友会说,那用 commons-lang3 的 StringUtils.trim() 不就行了。
也不行。它走的是 Character.isWhitespace,能救全角空格,但 NBSP 和 U+2007、U+202F 的 isWhitespace 是 false,照样穿过去。
这一堆里最狠的是 NBSP。
它在 Excel 里跟普通空格长得一模一样,肉眼分不出来。从网页上复制数据过来的时候特别容易出现,网页里的 就是它。而且两种 trim 都去不掉。
所以别指望在转换器前面兜一层 trim 就没事了,兜了也没用。
三、我拿真 JDK 对了一遍,发现脚本的尺子太松
到这里问题就清楚了,这批值得在导入之前揪出来,做个体检脚本是最直接的办法。
但这里有个坑,而且这个坑比上面那些都隐蔽。
我让 Agent 把脚本的判据拿去跟真 JDK 逐值对了一遍,对完发现脚本放过的、Java 必抛的,有这么多。
new BigDecimal(s) | ||
|---|---|---|
12 | ||
1,234 | ||
1,234 | ||
nan | ||
Infinity | ||
1_000 |
问题出在脚本判断「这格是不是数字」用的是 Python 的 float()。
float() 比 Java 宽容得多。它顺手把前后空格去掉,顺手把逗号去掉,nan 和 Infinity 它也认,用下划线分隔的 1_000 它也认。
两把尺子的松紧不一样。而它宽松的方向,刚好盖住了最容易炸的那批值。
一个前置校验工具最怕的不是报错,是给你一个假的安全感。
脚本说这份文件干净,你导进去还是炸。然后你就再也不会相信这个工具了。
还有一处是同一个毛病。脚本把所有数字类型看成一档,BigDecimal 和 Integer 一起查。但这两类在 Java 里要求不一样。
BigDecimal 收小数,Integer 不收。
所以某列是 private Integer year,Excel 里填了个 12.5,脚本说没问题,Integer.parseInt("12.5") 抛异常。凡是整数类型的列,那个脚本基本等于没查。
四、如果你要自己写一个,注意这三件事
这一节是这篇文章真正想给的东西。脚本本体我就不贴了,你要用的话,让 AI 按下面这三条约束写一份更适合你自己的,比抄我的强。
判据按 Java 写,不按 Python 写
这是最重要的一条,也是最容易漏的一条。AI 默认就会给你宽松的判据,因为宽松的判据看着更友好。
整数档只认整数,小数档按 BigDecimal 的语法来。
INTEGER_TYPES = {"Integer", "Long", "Short", "Byte"} # 只收整数DECIMAL_TYPES = {"BigDecimal", "Double", "Float"} # 收小数和科学计数法RE_INTEGER = re.compile(r"^[+-]?\d+$")RE_DECIMAL = re.compile(r"^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$")这两条正则是我让 Agent 拿 41 个边界值(.5、5.、1e999、1_000、--1、全角数字这些都塞进去了)同时喂给 JDK 和这两条正则,逐值比出来的。两边一处不一致都没有。
你要换别的方式判也可以,但必须拿真的 JDK 对一遍,不能靠眼睛看着等价。这块需要注意一下,看着等价和对下来等价,是两回事。
列号从 DTO 里读,别靠表头猜
第二个问题是脚本怎么知道第几列对应哪个字段。
Excel 的表头文字靠不住,五行合并表头里翻半天也未必找得到那个叶子节点。常规做法是读 DTO 上的注解。
@Excel(name = "投资金额", fixedIndex = 1)private BigDecimal investAmount;fixedIndex = 1 就是第 2 列,从 0 数。脚本把它读出来,按列号去取格子。
这里有两个容易栽的地方,我让 Agent 试了四种写法才试出来。
一个是 fixedIndex 和 index 不是一回事。你要是用了 index,脚本一个数值列都解析不出来。这个还好,它会直接报错,不会给你错的结果。但如果你在同一个 DTO 里混着用,那就是一部分列被静默漏掉。
另一个是注解和字段声明写在同一行的时候。像这样
@Excel(name = "年份", fixedIndex = 2) private Integer year;private BigDecimal investAmount;按行解析的脚本如果读到 fixedIndex 就往下跳,同一行后面那个 year 就丢了,然后 investAmount 会把它上一行的列号接过去。结果就是字段和列静默错配,脚本报出来的列号是对的,字段名是错的。
这种错最麻烦,因为你看不出来。
还有个小地方顺手提一下,数据从第几行开始,要当成参数传进去,别在代码里写死。而且别在行号上自己减一,openpyxl 取单元格的行号本来就是从 1 数的,你要是按别的语言的下标习惯减了个 1,那最后一行表头就被当成数据扫了。表头文字落在数值列上的话,它会给你报一串假问题。
输出要把不可见字符显形
这一条是整件事的关键,也是原脚本最该改的地方。
你要是把值直接打出来,结果是
' 12 '空格在报告里照样是隐形的。你拿着这份报告,还是找不到那一格。
所以得让脚本按码位渲染,把看不见的字符写成看得见的。
行8 B列(investAmount) BigDecimal 显形=[NBSP不换行空格]12 ↳ 可疑字符:NBSP不换行空格 U+00A0行11 B列(investAmount) BigDecimal 显形=[U+FF0D 全角减号]12 ↳ 可疑字符:全角减号 U+FF0D这样你才有一个可以照着改的东西。
顺便再把 Java 8 的 trim() 逻辑复刻进去,凡是前后带空白的格子,直接标出来它到底救不救得回来。
行7 显形=[空格]12[空格] | Java 的 trim() 能去掉,框架 trim 过就没事行8 显形=[NBSP不换行空格]12 | Java 的 trim() 也去不掉,必须改不然报告里一堆前后有空白,你也不知道哪几个要动手。
说完这三条约束,还得说一句这套做法的边界,不然你会对它期望过高。
它只管数值列。 String 字段不进检查范围,企业名称、项目名称这些列它一眼都不看。想管也行,得另写一组规则,判那一列里有没有全角标点、有没有前后空格。
它只读当前活动的那一个 sheet。 你那份文件里要是还有说明页、字典页,它不会去翻。
判据严了以后会误报,这是必然的代价。 业务上确实允许带单位的列,比如填 10m³ 那种,会被它报出来。误报多了就没人愿意跑这个脚本了,所以输出必须分组,把必炸的和取决于你框架的分开,别混成一堆推给人看。
写在结尾
回到开头那件事。
异常消息给你值,不给你位置,这是第一层。你以为有值就能搜到,是因为你默认看起来一样的东西就是同一个东西,这是第二层。等到你发现不一样的空格能有九种写法、一个负号能有六个兄弟,你才反应过来,这件事从头到尾都是定位问题,不是识别问题。
顺着这个想,很多排查场景其实是同一个形状。日志每次都告诉你出事了,你缺的从来不是那句「出事了」,是它没告诉你的那一维。
上面那三条约束我建议你留着。下次你让 AI 帮你写这类小工具,把它们一起发过去,能省掉一个自己很难发现的坑。
至于那个完整的脚本,你要是懒得自己写,我这份改完的版本可以直接跑,后台回复「Excel体检」,我发你。
以上,既然看到这里了,如果觉得不错,随手点个赞、转发给你周围的朋友,你的支持就是我更新的动力~
谢谢你看我的文章,我们,下次再见。