
简介一份面向前端及金融支付开发者的银行卡BIN码JSON数据文件将常见银行发卡机构识别码整理为结构化的键值对形式。通过读取卡号前六位即可快速匹配发卡银行、借记卡/信用卡类型及币种信息适用于用户输入实时校验、支付表单自动回填、风控预判等场景有助于降低人工维护对照表的成本。资源包共1个JSON文件压缩包整体仅33KB轻量易用可直接引入项目或按需二次加工。目前已有1746人学习下载数据覆盖多家主流银行按BIN码索引查询效率较高字段含义清晰便于开发者结合前端框架实现即时联想与验证。由于银行信息可能发生变更建议生产环境定期同步更新并配合HTTPS传输以兼顾数据准确性和合规性。1. 从一段卡号到发卡行JSON格式BIN码到底能干什么一次对接渠道时对方给了二十几个测试卡号要求判断每张卡的发卡行、卡类型和卡组织。这类需求在支付、风控、账务系统里很常见卡号前6位就是BIN码Bank Identification Number相当于银行卡的发卡行身份证决定了这张卡归谁发、走哪个卡组织、是借记还是贷记。这份JSON格式的BIN码大全把常用银行的BIN整理成结构化的键值数据加载后可以在本地完成发卡行匹配、卡类型识别和卡号合法性预校验全程不依赖外部接口。适合正在做支付对接、反欺诈预筛、卡Bin分析报表的开发者也适合在联调环境里快速批量核对卡资料避免一遍遍找渠道方要答案。2. BIN码的底层逻辑与JSON数据结构从卡号组成到字段语义2.1 银行卡号是怎么组成的从BIN到校验位一段标准银行卡号结构上由三部分组成发卡行标识BIN、个人账户段、校验位。银联老标准用6位BIN新标准逐步扩展为8位IINIssuer Identification Number发卡机构标识Visa、Mastercard、American Express也各自维护独立的区间分配。做支付的开发者心里要有一个区间直觉数字4开头的国际BIN多半对应Visa51到55开头的多半对应Mastercard34和37开头对应American Express。但也仅限于辅助排查明显的数据异常真正的判断责任必须交给BIN库。原因很简单区间直觉在双标卡、转接卡规则调整之后经常失真硬编码规则会让你的代码在别人换数据源时直接报废。校验位部分业界用的是Luhn算法也叫模10算法。规则可以这样记从卡号右侧第1位开始每隔一位从右边数第2位、第4位……乘2乘完如果大于9就减去9把全部数字加总后如果能被10整除这个卡号在数字结构上就是合法的。Luhn算法最早不是为银行卡设计的但后来几乎所有卡组织都接受了这套校验逻辑。要特别注意的是Luhn通过只代表形状上像一张卡号不代表卡真实存在也不代表BIN在库里有记录这三件事必须分开。def luhn_valid(card_no: str) - bool: if not card_no.isdigit(): return False total 0 # 从右往左处理idx从0开始idx为奇数位时乘2超过9则减9 for idx, ch in enumerate(reversed(card_no)): digit ord(ch) - 48 if idx % 2 1: digit * 2 if digit 9: digit - 9 total digit return total % 10 0这段代码的边界在idx的奇偶判断上。从右往左数第1位是idx0保持原值第2位是idx1乘2后按需减9。最容易写错的地方就是乘2的位置反了导致合法的卡号被判定为非法。参数上建议统一接收字符串而不是整数因为Python的int会丢弃前导0而入参用字符串还能带一层isdigit保护遇到非数字字符直接返回False不会抛异常。实际处理时我强烈建议把校验顺序固定成先长度、再Luhn、后BIN匹配。不少线上问题都出在顺序上有人先调BIN匹配接口结果传进来的卡号只有10位匹配接口返回一个空结果日志里留下一个模棱两可的错误码要花半天才能定位是传参问题还是BIN库问题。先做长度和Luhn的另一个好处是日志里每一个bin_not_found都更值得重视因为它已经排除了明显非法的输入。2.2 为什么用JSON承载BIN码而不是直接进数据库关于BIN码存哪里这个问题我踩过数据库方案也试过Redis方案最后回到本地JSON加载。三个原因。第一BIN码是低频更新的静态数据发布频率远低于业务数据不需要事务和复杂索引第二JSON语言无关Python、Java、Go、Node都能一行解析不用为一个几万行的小库维护DAO层第三一份几十KB到几MB的JSON启动时读入内存构建字典查询是O(1)比每次查MySQL快两三个数量级。和CSV比CSV在老系统里很常见但字段内容里如果出现逗号、换行或者中文编码混乱解析直接翻车和YAML比YAML可读性好但缩进规则太玄学程序对接时容易因为多一个空格解析失败。JSON的键值结构天然适合bin → 卡详情这种映射而且几乎所有语言的标准库都带JSON解析器零额外依赖。这里要澄清一个性能误会JSON文件大不等于解析慢。以几万个BIN为例Python标准库json.load加构建字典启动阶段消耗也就几百毫秒之后查询走哈希索引不消耗解析时间。真正影响性能的是匹配函数的写法而不是数据格式。如果你处理的是几十万条级别可以改成启动时转成SQLite再加载但绝大多数业务场景用不上这一步。更新策略上我的做法是维护一个version字段放在JSON最外层每次发布新BIN库时递增版本号。服务启动时和当前内存版本比对不一致才重新加载。这样既避免了每次重启全量解析也防止运行期读到半截旧数据。2.3 JSON字段语义每个字段在匹配结果里怎么用假如这份资源里单个条目长这样{ bin: 621700, bank_name: 某银行, card_type: debit, brand: UnionPay, card_level: gold, length: 16, valid: true }逐字段解释。bin是卡号前6位作为唯一检索键不能重复bank_name是发卡行名称直接展示或落到报表card_type区分debit借记卡、credit信用卡、quasi-credit准贷记卡在清结算里决定借贷方向也影响风控权重是最容易被忽略的字段brand表示卡组织UnionPay、Visa、Mastercard等决定受理渠道card_level是卡等级普卡、金卡、白金卡、钻石卡权益判断要用length是该BIN对应的标准卡号长度用来收紧校验范围valid表示该BIN当前是否仍在发卡有些老BIN已停发但仍有存量卡在流通标记false可以让业务层做降级处理。加载JSON时字段名的兼容性问题比想象中常见。有的数据源用camelCasebankName、cardType有的用snake_casebank_name、card_type还有的会带出多余字段。我的习惯是拿到数据先建一层字段映射统一内部约定而不是在业务代码里到处写item[bankName]。FIELD_MAP { bankName: bank_name, bank_name: bank_name, cardType: card_type, card_type: card_type, } def normalize_row(raw: dict) - dict: result {} for key, value in raw.items(): new_key FIELD_MAP.get(key, key) result[new_key] value return result这个映射函数逻辑很直白在FIELD_MAP里查表拿到新字段名没有映射就保留原字段名。好处是业务方临时发来一个命名风格不同的版本时只需要在映射里加一行而不是全局搜索替换。参数说明映射表用哈希查找O(1)开销如果同一个外部字段映射到两个内部字段后写入的覆盖先写入的加载阶段会打印重复告警方便排查数据源合并的问题。字段解析完之后还要处理字段缺失的情况。我的兜底原则是宁可为unknown也不猜。bank_name缺失时显示未知银行-加BIN码card_type缺失时填unknown不要默认成debit否则对账时借贷方向会错brand缺失时可以用前缀区间做一个辅助判断但要在结果里标注来源是derived而不是库记录避免下游误以为这是权威数据。字段兜底逻辑统一放在normalize_row之后、写入索引之前这样一个BIN只做一次兜底后面所有调用方拿到的都是干净数据。3. 匹配与验证落地从加载JSON到跑通全链路3.1 加载JSON数据一次性读入内存的工程处理加载这一步我推荐在服务启动时执行而不是在每个请求里解析JSON。原因前面说过解析成本只在启动时付一次运行期全部走内存字典。下面是加载函数的完整写法import json import logging BIN_INDEX_6 {} BIN_INDEX_8 {} CURRENT_VERSION None def load_bin_db(path, allow_updateFalse): global CURRENT_VERSION with open(path, r, encodingutf-8) as f: payload json.load(f) version payload.get(version) if version CURRENT_VERSION and not allow_update: logging.info(BIN库版本未变化: %s跳过加载, version) return rows payload[data] new_index_6 {} new_index_8 {} for item in rows: bin_code str(item[bin]) if len(bin_code) 8: new_index_8[bin_code] item elif len(bin_code) 6: new_index_6[bin_code] item else: logging.warning(跳过异常BIN: %s, bin_code) BIN_INDEX_6.clear() BIN_INDEX_8.clear() BIN_INDEX_6.update(new_index_6) BIN_INDEX_8.update(new_index_8) CURRENT_VERSION version logging.info(BIN库加载完成: 6位 %d 条, 8位 %d 条, len(BIN_INDEX_6), len(BIN_INDEX_8))这里有几个关键设计。第一用临时字典new_index_6和new_index_8先完成解析全部成功后再更新全局索引避免加载过程中途失败导致内存里留下半份数据。第二通过version判断版本是否变化没有变化就跳过加载这在多个实例滚动重启时特别有用。第三bin_code强制转成字符串因为JSON里的数字可能被序列化成int而8位BIN开头的0有可能被丢掉转字符串能保住原始格式。参数说明allow_update默认False调试时需要强制重载就直接传Truepath建议来自环境变量不要写死相对路径因为不同环境的当前工作目录很可能不一样。加载失败时我选择让启动流程直接失败而不是继续用旧数据宁可用新版文件把问题暴露出来也不要在生产环境里悄悄跑老版本。3.2 匹配BIN码前缀匹配的三层回退匹配函数的核心逻辑是先8位后6位的回退。银联新标准里的8位IIN会和老6位BIN共存一段时间新卡走8位索引老卡走6位索引两层必须分开。代码很短但顺序很关键def match_bin(card_no: str): if len(card_no) 6: return None b8 card_no[:8] if b8 in BIN_INDEX_8: return BIN_INDEX_8[b8] b6 card_no[:6] return BIN_INDEX_6.get(b6)先说明为什么先查8位一张8位IIN的新卡其前6位可能与某个老BIN的区间重叠。如果先查6位就会被错误匹配到老BIN上返回错误的发卡行。先查8位找不到再退到6位能尽量命中准确记录。card_no假定已经通过前面的长度和Luhn校验这里可以不做二次防护但如果这个函数被外部直接调用我建议保留长度判断防止切片拿到空串。不用Trie树的理由也值得说清楚。BIN匹配本质是固定长度的前缀匹配最长8位字典大小几万条哈希查找已经是O(1)。Trie树的优势在于处理变长前缀、需要遍历所有匹配前缀的场景比如关键词过滤或者路由表在这里属于过度设计还会引入额外的节点存储开销。真遇到性能瓶颈应该优先排查是否每次请求都重复构建了字典而不是质疑哈希不够快。自测脚本我会这么写随机生成100万次匹配统计QPS和P95耗时。在这个数据集规模下纯哈希查找的实现跑出几十万QPS很正常。如果测出来连一万都不到先看匹配函数里是不是有正则匹配或者字符转int这两个操作才是隐藏的耗时点。3.3 验证链路把长度、Luhn、BIN匹配串成一条流水线前面几节分别讲了三件事加载、匹配、校验。实际业务里它们是一条流水线顺序不能乱。下面给出一个可以直接放进服务里的完整verify_card函数def verify_card(card_no: str) - dict: if not card_no or not card_no.isdigit(): return {ok: False, reason: not_digit} if not 15 len(card_no) 19: return {ok: False, reason: bad_length} if not luhn_valid(card_no): return {ok: False, reason: luhn_fail} info match_bin(card_no) if info is None: return {ok: False, reason: bin_not_found} return {ok: True, info: info}返回结构里统一带ok和reason两个字段方便前端和日志系统直接使用。reason枚举值建议固定not_digit表示含非数字字符bad_length表示长度不在正常范围luhn_fail表示校验位不合法bin_not_found表示卡号结构合法但BIN库查不到。这个错误码设计清楚以后监控告警上看到bin_not_found的曲线上升能直接定位是新增了某个发卡行的卡号段还是上游传参有问题。如果JSON里带length字段还可以把15到19的宽范围收紧到该BIN的标准长度。具体做法是在match_bin返回后加一条判断如果info里存在length且len(card_no)不等于该值按bad_length处理。这个收紧能挡掉一部分手动录卡时漏位填错的情况但前提是数据源里的length足够准确否则误杀率会很高。我的建议是先按宽范围跑一段时间观察length字段与真实卡号的吻合度再决定要不要开启。注意Luhn校验通过只代表卡号结构合法BIN匹配成功只代表发卡行标识存在真正验证卡状态必须走发卡行或卡组织网络本地离线校验挡掉的是明显不合理的流量。流水线里我还习惯打一行结构化日志当前卡号前8位加尾号4位脱敏、匹配结果、耗时毫秒数。不要打完整卡号合规上留有风险。这种格式既能做问题回溯又不碰敏感数据。日志级别用INFO生产环境默认输出排查问题时不需要临时改日志级别。4. 实操避坑BIN码匹配中我踩过的五个坑BIN码匹配看起来只有几行代码真正花时间的不是写函数而是处理数据源里那些看起来正常但经不起推敲的记录。以下五类问题是维护BIN库和对接卡数据时真实碰到的每条按现象、原因、解决记录方便对照排错。4.1 新发行的卡在库里永远查不到系统误判为非法卡现象业务方反馈某月新发的卡全被风控拦截日志里满是bin_not_found。原因BIN库是快照数据新卡发行、存量BIN回收都需要人工维护不可能实时同步。很多团队以为拿下这份JSON就一劳永逸没有设计更新机制。解决把bin_not_found当成正常业务分支而不是异常。同时建一个异步更新任务每周拉取权威BIN数据源比对版本号后替换本地文件并触发重新加载。我在生产环境里还加了一个未识别BIN上报接口匹配失败时把卡号前8位匿名上报到统计表新卡上线第二天就能从报表里看到趋势。更新时要比对文件MD5确认内容真的变化了再动内存索引import hashlib def file_md5(path): h hashlib.md5() with open(path, rb) as f: for chunk in iter(lambda: f.read(65536), b): h.update(chunk) return h.hexdigest()这段MD5计算用分块读取避免一次性把大文件载入内存。参数说明chunk大小65536字节是IO经验值读小文件也不会有明显开销返回的hexdigest字符串和上次记录比对相等就跳过加载不相等再走load_bin_db。这个习惯能避免文件被替换了但version忘了改导致的内存索引不更新问题。4.2 发卡行匹配正确但卡组织判断出错现象卡号是62开头库里返回的发卡行正确brand却被标记为Visa。原因双标卡时代的交叉发卡问题。同一张卡可能同时带银联和Visa标志不同数据源对brand的理解不一致有的记主标有的记双标。解决以库里的brand字段为权威不要在匹配逻辑里硬编码62开头就是UnionPay这种规则。遇到计算规则和库不一致记录日志交给运营人工核对而不是在代码里修正。你永远不知道下一次发卡行的规则调整会把区间改成什么样代码里每一条硬编码规则都是一颗隐性地雷。4.3 卡号只有10位匹配函数照样跑返回空字典现象测试环境传了一个10位卡号match_bin返回None正常走到了bin_not_found但日志里看不到任何参数痕迹排查半天才发现接口文档写的是16位。原因匹配函数内部只做了小于6位返回None的保护没有对传入参数的完整性和长度做统一检查短卡号被静默处理掉调用方以为传参没有问题。解决把verify_card里的先长度后匹配作为所有入口的唯一通道禁止绕过它直接调match_bin。如果match_bin设计成公共函数入口也补一道长度校验并在日志里输出脱敏后的前6位def match_bin_safe(card_no: str): if not card_no or not card_no.isdigit() or len(card_no) 6: logging.warning(match_bin_safe: 入参异常, 长度%s, len(card_no)) return None b8 card_no[:8] if b8 in BIN_INDEX_8: return BIN_INDEX_8[b8] b6 card_no[:6] return BIN_INDEX_6.get(b6)入参是空串时len(card_no)的日志也能正常打印不会在日志函数本身再抛一次异常。从那以后每新增一个处理函数我都先问自己一句如果传错长度它会不会静默失败答案如果是会就在入口直接拦下来。4.4 并发启动时文件句柄被打满服务直接翻车现象生产环境多个实例同时启动日志突然出现too many open files进程陷入重启循环。原因加载逻辑被放在了一个会被多次调用的初始化方法里每个线程都去open同一个JSON文件。文件虽然小但并发一高文件描述符就超限了。解决确保加载只发生一次把它放到进程启动流程的单例入口。如果一定有多线程初始化加上线程锁import threading _load_lock threading.Lock() def safe_load_once(path): with _load_lock: load_bin_db(path)锁的作用是让同一时刻只有一个线程执行加载逻辑其他线程等锁释放后进入看到CURRENT_VERSION已更新就直接跳过。不要把open文件的动作放在每次请求里做这属于典型的设计错误。虽然load_bin_db内部逻辑上不会反复open但如果初始化入口被并发调用还是会出现大量文件描述符占用所以保险起见我都会加锁。4.5 同一个BIN出现两条记录字段互相矛盾现象跑报表时发现同一个62开头的BIN一条记录写debit另一条写credit发卡行名字还不同。原因数据来源合并时去重不严格或者维护人员误录了多行。JSON本身没有数据库那样的唯一约束重复BIN不会报错。解决加载阶段对bin做唯一性校验发现重复时按规则保留。我选择的规则是异常时宁可跳过也不随便选一条因为选错卡类型在清结算里可能造成资金方向错误。具体做法是在加载循环里加一个seen集合重复时就写入冲突日志同时把该BIN标记为manual_review并跳过入索引。这样报表里能看到冲突量运营能及时核对修正而线上不会因为一条坏数据干扰正常匹配。5. 进阶让BIN码表更可控的两个习惯5.1 给BIN库加版本号和生效时间拿到新的BIN库文件后我习惯先看一眼最外层有没有version和effective_date字段。没有的话自己加一层包装{ version: 2025-06, effective_date: 2025-06-01, data: [] }加载时把version存进内存每次请求可以在响应头里带回一个X-Bin-Version方便链路排查。增量更新时不需要全表替换记录两个版本之间的diff文件加载时按diff合并也能做但合并顺序很难控制建议直接全量替换更可靠。还有一个实用习惯在测试环境里保存一份手工构造卡号集包含6位老BIN、8位新IIN、借记卡、信用卡、双标卡各若干张每次更新BIN库后先把这批卡号跑一遍验证再上生产。这相当于给数据源做了一次回归测试能提前暴露兼容性变化。5.2 未识别BIN的降级策略当匹配结果为空时不要把请求直接判死。我常用的降级顺序是先返回unknown让业务继续走人工审核而不是直接拒绝同时把匿名BIN写进本地统计表等积累到一定数量再批量核对补充进自定义库。降级策略要写成配置而不是代码方便运营随时调整阈值。真实案例是某次新增卡种上线第一周unknown率从0.2%涨到3%通过统计表识别出了新BIN段及时补录后才降回正常。如果当初直接拒绝那批卡的用户体验就全毁了。有一次上线前忘了更新BIN库结果大量新卡在联调阶段被误判排查了两个小时才发现是version没变加载函数认为数据没有更新跳过了重载。从那以后我每次接手新环境都强制走一遍版本号、文件MD5、手工卡号集三项检查再开始调接口。BIN码数据库看着简单但把它当成静态资源胡乱一放总有一天会在线上咬你一口。希望帮到你。本文还有配套的精品资源点击获取