ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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。比如卡号6222021234567890,BIN 表里可能有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_match

build_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') ); // 构建 Map,key 为 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做防抖。另外,卡号输入框最好限制只能输数字,inputmode="numeric"在移动端能唤起数字键盘,体验会好很多。

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_name,value 是限额对象。查询 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", encoding="utf-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 表,先跑一遍这个脚本,把问题记录清掉再入库。从那以后,再也没出现过因为前导零丢失或者长度字段类型不对导致的线上问题。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进