JSON银行卡BIN码表实战:从校验到风控的完整指南

发布时间:2026/10/18 2:05:21
JSON银行卡BIN码表实战:从校验到风控的完整指南
简介这份资源是一份JSON格式的银行卡BIN码大全面向需要实现银行卡识别与验证功能的前端开发者及支付相关技术人员。BIN码即银行卡前六位数字可用于判断发卡银行、卡片类型信用卡或借记卡及币种等信息借助该对照表可在用户输入卡号时即时匹配并展示发卡行提升表单校验的准确性与交互体验。压缩包内共1个文件为单个json数据文件整体约33KB采用键值对结构存储各BIN码对应的银行名称、卡类型与货币字段便于直接解析调用。目前已有1747人学习下载数据覆盖面较广适合作为本地查询库或接口数据源使用。使用时建议关注数据更新频率、隐私合规、未匹配时的错误提示以及HTTPS安全传输以保证识别结果的准确与稳定。1. 一份 JSON 银行卡 BIN 码表到底能扛住多少种校验场景做支付、风控或者收银系统的同行大概率都遇到过这种需求用户刚输入卡号前六位前端就要立刻显示发卡行和卡种甚至还要判断是不是借记卡、能不能走某个通道。这时候如果手里没有一份靠谱的 BIN 码表要么去调第三方接口要么自己维护一张残缺的 Excel时间一长全是坑。我这次拆的是一份 JSON 格式的银行卡 BIN 码大全核心用途就两个匹配和验证。它把发卡行标识、卡种、卡号长度、校验规则这些信息结构化成了 JSON方便直接塞进代码里做本地查询。适合做支付网关、聚合收银、风控规则引擎的开发者也适合需要离线校验卡号的前端项目。下面我从数据长什么样、怎么读、怎么匹配、怎么避坑一路拆到进阶用法。2. 先看清 JSON 结构字段含义与卡号校验的底层逻辑拿到一份 BIN 码表最怕的就是字段名对不上、卡种定义模糊。这份 JSON 的结构不算复杂但每个字段背后都对应着真实的卡号规则。先把它拆开看再决定怎么用。2.1 BIN、卡种、卡号长度三个必须对齐的字段BIN 是 Bank Identification Number 的缩写通常指卡号前六位但实际匹配时可能用到前八位甚至前十位。JSON 里一般会有一个bin字段可能是字符串也可能是数字这里要特别注意如果存成数字前导零会丢比如某些卡 BIN 以 0 开头存成数字就变成短一截了。所以拿到数据后第一件事是确认bin是不是字符串类型。卡种字段常见的有card_type、brand、scheme值可能是debit、credit、prepaid也可能是visa、mastercard、unionpay。不同来源的 BIN 表命名习惯不一样用之前得统一映射。卡号长度字段通常是length或card_length可能是一个整数也可能是一个数组比如[16, 19]表示该 BIN 下卡号长度可能是 16 位或 19 位。这个字段直接决定你前端输入框的最大长度和校验逻辑。还有一个容易被忽略的字段是luhn布尔值表示该卡种是否强制走 Luhn 算法校验。大部分银行卡都走 Luhn但少数预付卡或特定区域的卡可能不走。如果 JSON 里没有这个字段默认按走 Luhn 处理但遇到校验失败时得回头确认是不是这个原因。{ bin: 622202, bank_name: 某发卡行, card_type: debit, brand: unionpay, length: [16, 19], luhn: true }上面是一个简化后的单条记录。实际文件里可能还有bank_code、country、currency等字段。字段越多匹配时能做的判断越细但同时也意味着数据维护成本越高。我一般会先跑一遍去重和字段完整性检查把bin为空、length缺失、luhn非布尔的记录挑出来单独处理。2.2 Luhn 算法不是万能的什么时候该用什么时候该跳过Luhn 算法也叫模 10 算法是银行卡号校验的通用规则。它的原理很简单从卡号最后一位往前偶数位乘 2乘积超过 9 就减 9然后把所有位相加总和能被 10 整除就算通过。很多开发者以为只要 Luhn 过了卡号就是合法的其实不然。Luhn 只能挡住随机输错一位的情况挡不住刻意伪造的卡号。在这份 BIN 码表的场景里Luhn 的正确用法是先根据 BIN 匹配到卡种和长度范围再用 Luhn 做一次快速校验。如果 BIN 表里明确标了luhn: false那就跳过 Luhn只做长度和 BIN 匹配。我见过有项目对所有卡号无脑上 Luhn结果某些预付卡死活过不了排查半天才发现是卡种本身就不走 Luhn。def luhn_check(card_number: str) - bool: digits [int(d) for d in card_number] checksum 0 # 从右往左偶数位索引为奇数乘2 for i, d in enumerate(reversed(digits)): if i % 2 1: d * 2 if d 9: d - 9 checksum d return checksum % 10 0这段代码里reversed(digits)让遍历从卡号最后一位开始i % 2 1对应的是从右数第二位、第四位……也就是需要乘 2 的位置。注意这里没有处理卡号里的空格或横线实际用的时候得先replace掉非数字字符。参数上card_number必须是纯数字字符串长度不限但一般银行卡在 12 到 19 位之间。2.3 匹配策略前缀树还是二分查找BIN 匹配的本质是前缀匹配。给定一个卡号要找到最长的匹配 BIN。比如卡号6222021234567890BIN 表里可能有6222、622202、6222021三条记录应该匹配到最长的那条。数据量小的时候直接遍历或者用字典按前六位查都行。但这份 JSON 号称“大全”记录数可能上万这时候就得考虑效率。常见做法有两种一是把 BIN 按长度分组先查 8 位再查 6 位再查 4 位逐级降级二是构建前缀树Trie把每个 BIN 的每一位作为节点查询时从根节点往下走记录最后一个有卡种信息的节点。前缀树查询时间复杂度是 O(k)k 是 BIN 最大长度通常不超过 8所以非常快。我一般会选前缀树因为实现不复杂而且能天然处理变长 BIN。class TrieNode: def __init__(self): self.children {} self.info None # 存储卡种、长度等元数据 def build_trie(bin_list): root TrieNode() for item in bin_list: node root for ch in item[bin]: if ch not in node.children: node.children[ch] TrieNode() node node.children[ch] node.info item return root def match_bin(root, card_number): node root last_match None for ch in card_number: if ch not in node.children: break node node.children[ch] if node.info: last_match node.info return last_matchbuild_trie把每条 BIN 逐字符插入树中最后一个字符对应的节点存完整信息。match_bin从卡号第一位开始走每走一步检查当前节点有没有信息有就更新last_match这样最后拿到的就是最长匹配。注意card_number要是纯数字字符串如果卡号里有空格得先清理。3. 把 JSON 用起来加载、查询与接口封装数据看懂了接下来就是怎么把它塞进项目里。这一步的核心是加载速度、查询效率和接口设计。不同语言、不同场景做法不一样但思路是通的。3.1 加载与索引一次性构建常驻内存JSON 文件如果超过几兆每次请求都读文件肯定不行。常见做法是服务启动时加载一次构建好索引后常驻内存。Python 里可以用json.load读进来然后跑一遍build_trie。Node.js 里可以用require或fs.readFileSync加JSON.parse。Java 里可以用 Jackson 或 Gson 反序列化成对象列表再构建 Map 或 Trie。这里有个细节如果 JSON 文件很大比如几十兆解析本身可能耗时几百毫秒甚至更久。我一般会在构建索引时加个计时日志看看启动阶段能不能接受。如果太慢可以考虑把 JSON 转成更紧凑的格式比如 MessagePack 或者直接序列化成二进制但那就偏离这份资源“JSON 格式”的初衷了。对于大多数项目几万条 BIN 记录解析起来也就几十毫秒不用过度优化。const fs require(fs); const path require(path); // 启动时加载一次 const binData JSON.parse( fs.readFileSync(path.join(__dirname, bin-list.json), utf-8) ); // 构建 Mapkey 为 BIN 字符串 const binMap new Map(); binData.forEach(item { binMap.set(item.bin, item); }); function queryBin(cardNumber) { // 从长到短尝试匹配 for (let len 8; len 4; len--) { const prefix cardNumber.slice(0, len); if (binMap.has(prefix)) { return binMap.get(prefix); } } return null; }这段 Node.js 代码用Map做索引查询时从 8 位前缀开始降级到 4 位。binMap.set的 key 是字符串避免了数字前导零丢失的问题。queryBin里的len从 8 开始是因为大部分 BIN 表最长到 8 位如果你的数据里有更长的把 8 改成实际最大值就行。注意cardNumber传进来之前要去掉空格和横线。3.2 查询接口设计返回什么字段怎么处理未匹配查询接口不能只返回一个布尔值得把卡种、发卡行、卡号长度都带出来前端才能做后续判断。我一般会设计成返回一个对象包含matched、bank_name、card_type、brand、length这几个字段。如果没匹配到matched为false其他字段为空前端根据这个决定是提示“暂不支持该卡”还是继续走通用校验。未匹配的情况分两种一种是 BIN 确实不在表里另一种是卡号太短还没到六位。对于第二种前端应该等用户输够六位再查或者查的时候只做前缀匹配不报错。我见过有项目在用户输第一位数字时就调接口结果每次都返回未匹配体验很差。正确做法是前端加个防抖输满六位再触发查询。def query_card_info(card_number: str) - dict: card_number card_number.replace( , ).replace(-, ) if len(card_number) 6: return {matched: False, reason: too_short} info match_bin(trie_root, card_number) if not info: return {matched: False, reason: bin_not_found} # 校验长度 if isinstance(info[length], list): if len(card_number) not in info[length]: return {matched: False, reason: invalid_length} elif len(card_number) ! info[length]: return {matched: False, reason: invalid_length} # 按需走 Luhn if info.get(luhn, True) and not luhn_check(card_number): return {matched: False, reason: luhn_failed} return { matched: True, bank_name: info.get(bank_name), card_type: info.get(card_type), brand: info.get(brand), length: info.get(length) }这个函数把长度校验和 Luhn 校验都串起来了。reason字段方便排查问题前端也可以根据不同的 reason 给不同提示。注意info.get(luhn, True)默认走 Luhn如果 JSON 里明确写了false才跳过。length可能是列表也可能是整数这里做了兼容。3.3 与前端联调什么时候查 BIN什么时候查完整卡号BIN 查询和完整卡号校验是两个阶段。用户输入前六位时可以查 BIN 拿到发卡行和卡种展示在输入框下方。用户输完所有位数后再做一次完整校验包括长度和 Luhn。这两个阶段可以共用一个接口也可以拆成两个。我一般会拆开因为 BIN 查询频率高、逻辑轻完整校验频率低、逻辑重拆开后缓存策略也好做。前端联调时有个坑不同浏览器的input事件触发时机不一样有的在输入法组合期间也会触发。如果直接在input事件里调接口中文输入法下可能会发很多无效请求。常见做法是监听compositionend事件或者用setTimeout做防抖。另外卡号输入框最好限制只能输数字inputmodenumeric在移动端能唤起数字键盘体验会好很多。4. 避坑与排查BIN 码表用错比不用还危险这份资源本身是数据但数据用错了后果比没有数据还严重。下面这几条是我和同行踩过的真实坑按现象、原因、解决三段式写出来供参考。4.1 前导零丢失导致匹配失败现象某些卡号前六位以 0 开头比如012345查询时死活匹配不到但表里明明有这条记录。原因JSON 里bin字段存成了数字类型012345被解析成12345前导零丢了。或者代码里把卡号转成了整数再取前六位同样会丢零。解决确认 JSON 里bin是字符串类型。如果原始数据是数字加载后统一转成字符串并补零到固定长度。查询时卡号也保持字符串不要转数字。4.2 最长匹配没做短 BIN 覆盖了长 BIN现象卡号6222021234567890匹配到了6222这条记录但实际应该匹配622202导致发卡行显示错误。原因查询时只做了前四位匹配或者遍历时先遇到了短 BIN 就返回了没有继续找更长的。解决用前缀树或者从长到短降级匹配。如果用的是 Map查询时从 8 位开始试依次降到 4 位第一个命中的就是最长匹配。4.3 Luhn 校验把合法卡号判死现象某些卡号长度和 BIN 都匹配但 Luhn 校验不通过被判定为非法。原因该卡种本身不走 Luhn但代码里无脑上了 Luhn。或者卡号里混入了非数字字符导致 Luhn 计算错误。解决先查 BIN 表里的luhn字段明确为false的跳过 Luhn。计算前先replace掉空格、横线等非数字字符。4.4 卡号长度字段类型不一致现象有的记录length是整数16有的是数组[16, 19]代码里直接比较len(card_number) info[length]时报类型错误。原因数据来源不统一有的表用整数有的用数组。解决加载后统一转成数组查询时用in判断。或者写个兼容函数整数转成单元素数组再比较。4.5 数据更新滞后新 BIN 查不到现象某家银行新发了一批卡BIN 不在表里用户反馈“不支持该卡”。原因BIN 码表是静态文件没有更新机制。发卡行新增 BIN 段后旧表自然查不到。解决定期检查数据源更新或者留一个兜底逻辑未匹配时走通用校验不直接拒绝。同时记录未匹配的 BIN积累一段时间后手动补充。5. 进阶用法把 BIN 表变成风控规则的一部分BIN 查询只是第一步真正体现价值的是把它嵌进风控规则里。比如根据卡种决定是否允许分期、根据发卡行决定限额、根据卡号长度决定前端输入框行为。下面说几个我实际用过的技巧。5.1 按卡种和发卡行做差异化限额在支付场景里不同卡种的限额策略往往不一样。借记卡可能单笔限额低信用卡可以高一些预付卡则要更严格。BIN 表里如果有card_type和bank_name就可以在查询后直接映射到限额配置。我一般会维护一张额外的规则表key 是card_type bank_namevalue 是限额对象。查询 BIN 后拿到的信息作为 key 去查规则表查不到就走默认限额。LIMIT_RULES { (debit, 某发卡行): {single: 5000, daily: 20000}, (credit, 某发卡行): {single: 20000, daily: 50000}, (prepaid, *): {single: 1000, daily: 3000}, } def get_limit(card_info): key (card_info.get(card_type), card_info.get(bank_name)) if key in LIMIT_RULES: return LIMIT_RULES[key] # 按卡种兜底 fallback_key (card_info.get(card_type), *) return LIMIT_RULES.get(fallback_key, {single: 1000, daily: 5000})这段代码里LIMIT_RULES的 key 是元组*表示通配。get_limit先精确匹配匹配不到再按卡种兜底。实际项目中限额规则可能更复杂还会涉及商户类型、交易时间等但 BIN 信息是其中一环。5.2 用 BIN 表做卡号输入框的实时提示前端体验上用户输入卡号前六位后立刻显示发卡行和卡种能显著降低输错概率。实现方式是在input事件里做防抖输满六位后调查询接口把返回的bank_name和card_type展示在输入框下方。如果返回未匹配可以提示“暂未识别该卡”但不阻止继续输入。这里有个细节不同卡种的卡号长度不一样输入框的maxlength应该动态调整。比如匹配到length: [16, 19]就把maxlength设为 19同时允许用户输到 16 位时就触发完整校验。如果匹配到length: 16就设成 16。这样用户不会多输也不会少输。5.3 验证 BIN 表完整性的三个检查脚本拿到一份 BIN 表别急着用先跑三个检查一是 BIN 字段是否全为字符串且无重复二是length字段是否都在合理范围12 到 19三是luhn字段是否全为布尔值。这三个检查能挡掉大部分数据质量问题。import json from collections import Counter def validate_bin_list(file_path): with open(file_path, r, encodingutf-8) as f: data json.load(f) # 检查 BIN 类型和重复 bins [item.get(bin) for item in data] non_str [b for b in bins if not isinstance(b, str)] duplicates [b for b, c in Counter(bins).items() if c 1] # 检查长度范围 bad_length [] for item in data: lengths item.get(length) if isinstance(lengths, int): lengths [lengths] if not isinstance(lengths, list): bad_length.append(item.get(bin)) continue for l in lengths: if not isinstance(l, int) or l 12 or l 19: bad_length.append(item.get(bin)) break # 检查 luhn 类型 bad_luhn [item.get(bin) for item in data if luhn in item and not isinstance(item[luhn], bool)] return { non_string_bin: non_str, duplicate_bin: duplicates, bad_length: bad_length, bad_luhn: bad_luhn, }validate_bin_list返回四个列表分别对应四类问题。non_string_bin里如果有值说明有 BIN 存成了数字得转字符串。duplicate_bin说明有重复记录得去重或者确认哪条为准。bad_length里的 BIN 长度字段有问题得手动修。bad_luhn里的luhn字段不是布尔值得统一。我现在的习惯是每次拿到新的 BIN 表先跑一遍这个脚本把问题记录清掉再入库。从那以后再也没出现过因为前导零丢失或者长度字段类型不对导致的线上问题。希望帮到你。本文还有配套的精品资源点击获取