应付账款里常见的失误:OCR 从扫描发票里读出的 IBAN 是 DE89 3704 0044 0532 O130 00。倒数第二组里的字母 O 很容易看漏,带着它发出的付款会被银行拒绝或退回。
一次 mod-97 校验就能拦下它。IBAN 标准正是为这种场景设计的。
IBAN 到底是什么
IBAN(International Bank Account Number,国际银行账号)由 ISO 13616 定义,SWIFT 通过 IBAN Registry 维护。每个 IBAN 把四样东西压进同一个字符串:
| 部分 | 长度 | 来源 |
|---|---|---|
| 国家代码 | 2 个字母 | ISO 3166-1 alpha-2 |
| 校验位 | 2 位数字 | 其余位的 mod-97 结果 |
| BBAN(Basic Bank Account Number) | 11–30 字符 | 国家标准 |
| 总长度 | 15–34 字符 | 各国固定 |
挪威最短 15 字符;圣卢西亚和马耳他分别 32 与 31。校验位紧跟在国家代码后面——这就是为什么英国 IBAN 长成 GB82 WEST…:GB 是国家,82 是校验位,WEST 起是 BBAN。
世界上没有一个全球机构给个人发 IBAN。每个国家先采纳标准,再定义自己的 BBAN 结构(银行代码在哪、账号多长、每个位置是字母还是数字),并把这套结构发布到 SWIFT IBAN Registry。这套结构就是每个校验器要实现的合同。
mod-97 怎么工作,为什么巧妙
校验位不是随机的。它的取值要保证整个 IBAN——把字母转成数字之后——除以 97 余 1。算法 5 步,浏览器里微秒级完成:
- 归一化。 去掉空白,全部转大写。
- 轮转。 把前 4 个字符(国家代码 + 校验位)移到末尾。
- 字母替换。 每个字母换成两位数字:
A → 10,B → 11,…,Z → 35。 - 取模。 把得到的纯数字串当作一个大整数,计算
n mod 97。 - 比较。 余数为
1即校验通过。
function isValidIban(raw) {
const s = raw.toUpperCase().replace(/[^A-Z0-9]/g, '');
if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/.test(s)) return false;
const rearranged = s.slice(4) + s.slice(0, 4);
const numeric = [...rearranged]
.map(c => (c >= 'A' ? (c.charCodeAt(0) - 55).toString() : c))
.join('');
// BigInt 避免长 IBAN(俄罗斯 33 字符)超过 53 位浮点上限。
return BigInt(numeric) % 97n === 1n;
}
为什么是 97?三个理由:
- 质数。 选一个质数模数能最大化「任意一位错误改变余数」的概率,因为没有任何更小的因子能吞掉这个错误。
- 两位数。 余数 mod 97 永远落在两个字符里,正好对得上标准给校验位留的两位空间。
- 覆盖常见手误。 IBAN 采用的 ISO/IEC 7064 MOD 97-10 方案,设计上能检出所有单个字符替换和所有相邻两位的调换。
第 2 步的轮转把校验位挪到末尾,这是 ISO/IEC 7064 要求的位置。国家代码也在参与取模的数字里,所以把 DE 换成 FR、或者校验位手误,同样会让结果对不上。
BBAN 才是各国发挥创意的地方
国家代码 + 校验位之后的 BBAN 由各国自行定义。SWIFT IBAN Registry 把每个国家的布局编码进去。光看几个就知道有多花:
| 国家 | 长度 | BBAN 结构 |
|---|---|---|
| 挪威(NO) | 15 | 4 位银行 + 6 位账号 + 1 位国内校验位 |
| 比利时(BE) | 16 | 3 位银行 + 7 位账号 + 2 位国内校验位 |
| 荷兰(NL) | 18 | 4 字母银行(ABNA、RABO、INGB…) + 10 位账号 |
| 德国(DE) | 22 | 8 位银行 + 10 位账号(无分行) |
| 英国(GB) | 22 | 4 字母银行 + 6 位 sort code + 8 位账号 |
| 法国(FR) | 27 | 5 位银行 + 5 位分行 + 11 字符账号 + 2 位国内校验位 |
| 意大利(IT) | 27 | 1 字母国内校验 + 5 位银行 + 5 位分行 + 12 字符账号 |
| 沙特阿拉伯(SA) | 24 | 2 位银行 + 18 字符账号 |
| 巴西(BR) | 29 | 8 位银行 + 5 位分行 + 10 位账号 + 1 字母账户类型 + 1 字符持有人 |
| 毛里求斯(MU) | 30 | 6 位银行代码(4 字母 + 2 数字)+ 2 位分行 + 12 位账号 + 000 + 3 字母币种 |
有两个模式值得专门讲一下:
拉丁语区集团(FR、IT、BE、MC、MR、PT、SM、ST、TN) 都在 BBAN 里另嵌一个国内校验位,与 IBAN 层面的 mod-97 无关。法国的 RIB key 是对银行 + 分行 + 账号单独做的一次 mod-97;意大利 BBAN 的第一个字符是 CIN 字母(A–Z),按位置加权表算出。IBAN 的 mod-97 已经能抓住所有单字符替换,国内校验位的作用主要在别处:如果先录入国内账号、再由系统生成 IBAN,录错一位得到的 IBAN 校验位照样正确,只有国内校验能把它拦下来。
英语区集团(GB、IE、MT) 用 字母作为银行代码。这些字母是银行短名的前缀(WEST、BARC、LOYD、HSBC),所以解析一个 GB IBAN 拿到的信息比德国的更可读:不查表你就能猜到 BARC 是 Barclays。
现实里 IBAN 错误从哪里来
IBAN 错了,损失通常不在钱本身——银行会把款退回来,有时几周后才退。真正的成本是那笔退单费(每笔 5–30 EUR)、运营跟单的工时、以及供应商关系上的摩擦。值得防范的几类:
OCR 误识
扫描发票的失败方式可预测。最常见的字符替换:
| 错 | 对 | 原因 |
|---|---|---|
O | 0 | 字母 O 与数字零在低质量字体下几乎一样 |
I / l / 1 | 1 | 无衬线字体让这几个字形撞在一起 |
S | 5 | 斜体或装饰字体下的常见识别错误 |
B | 8 | 银行流水的压缩输出 |
Z | 2 | 欧洲大陆手写体习惯 |
这些都属于单字符替换,mod-97 全都能抓住。ZeroTool 校验器会提示「校验失败(mod 97 ≠ 1),可能是校验位或账号正文有误。」它不会指出是哪一个字符错了,需要你对照上表找外形相近的字符。
空白字符与零宽字符
从 PDF 或 Outlook 复制粘贴经常带进来不间断空格(U+00A0)、零宽连接符(U+200D)、偶尔还有 BOM。只去掉 ASCII 空格的脚本会在这些字符上出错。ZeroTool 的规范化步骤先转大写,再去掉 A–Z、0–9 以外的所有字符,然后才校验。ISO 13616 规定电子格式不带分隔符、打印格式每四位用空格分组,所以去掉空格是预期之内的;去掉其他字符则是方便之举,也会把混进来的标点一起藏掉。
前导零丢失
完整的 IBAN 以字母开头,电子表格会把它当文本保留。出问题的是拆开的部分:单独存放在各列里的国内账号和银行代码,被 Excel 转成数字后会丢掉前导零(0123 变 123),用这些列拼回来的 IBAN 长度就不对了。每一部分都要按文本存。
国家代码错位
DE(德国)和 DK(丹麦)只差一个字母,而它们的 IBAN 长度差了 4 位。把德国 IBAN 的正文放到丹麦前缀下,长度检查会立刻失败。校验器会给出具体的错误:「Denmark 的 IBAN 长度不正确:期望 18,实际 22。」
小写字母
标准是仅允许大写。有些银行为了可读性会印成大小写混排;有些邮件把 IBAN 包进超链接,链接的处理逻辑会顺手把文本小写。归一化能处理这一类,但遇到「按理该过却没过」的报障时记得想到它。
IBAN 校验器告诉不了你的事
mod-97 一过就说校验器「对了」很有诱惑力。其实不对。三件事永远在它能力范围之外:
- 账户是否存在。 银行可能上周就把它销户了。mod-97 不知道。
- 账户是否能正常使用。 冻结、长期未激活、被封——这些账户都会先收下指令再在结算时退回。
- IBAN 与账户名是否对得上。 欧盟的 PSD2 与 SEPA 方案把这件事压给收款行,发送方不查。英国的 Confirmation of Payee 与欧盟 2024–2025 落地的 Verification of Payee 正是冲着这个去的,但它们生活在银行层,不在你的前端校验里。
正确的心智模型:IBAN 校验是必要的第一道筛子,不是充分条件。 客户端用它便宜地抓住手误,剩下交给银行的 API(或一次真实的付款尝试)来兜底。
把校验集成进支付表单
典型模式:边输入边校验,成功显示绿色对勾,失败显示行内错误,没过 mod-97 就不准提交。原生 JS 的骨架:
<label for="iban">IBAN</label>
<input
id="iban"
type="text"
inputmode="text"
autocapitalize="characters"
spellcheck="false"
autocomplete="off"
aria-describedby="iban-msg"
/>
<p id="iban-msg" role="status" aria-live="polite"></p>
<script>
const input = document.getElementById('iban');
const msg = document.getElementById('iban-msg');
input.addEventListener('input', () => {
const result = isValidIban(input.value); // 用前面那个函数
msg.textContent = result ? 'Valid IBAN.' : 'IBAN checksum invalid.';
msg.className = result ? 'ok' : 'err';
});
</script>
落地时有三个小细节值得多看一眼:
autocapitalize="characters"防止 iOS 自动纠正把输入变成gB82…——那样会直接挂在 format regex 上。aria-live="polite"让屏幕阅读器用户能听到校验结果,但不会抢走焦点。- 边输入边校验,而不是只在提交时校验。 在第
n次按键就抓住错,而不是等到 300ms submit round-trip 之后——这是整件事的关键。
如果你在校验成功后还要打一个付费 IBAN API(带国内校验位 + 银行存在性查询),就给校验加个 debounce:mod-97 跑得太快可以每次按键都跑;API 调用建议 debounce 300–500ms。
比较一下几种免费选项
把 IBAN 校验放到三种地方:客户端库、SaaS API、浏览器工具。
| 选项 | 覆盖面 | 费用 | 隐私 |
|---|---|---|---|
iban(npm 包) | mod-97 + 长度 | 免费 | 客户端 |
| ZeroTool IBAN Validator & Parser | mod-97 + 长度 + BBAN 拆分 | 免费 | 客户端 |
| iban.com REST API | mod-97 + 银行查询 + IBAN→BIC | 按次收费 | server-to-server |
| openiban.com REST API | mod-97 + 长度 | 免费,限速 | server-to-server |
选哪个看你的问题是什么。个人项目的免费表单——npm 包或 ZeroTool 页面都够用。真的在跑钱的支付处理器——API 那一档值这个钱:国内校验位的覆盖和 IBAN→BIC 映射,第一次拦下本地校验漏掉的问题就回本了。
ZeroTool 这个工具专门做「检视」用例:手里有一个 IBAN,想看它解析成什么,又不想把它传出去给任何人。同样的 mod-97 引擎,加上标准 npm 库不一定暴露的国家感知 BBAN 字段拆分。
范围之外的事(以及为什么)
你会发现 ZeroTool 不做这几件事:
- IBAN 生成器。 给定国家和银行代码,数学上可以反推出一个校验位正确的「合法 IBAN」。我们不开放这个能力,因为它降低了账号伪造类骗局的门槛。真正的 IBAN 来自银行,绕开这条路的开发者需求基本都不正当。
- BIC / SWIFT 查询。 把银行代码映射到银行名需要一份按月更新的授权数据库,必须分发,长期维护成本不低。BIC 查询请用 SWIFT 官方目录或所在国央行注册表,那才是权威。
- SEPA 收款 QR 码。 欧洲支付理事会的 EPC069-12 格式做了一个能让银行 App 自动填好转账界面的 QR。ZeroTool 的 QR 码生成器 可以生成 QR,只要你自己拼出 EPC069-12 的 payload 即可——校验工具的职责是 IBAN,不是支付指令。
这些是刻意留下的空白。把所有相关能力塞进同一个工具,既稀释工具本身的定位,也稀释它的信任模型。
延伸阅读
- ISO 13616-1:2020 — 正式标准,付费下载,但摘要免费可见。
- SWIFT IBAN Registry — 每个国家 BBAN 结构的公开 PDF,定期更新。
- 欧洲支付理事会 — SEPA Credit Transfer rulebook — 你提交 IBAN 之后银行实际怎么处理。
- Wikipedia: International Bank Account Number — 标准的可读版历史,以及各国采纳时间线。
在 ZeroTool 站内,IBAN 校验器和 URL 解析器(解析支付跳转 URL 里的 token)、Cookie 解析器(调试支付门户下发的 session cookie)、QR 码生成器(IBAN 校验通过后构造 SEPA EPC069-12 二维码)天然搭配。
下次发票里出现一个 OCR 刚刚识别出来的 IBAN,在粘进支付页面之前先粘到校验器里。mod-97 会在微秒内告诉你这笔汇款会不会被退回。两分钟的尽职调查,第一次替你省下 25 EUR 的退单费就回本了——给供应商关系带来的隐性回报远不止此。