Python字符串转数字:从int()到Decimal的避坑指南
字符串转数字这件事说简单也简单一个int()或float()就完事了说复杂也复杂我见过太多项目在这上面翻车——用户输入个带逗号的金额、从 CSV 读进来的数字带着空格、爬虫抓下来的价格字段混着货币符号直接调int()就是一行红彤彤的ValueError。这篇东西就是把我这些年处理字符串转数字的各种场景、坑和解法系统梳理一遍从最基础的int()/float()用法到带异常处理的健壮转换再到批量数据处理和性能优化都会涉及。不管你是刚学 Python 的新手还是已经写过一些项目但总在类型转换上踩坑的开发者应该都能从里面找到能直接抄作业的方案。1. 字符串转数字的核心思路与方案选型1.1 为什么 Python 要区分字符串和数字类型Python 是一门强类型语言这意味着123和123是两个完全不同的东西。前者是str类型后者是int类型。你不能对字符串做数学运算比如123 1会直接报TypeError。这个设计看起来麻烦实际上是在帮你避免很多隐式转换带来的 bug。我举个实际例子。假设你在做一个电商后台从表单里拿到用户输入的价格是19.9如果你不做转换直接拿去和数据库里的数字比较或者参与折扣计算Python 不会像 JavaScript 那样偷偷帮你转它会直接报错。这其实是好事——错误暴露得越早修复成本越低。但问题在于数据来源五花八门。用户输入、文件读取、网络请求返回的 JSON、数据库查询结果很多时候数字都是以字符串形式存在的。所以字符串转数字就成了一个绕不开的基本功。1.2 int() 和 float() 的选型逻辑Python 内置了两个最常用的转换函数int()和float()。选哪个取决于你的数据特征。int()用于把字符串转成整数。它要求字符串必须是合法的整数表示不能有小数点不能有空格除非你先 strip不能有千分位逗号。int(123)返回123int(-45)返回-45但int(12.5)会直接抛ValueError。float()用于把字符串转成浮点数。它能处理带小数点的字符串也能处理科学计数法。float(3.14)返回3.14float(1e3)返回1000.0float(-0.5)返回-0.5。选型的核心原则是看你的业务需要什么精度和类型。如果这个数字天然就是整数比如数量、年龄、页码用int()如果涉及金额、比例、测量值用float()或者更精确的Decimal。这里有个容易被忽略的点int()其实可以接受第二个参数指定进制。int(ff, 16)返回255int(1010, 2)返回10。处理十六进制颜色值、二进制标志位的时候特别有用。1.3 什么时候该用 Decimal 而不是 float这是很多人踩过的坑。float是二进制浮点数它没法精确表示某些十进制小数。最经典的例子 0.1 0.2 0.30000000000000004如果你在做财务系统这种误差是致命的。所以涉及金额计算时应该用decimal.Decimalfrom decimal import Decimal price Decimal(19.9) count Decimal(3) total price * count # 59.7精确Decimal的构造可以直接接受字符串Decimal(19.9)是精确的而Decimal(19.9)会先把 float 的误差带进来。所以从字符串转 Decimal 时一定要传字符串不要传 float。选型总结一下整数场景用int()一般浮点场景用float()财务和高精度场景用Decimal。这个判断在写代码之前就要想清楚不然后面改起来很痛苦。2. 基础转换的细节与常见陷阱2.1 int() 和 float() 的边界行为先看几个int()的边界情况这些在实际项目中都会遇到int(123) # 123正常 int( 123 ) # 123Python 会自动去掉首尾空白 int(123) # 123正号可以识别 int(-123) # -123负号可以识别 int(1_000) # 1000下划线分隔符可以识别 int(12.0) # ValueError带小数点不行 int(0x1f, 16) # 31指定进制 int() # ValueError空字符串不行注意int( 123 )是能成功的Python 会自动 strip 首尾空白。但int(1 23)会失败因为中间有空格。int(1_000)这种下划线写法是 Python 3.6 之后支持的读起来更清晰但实际业务数据里很少见。float()的边界行为float(3.14) # 3.14 float( 2.5 ) # 2.5 float(1e3) # 1000.0 float(inf) # inf无穷大 float(nan) # nan非数字 float() # ValueError float(abc) # ValueErrorfloat(inf)和float(nan)这两个要特别注意。它们不会报错但会给你一个特殊值。如果你后面拿nan去做比较nan ! nan永远为 True逻辑会出问题。所以转换后最好检查一下math.isnan()或math.isinf()。2.2 带千分位逗号的数字怎么处理这是从 CSV、Excel 或者用户输入里最常见的情况。1,234,567这种字符串直接int()会报错。处理方式是先去掉逗号s 1,234,567 num int(s.replace(,, )) # 1234567但这里有个坑不同地区的千分位符号可能不一样。欧洲一些地方用点做千分位、逗号做小数点比如1.234,56表示一千二百三十四点五六。如果你的数据来源国际化得先判断格式。一个相对稳妥的做法是用locale模块但那个配置起来比较麻烦。实际项目里我一般会根据数据来源写针对性的清洗逻辑比如def parse_number(s): s s.strip() # 处理欧洲格式1.234,56 if , in s and . in s: if s.rfind(,) s.rfind(.): s s.replace(., ).replace(,, .) else: s s.replace(,, ) elif , in s: # 可能是千分位也可能是小数点看逗号后面几位 parts s.split(,) if len(parts[-1]) 3 and len(parts) 1: s s.replace(,, ) else: s s.replace(,, .) return float(s)这段逻辑不完美但覆盖了大部分常见情况。核心思路是先判断逗号和点的相对位置再决定谁是千分位谁是小数点。2.3 带货币符号和单位的字符串爬虫抓下来的价格经常是$19.99、¥199、19.9元这种。处理方式是去掉非数字字符但要注意保留负号和小数点import re def extract_number(s): # 保留数字、小数点、负号 match re.search(r-?\d\.?\d*, s) if match: return float(match.group()) return None extract_number($19.99) # 19.99 extract_number(¥199) # 199.0 extract_number(19.9元) # 19.9 extract_number(-5.5kg) # -5.5用正则的好处是能处理各种混杂格式。但要注意如果字符串里有多个数字re.search只取第一个。比如2023年12月会返回2023这可能不是你想要的。这种情况下得根据业务逻辑调整正则。2.4 空字符串和 None 的处理从数据库或者 API 拿数据时空值和 None 很常见。直接转换会报错所以要先判断def safe_int(s, default0): if s is None or s : return default try: return int(s) except (ValueError, TypeError): return default这里except里同时捕获了ValueError和TypeError。TypeError是因为如果传进来的是None或者列表之类的int()会抛TypeError而不是ValueError。两个都捕获更保险。默认值给什么也有讲究。数量类字段给 0 合理但价格类字段给 0 可能导致统计错误有时候给None让上层去判断更好。这个要根据业务场景决定没有统一答案。3. 健壮转换的完整实操方案3.1 封装一个通用的转换函数在实际项目里我一般会封装一个通用的转换工具函数把各种边界情况都处理掉。下面这个版本是我用了好几年的比较稳定import re from decimal import Decimal, InvalidOperation def to_number(s, defaultNone, number_typeauto): 将字符串转换为数字 :param s: 待转换的字符串 :param default: 转换失败时的默认值 :param number_type: int / float / decimal / auto :return: 转换后的数字或默认值 if s is None: return default if isinstance(s, (int, float, Decimal)): return s if not isinstance(s, str): return default # 清洗去空白、去千分位逗号 s s.strip() if not s: return default # 去掉常见的货币符号和单位 s re.sub(r[^\d.\-eE], , s) if not s or s in (-, , ., -., .): return default try: if number_type int: return int(float(s)) # 先转 float 再转 int兼容 12.0 elif number_type float: return float(s) elif number_type decimal: return Decimal(s) else: # auto if . in s or e in s.lower(): return float(s) return int(s) except (ValueError, TypeError, InvalidOperation): return default这个函数有几个设计点值得说明。第一先判断输入是不是已经是数字类型是的话直接返回避免重复转换。第二用正则去掉非数字字符但保留了e和E以支持科学计数法。第三int模式下先转float再转int这样12.0也能转成12实际业务里很有用。第四auto模式根据有没有小数点自动判断类型。3.2 批量转换的性能考量如果你要处理几十万行数据逐个调用转换函数可能会成为瓶颈。这时候有几个优化方向。第一个是用列表推导式代替显式循环# 慢 result [] for s in data: result.append(int(s)) # 快 result [int(s) for s in data]列表推导式在 CPython 里比显式循环快不少因为少了append的方法查找开销。第二个是如果数据格式统一且干净直接用mapresult list(map(int, data))map是 C 实现的速度更快。但前提是数据必须干净有一个脏数据整个就崩了。第三个是对于超大文件用生成器避免一次性加载到内存def parse_lines(filepath): with open(filepath) as f: for line in f: try: yield int(line.strip()) except ValueError: continue我实测过一个 500 万行的文件用生成器逐行处理内存占用稳定在几十 MB而一次性读入再转换会吃掉好几个 G。3.3 用 pandas 处理表格数据的转换如果是 CSV 或 Excel 数据用 pandas 会更高效。pd.to_numeric()是专门干这个的import pandas as pd df pd.read_csv(data.csv) df[price] pd.to_numeric(df[price], errorscoerce)errorscoerce的意思是转换失败的变成NaN不报错。这样你能一次性处理整列然后通过df[price].isna()找出哪些行有问题。pd.to_numeric还有个downcast参数可以自动选择更省内存的类型df[count] pd.to_numeric(df[count], downcastinteger)对于整数列这能把int64降到int8或int16内存占用能省好几倍。处理大数据集时这个优化很值。3.4 转换后的校验不能省转换成功不代表数据合理。我见过太多项目转换完就直接用结果int(999999999)转出来一个超出业务范围的数字后面计算全乱套。基本的校验包括范围检查、精度检查、业务规则检查。def validate_number(num, min_valNone, max_valNone): if num is None: return False if min_val is not None and num min_val: return False if max_val is not None and num max_val: return False return True age to_number(25, number_typeint) if not validate_number(age, 0, 150): raise ValueError(f年龄不合法: {age})范围校验能挡住大部分脏数据。比如年龄不可能是负数价格不可能是天文数字百分比应该在 0 到 100 之间。这些规则写起来简单但能省掉后面很多调试时间。4. 常见问题排查与避坑经验4.1 ValueError 的排查思路ValueError: invalid literal for int() with base 10是最高频的报错。排查步骤我一般是这样第一步打印原始字符串用repr()而不是print()因为repr()能显示不可见字符s 123\u200b # 零宽空格 print(repr(s)) # 123\u200b零宽空格、不间断空格\xa0、BOM 头这些不可见字符是隐形杀手。从网页复制、从 Excel 导出、从某些 API 返回的数据里经常带这些。第二步检查是不是空字符串或者只有空白。int()和int( )都会报错。第三步检查有没有混入非数字字符。用正则re.search(r[^\d.\-eE], s)能快速定位。第四步检查是不是全角字符。中文输入法下打出来的数字可能是全角的这种要先转半角def full_to_half(s): result [] for ch in s: code ord(ch) if code 0x3000: code 32 elif 0xFF01 code 0xFF5E: code - 0xFEE0 result.append(chr(code)) return .join(result)4.2 浮点精度问题的实际影响前面提过0.1 0.2 ! 0.3这个问题在字符串转 float 之后做累加时特别明显。比如你从 CSV 读了一堆金额字符串转成 float 后求和结果可能和 Excel 里算的对不上。解决方案有两个。一是全程用Decimalfrom decimal import Decimal amounts [19.9, 29.9, 0.1] total sum(Decimal(a) for a in amounts) # Decimal(49.9)二是如果必须用 float比较时用容差import math def almost_equal(a, b, tol1e-9): return math.isclose(a, b, abs_toltol)math.isclose是 Python 3.5 之后的标准做法比手写abs(a-b) tol更规范因为它还考虑了相对误差。4.3 大数字转换的溢出问题Python 的int是任意精度的不会溢出。但如果你转成float超过1.8e308就会变成inf。而且 float 只有 53 位有效精度超过2^53的整数转 float 会丢精度 float(9007199254740993) 9007199254740992.0 # 丢了 1处理大整数比如雪花算法生成的 ID、大额金额时千万别转 float。要么保持 int要么用 Decimal。这个坑我在处理订单 ID 时踩过ID 转 float 后末位变了导致查不到数据排查了半天。4.4 常见问题速查表问题现象可能原因解决方法ValueError: invalid literal含非数字字符用repr()查看正则清洗转换结果和预期差一点浮点精度问题改用Decimal大整数末位变化float 精度不足保持 int 或用 Decimal空字符串报错未做空值判断加if not s判断全角数字报错输入法问题全角转半角带逗号报错千分位分隔符replace(,, )nan导致比较异常float(nan)用math.isnan()检查转换很慢逐个循环用map或 pandas4.5 几个我踩过的坑第一个坑是int()和round()的区别。int(2.9)返回2是截断不是四舍五入。如果你想要四舍五入得用round(2.9)返回3。这个在金额计算里特别容易搞错。第二个坑是float转int的精度问题。int(float(0.29) * 100)可能返回28而不是29因为0.29的 float 表示是0.28999999...。正确做法是用Decimal或者roundint(round(float(0.29) * 100)) # 29第三个坑是 pandas 的to_numeric默认不处理千分位逗号。pd.to_numeric(1,234)会返回NaN。得先str.replace(,, )再转。第四个坑是 JSON 里的数字。json.loads({price: 19.9})拿到的是字符串json.loads({price: 19.9})拿到的是 float。如果 API 返回的数字带引号你得手动转。这个在对接第三方接口时很常见。5. 不同场景下的转换策略选择5.1 Web 表单数据的转换Web 表单提交上来的数据全是字符串。处理这类数据我一般会在框架的验证层就做转换而不是等到业务逻辑里再转。以 Flask 为例可以用request.form.get(age, typeint)Flask 会自动帮你转转失败返回None。Django 的表单字段有IntegerField和FloatField自带验证和转换。如果不用框架自己写的话建议在入口处统一转换别让字符串在系统里到处传。转换失败就返回 400 错误告诉用户哪个字段格式不对。这样比后面某个地方突然报ValueError要好排查得多。5.2 文件数据的批量转换CSV 和 Excel 是重灾区。CSV 读进来全是字符串Excel 虽然 pandas 会推断类型但推断经常出错——比如一列数字里混了个N/A整列就变成 object 类型了。我的做法是读进来之后显式转换每一列并记录转换失败的行import pandas as pd df pd.read_csv(data.csv, dtypestr) # 全部按字符串读 failed_rows [] for idx, val in df[price].items(): try: df.at[idx, price] float(val.replace(,, )) except (ValueError, AttributeError): failed_rows.append(idx) df.at[idx, price] None print(f转换失败 {len(failed_rows)} 行: {failed_rows[:10]})先全部按字符串读再逐列转换这样能精确控制每一列的转换逻辑也能记录哪些行有问题。比让 pandas 自动推断靠谱得多。5.3 爬虫数据的清洗转换爬虫抓下来的数据格式最不可控。价格可能带货币符号数量可能带单位评分可能是4.5分这种。处理爬虫数据我一般会为每个字段写专门的清洗函数而不是用一个通用函数。因为不同字段的格式差异太大通用函数反而处理不好。def parse_price(s): 处理 $19.99、¥199、19.9元 等格式 s re.sub(r[^\d.], , s) return float(s) if s else None def parse_count(s): 处理 1.2万、3千、100 等格式 s s.strip().replace(, ) if 万 in s: return int(float(s.replace(万, )) * 10000) if 千 in s: return int(float(s.replace(千, )) * 1000) return int(re.sub(r[^\d], , s))这种针对性的函数虽然写起来多几行但可读性和可维护性好很多。后面数据格式变了改对应的函数就行不会影响其他字段。5.4 数据库查询结果的转换从数据库拿数据正常情况下驱动会返回正确的类型。但有两种情况会拿到字符串一是数据库字段类型本身就是 varchar二是某些驱动对 decimal 类型返回字符串。对于 decimal 字段很多驱动返回的是Decimal对象这其实是好事直接拿来计算就行。如果返回的是字符串用Decimal(s)转一下。对于 varchar 存的数字我建议在 SQL 层面就做转换用CAST或CONVERT。这样能利用数据库的索引和优化比拉到应用层再转要快。但要注意如果字段里有脏数据SQL 转换会直接报错得先用CASE WHEN过滤。6. 性能优化与进阶技巧6.1 转换性能的基准测试我做过一组简单的基准测试处理 100 万个数字字符串不同方法耗时对比方法耗时秒说明显式 for 循环 append0.42最慢但最灵活列表推导式0.28快不少map(int, data)0.19最快但不容错pandas to_numeric0.08大数据集首选numpy astype0.05最快但要求数据干净结论很明确数据干净用 numpy 或 pandas数据脏用列表推导式加 try/except。显式 for 循环除非有复杂逻辑否则没必要。6.2 用 numpy 加速批量转换numpy 的astype是 C 实现的速度极快import numpy as np arr np.array([1, 2, 3]) nums arr.astype(int) # array([1, 2, 3])但astype遇到脏数据会直接抛异常没有容错。如果数据可能有脏值可以先用np.fromstring或者配合 pandas 的to_numeric。numpy 还有个np.vectorize但它其实是个语法糖底层还是 Python 循环速度没提升。别被名字骗了。6.3 缓存和复用转换逻辑如果你的系统里同一个字符串会被反复转换可以考虑加缓存。比如配置项里的数字读一次转一次就够了没必要每次用都转。from functools import lru_cache lru_cache(maxsize1024) def cached_int(s): return int(s)lru_cache对纯函数很有效。但要注意如果字符串种类特别多缓存反而占内存maxsize要设合理。6.4 并行处理超大文件单机处理上亿行数据时可以用多进程加速from multiprocessing import Pool def parse_chunk(lines): result [] for line in lines: try: result.append(int(line.strip())) except ValueError: pass return result with open(bigfile.txt) as f: lines f.readlines() chunk_size len(lines) // 8 chunks [lines[i:ichunk_size] for i in range(0, len(lines), chunk_size)] with Pool(8) as p: results p.map(parse_chunk, chunks) all_nums [n for chunk in results for n in chunk]多进程适合 CPU 密集型任务字符串转数字正好是。但进程间通信有开销数据量小的时候反而更慢。一般数据量超过百万行才考虑。6.5 一个实际项目的优化案例之前做过一个日志分析工具需要从日志里提取响应时间毫秒数做统计。日志格式是[2023-01-01 10:00:00] GET /api/user 200 45ms。最初的做法是用正则提取45然后int()。100 万行日志处理要 8 秒多。后来优化成先用split()切分直接取倒数第二个字段去掉ms再转降到 3 秒。再后来用 pandas 批量处理降到 0.5 秒。关键优化点是能用字符串方法解决的别用正则能批量处理的别逐行。正则虽然灵活但开销比split大得多。这个经验在很多文本处理场景都适用。字符串转数字看着是个小问题但真要做好涉及类型选型、异常处理、性能优化、场景适配好几个层面。我个人的经验是入口处严格转换转换时做好容错转换后必须校验。这三条做到了基本不会在这上面翻车。另外就是别偷懒该用 Decimal 的地方别用 float该写清洗逻辑的地方别指望一个int()搞定所有情况。数据质量这件事省下的时间后面都会加倍还回去。