银行卡BIN数据实战:从Excel清洗到MySQL查询与风控应用 简介这套银行卡BIN数据基于银联官方2020年4月25日发布的最新版本整理共涵盖9868条记录字段包含银行卡bin、bin长度、发卡行、银行卡名、银行卡类型、银行卡长度等核心信息适合支付系统开发、金融风控、账户校验、渠道对账等场景直接使用免去自行收集与清洗的繁琐流程。压缩包共7个文件整体大小约1.12MB其中5个xls按卡表分类非标卡、农民工卡、跨行转账卡、单位结算卡及标准卡表便于分类查阅和对比另将Excel数据整理为sql文件导入MySQL即可完成建库配合官方字段说明能快速搭建本地银行卡BIN库.DS_Store为系统冗余文件可忽略。目前已有3490人学习下载数据来自银联官方发布权威性和完整性均有保障尤其适合对接银联规范、需要定期同步卡表或开展银行卡识别类业务的开发者与测试人员使用。1. 银行卡 BIN 数据不只是查发卡行更是一张支付路由表绝大多数人拿到一份银行卡 BIN 表第一反应是“用来查一下这张卡是哪家银行的”。确实这是 BIN 数据最基础的功能但把它当作一张静态的“银行对照表”来用等于把一个完整的支付基础设施降级成了一本电话簿。银行卡 BINBank Identification Number是卡号前 6 位它承载的信息远比很多人想象的多卡组织银联、Visa、Mastercard、卡种借记卡、贷记卡、准贷记卡、发卡行、卡号长度、甚至卡片的有效期规则。这份 2020 年银联官方发布的 Excel MySQL 双格式 BIN 数据适合支付系统开发者、风控策略工程师、数据分析师以及对账系统维护者。它能解决的不只是“这张卡是谁发的”而是“这张卡在支付链路里该怎么走、该走哪条结算通道、该匹配哪条风控规则”。2. BIN 数据的内在结构先把字段含义和卡组织规律弄透2.1 从卡号前 6 位能拆出什么机构号、卡种、借贷记标识BIN 的全称是 Bank Identification Number在 ISO/IEC 7812 标准里它被定义为主行业识别符Major Industry IdentifierMII 发卡机构标识。卡号的前 1 位是 MII前 2 到 6 位是发卡机构标识而中国银联的 BIN 范围是 62 开头这是国际标准组织分配给银联的专属前缀。所以当你在代码里看到card_number.startswith(62)理论上它就应该是银联卡但现实里有个细节62 开头不一定是银联标准卡也可能是双标卡或部分地方性银行发行的特殊卡种这正是 BIN 表的价值所在——它把这种“理论上”“大概率”变成了精确匹配。一份完整的银联 BIN 记录通常包含以下核心字段卡 BIN6 位数字、卡名如“银联标准金卡”、卡种借记卡/贷记卡/准贷记卡、发卡行代码、发卡行名称、卡号长度、卡组织标识。这里的“卡号长度”很关键银联卡标准长度是 16 到 19 位但 16 位和 19 位的卡在部分支付通道里的处理逻辑不一样比如某些老旧的收单系统只认 16 位遇到 19 位会直接报错拒收。风控系统里判断借贷记也有讲究贷记卡信用卡和借记卡储蓄卡在交易限额、计费方式、清算路径上完全不同如果用错了卡种标签轻则扣费异常重则触发风控误判。我一般会先用 Python 把这份 Excel 读进来做一次全量扫描目的不是“看看有多少行”而是先确认字段有没有空值、有没有重复 BIN、卡种列有没有脏数据比如“借计卡”这种错别字。这一步是后面所有工作的地基地基歪了后面建的房子全是斜的。2.2 卡组织前缀规律与发卡行代码的对应关系卡组织前缀是有层级规律的不只是“62 开头是银联”这么简单。银联分配给各家银行的 BIN 段也不是随机分配的常规的 62 开头 BIN 段发卡行代码通常 3 到 4 位在 BIN 里并不直接体现它需要靠 BIN 表里的映射列去关联。举个例子同样是以622200开头的卡可能是某国有大行发行的借记卡也可能是另一家股份制银行的联名卡光靠区间猜测完全不靠谱这就是为什么支付系统里必须有一张本地 BIN 表而不是写死几个 if 判断。发卡行代码这块我要多提醒一句银联官方数据里的“发卡行代码”和实际清算系统里的“联行号”不是一回事。联行号是央行支付系统用的 12 位数字发卡行代码是卡组织内部标识两者做映射时不能想当然按名称匹配最好用官方给的那一列直接关联。我在做一次跨行代付接口对接时就见过某同事拿发卡行名称去匹配联行号结果名字里带“分行”的全都匹配不上最后在代码里写了两层映射表才救回来。2.3 数据预处理把 Excel 清洗成可入库的标准 CSV拿到 Excel 版 BIN 数据后第一步不是急着导入 MySQL而是先做清洗和标准化。银联官方发布的 Excel 通常包含多个 sheet有的 sheet 是说明文档有的 sheet 是数据明细写导入脚本时要先指定 sheet 名别让 pandas 默认把第一个 sheet 读进来就当成数据。清洗的标准步骤我一般这么走先去除全空白行和全空白列再处理表头合并单元格官方表格的表头经常是两行结构然后统一字段名最后检查 BIN 列的数值类型。这里有个高频坑BIN 是 6 位数字但 Excel 里它可能是文本格式也可能是数字格式如果是数字格式622848这种 BIN 没问题但像003456这种以 0 开头的 BIN 会被 Excel 自动吃掉前面的 0变成3456这会导致 BIN 长度不足 6 位直接破坏匹配逻辑。import pandas as pd # 读取指定 sheetheader1 表示跳过第一行说明把第二行作为表头 df pd.read_excel( 银联BIN数据_2020.xlsx, sheet_nameBIN明细, header1, dtype{卡BIN: str, 发卡行代码: str}, ) # 删除全空行和全空列 df.dropna(howall, inplaceTrue) df.dropna(axis1, howall, inplaceTrue) # 统一列名去掉空格和特殊字符 df.columns [str(col).strip().replace(\n, ).replace( , ) for col in df.columns] # 卡BIN列补零对齐到6位防止前导零丢失 df[卡BIN] df[卡BIN].str.zfill(6) # 检查BIN是否都是6位数字 invalid_bin df[~df[卡BIN].str.match(r^\d{6}$)] print(f非6位数字的BIN数量: {len(invalid_bin)}) if not invalid_bin.empty: print(invalid_bin.head()) # 输出清洗后的CSV指定utf-8-sig防止Excel打开乱码 df.to_csv(bin_cleaned.csv, indexFalse, encodingutf-8-sig)这段脚本做了四件关键事第一dtype{卡BIN: str}强制把 BIN 列当成字符串读取防止前导零在读取阶段就被吞掉第二header1根据官方表格的实际结构跳过说明行第三列名清洗是为了解决表头里可能有换行符或空格的问题这问题在 Excel 表格里极常见第四str.zfill(6)是后悔药哪怕前面读进来的是数字格式也能强行补回 6 位。如果跑完这段脚本发现invalid_bin数量不为 0不要急着删行先用head()看看具体内容——有的是因为卡 BIN 列里混入了注释文本有的是表格里有多余的汇总行这些需要手动确认后单独处理。3. 导入 MySQL建表、LOAD DATA 与索引设计的取舍3.1 建表 DDL字段类型选错是万恶之源MySQL 里存放银行卡 BIN 数据建表是第一关也是后面几乎所有坑的源头。卡 BIN 字段我强烈建议用CHAR(6)而不是INT原因很直接BIN 是定长数字串用 INT 会丢掉前导零而且后续查询时你还要在代码里补零或者让 MySQL 做隐式类型转换这笔额外开销完全没有必要。发卡行代码如果是纯数字且长度固定可以用CHAR如果发卡行代码是数字但长度不固定有的 3 位有的 4 位建议用VARCHAR(8)来容错。卡号长度字段要注意它是一段区间还是一个单一值我见过有些 BIN 表里“卡号长度”写成16-19这种就不能用TINYINT得用VARCHAR(5)存成字符串或者拆成min_len和max_len两列。拆列的好处是查询时可以直接用整数比较不用在代码里解析字符串区间这一点在后续做“根据卡号长度校验”时会非常方便。CREATE TABLE IF NOT EXISTS card_bin ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 自增主键, bin CHAR(6) NOT NULL COMMENT 银行卡BIN前6位, card_name VARCHAR(100) DEFAULT NULL COMMENT 卡名称, card_type TINYINT NOT NULL DEFAULT 0 COMMENT 卡种1-借记卡 2-贷记卡 3-准贷记卡, bank_code VARCHAR(8) DEFAULT NULL COMMENT 发卡行代码, bank_name VARCHAR(100) DEFAULT NULL COMMENT 发卡行名称, card_len_min TINYINT UNSIGNED DEFAULT 16 COMMENT 卡号最短长度, card_len_max TINYINT UNSIGNED DEFAULT 19 COMMENT 卡号最长长度, card_org CHAR(8) DEFAULT CUP COMMENT 卡组织CUP-银联, data_version VARCHAR(16) DEFAULT 2020 COMMENT 数据版本标识, UNIQUE KEY uk_bin_version (bin, data_version), KEY idx_bank_code (bank_code), KEY idx_bin (bin) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT银联银行卡BIN数据表;这张表的设计有五个值得说的地方。第一UNIQUE KEY uk_bin_version (bin, data_version)是防止同一版数据重复导入的兜底约束有了它LOAD DATA 重复执行时要么直接报错要么用IGNORE跳过不会产生重复行。第二card_type用 TINYINT 而不是字符串目的是查询时用数字比较效率更高风控引擎里直接card_type 1就能筛出借记卡不用做字符串匹配。第三card_len_min和card_len_max拆成两列对应前面说的区间问题这样后面做卡号长度校验时直接card_number_len BETWEEN card_len_min AND card_len_maxMySQL 还能走索引。第四data_version字段非常重要——BIN 数据会不定期更新有了版本号后续导入新数据时两条记录的 BIN 相同但版本不同可以同时存在做历史对比时直接按版本过滤就行。第五bank_name我冗余了一列虽然关联一张银行维表更符合范式但在这种低数据量、高频查询的场景下冗余省一次 JOIN 的收益远大于存储成本。3.2 导入数据LOAD DATA 与参数陷阱清洗完 CSV 之后导入 MySQL 用LOAD DATA LOCAL INFILE比逐条 INSERT 快好几个数量级但有几个参数陷阱必须提前堵住。第一个陷阱是字符集。CSV 文件如果是utf-8-sig编码带 BOMMySQL 5.7 里LOAD DATA时如果不指定CHARACTER SETBOM 会被当成一个不可见字符写进第一行第一个字段导致第一条记录的 bin 变成\ufeff622848而这条数据在唯一索引冲突检测时又是合法的结果就是表里出现一条“看不见”的脏数据。解决办法是导入时指定CHARACTER SET utf8mb4并且把 CSV 存成不带 BOM 的纯utf-8。第二个陷阱是本地文件权限。MySQL 8.0 默认local_infile是关闭的直接执行 LOAD DATA 会报Loading local data is disabled错误需要先执行SET GLOBAL local_infile ON;然后连接 MySQL 时加上--local-infile1参数。这是环境层面的原因网上很多教程没提这一步新手最容易在这里卡住。mysql -u root -p --local-infile1 mydatabase LOAD DATA LOCAL INFILE /path/to/bin_cleaned.csv INTO TABLE card_bin CHARACTER SET utf8mb4 FIELDS TERMINATED BY , OPTIONALLY ENCLOSED BY LINES TERMINATED BY \n IGNORE 1 LINES (bin, card_name, card_type, bank_code, bank_name, card_len_min, card_len_max) SET data_version 2020;这段命令的执行逻辑是跳过 CSV 第一行表头按逗号分隔字段双引号包裹的内容被识别为字符串最后一列data_version不由 CSV 提供而是通过SET子句统一写成2020。这样做的妙处在于CSV 里不需要有版本号这一列导入脚本自动打标签后续换新版本数据时只需要改SET data_version 2021再跑一次导入即可。OPTIONALLY ENCLOSED BY 这个参数建议保留因为 CSV 里如果卡名里有逗号比如“银联标准卡,金卡”没有这个参数就会被错误地拆成两个字段。导入完成后跑一条验证语句如果结果显示的行数和 CSV 里的行数对不上优先检查有没有数据被唯一索引拦掉重复导入的情况以及card_type列里是不是有 CSV 里没清洗干净的非数字值LOAD DATA 遇到非数字会给 0 并在 warning 里提示不会直接报错。3.3 查询性能全表匹配的代价与索引优化策略BIN 查询的核心场景是“给定一个完整卡号找出对应的 BIN 记录”所以查询逻辑通常是LEFT(卡号, 6)去匹配bin列。这个逻辑看似简单但如果你在应用层先在代码里截取前 6 位再去查MySQL 已经知道一个字面值走uk_bin_version索引直接命中速度极快。麻烦的是那种在 SQL 里直接写的写法SELECT * FROM card_bin WHERE bin LEFT(6228481234567890, 6) AND data_version 2020;这条 SQL 其实也能用上索引因为LEFT()函数作用于常量值MySQL 优化器会在执行阶段先计算出LEFT(6228481234567890, 6)的结果再走索引。但如果你写成WHERE LEFT(bin, 6) 622848这就废了——索引失效全表扫描虽然这张表量级不大银联 BIN 总数在几万级别全表扫描其实也只要几十毫秒但在高频风控调用场景每秒几百次查询下几十毫秒累积起来就能拖垮接口的 P99 延迟。关于索引设计还有一点取舍要说UNIQUE KEY uk_bin_version (bin, data_version)已经覆盖了等值查询bin ? AND data_version ?所以单独的idx_bin其实是冗余的。保留它的原因只有一个如果某些查询场景只需要bin一个过滤条件比如“查全球所有卡组织所有版本的 BIN”单独索引更高效。但在 2020 版银联数据这个范围内我通常会删掉idx_bin让唯一索引一肩挑减少索引维护开销。表小的时候这种优化收益不明显但养成这种习惯能避免以后导入上百万行的大表时被索引拖慢写入速度。4. 避坑指南导入与查询中常见的五个翻车现场4.1 前导零丢失622848 查得到003456 查不到现象导入后发现部分 6 位 BIN 永远匹配不到比如 CSV 里明明是003456查出来却匹配的是3456或者直接查无此条。原因CSV 里该列存的是数字格式pandas 读 Excel 时即使指定了dtypestr如果原始 Excel 单元格本身就是数字格式pandas 读出来的值可能是3456而不是003456因为 Excel 的数字格式已经把前导零吃掉了。解决不要指望在 pandas 层面补回丢失的零要从源头解决——在 Excel 里把 BIN 列的单元格格式改成文本或者导 CSV 时在 BIN 列前面加一个制表符或单引号强制文本格式。如果数据已经丢了只能靠 BIN 表里其他关联字段比如卡名去反推补全这种活非常痛苦。4.2 乱码不是 MySQL 的锅是 CSV 的 BOM 在搞鬼现象导入后查询bank_name字段发现每条记录开头都有一个\ufeff或乱码符号明明 CSV 在记事本里看着是正常的。原因CSV 文件用utf-8-sig编码保存头部带了 BOMByte Order MarkMySQL 的LOAD DATA ... CHARACTER SET utf8mb4不会主动帮你剥离 BOM它会把这个 3 字节的 BOM 当成第一行的第一个字符处理掉——注意这里说的是第一行第一行的第一个字段会变成\ufeff622848这种带隐藏字符的值。解决保存 CSV 时用纯utf-8编码或者在导入前用sed -i 1s/^\xef\xbb\xbf// bin_cleaned.csv去掉 BOM。我个人习惯在清洗脚本里直接保存成encodingutf-8一步到位特别急的时候用codecs.BOM_UTF8在 Python 里手动剥离。4.3 卡号长度是“16-19”字符串TINYINT 直接报错现象DDL 里把card_len定义成TINYINTLOAD DATA 导入时直接报Data truncated或导入后该列全是 0。原因官方 BIN 表里的“卡号长度”列是字符串描述比如16-19或者16,18,19一个字段里塞了多个值TINYINT 根本接不住。解决按前面 DDL 的设计拆成card_len_min和card_len_max清洗脚本里把16-19用split(-)拆开分别给两列赋值。如果遇到16,18,19这种不连续的列表就只能取最小值和最大值宁宽勿严因为风控校验时宽进严出比严进宽出安全。4.4 同一个 BIN 对应多条记录JOIN 出来的数据直接翻倍现象用bin去关联交易流水表发现关联后的行数比交易流水多了一倍排查半天发现 BIN 表里同一个 BIN 在card_name不同时存在多条记录比如“银联标准金卡”和“银联标准白金卡”共用一个 BIN。原因这是真实存在的——同一家银行同一张卡种可能有多个子产品BIN 一样卡名不一样。银联官方 BIN 表里这种情况并不是错误设计是产品维度的一种表达方式。解决业务上需要“一卡一规则”时给查询语句加GROUP BY bin或者ORDER BY id DESC LIMIT 1取最新一条或指定优先级。严格的做法是建表时去掉card_name字段只保留bin、card_type、bank_code这些唯一性强的属性产品名称丢到附属表里去关联这样主表就是天然的 BIN 维表了。4.5 查询时对 BIN 列动函数索引当场失效现象同样的查询语句有时毫秒级返回有时几百毫秒DBA 一看执行计划发现走了全表扫描。原因应用层代码里写了WHERE LEFT(bin, 6) SUBSTRING(622848..., 1, 6)这种写法把索引列当成了函数参数MySQL 无法对函数处理过的列走索引除非你建了函数索引MySQL 8.0 支持但一般不会这么用。解决查询条件改为WHERE bin LEFT(6228481234567890, 6)让 BIN 列保持裸列状态函数只作用在常量值上索引就能正常命中。这一点在接入 ORM 框架时特别容易踩因为 ORM 生成的 SQL 有时会对字段做隐式转换。提示BIN 数据导入后第一件该做的事是统计SELECT COUNT(DISTINCT bin)和SELECT COUNT(*)两个数如果差值大于预期说明存在一卡多记录需要在应用层约定一条“最近优先”的取数规则。5. 进阶应用把静态 BIN 表变成本地查询服务与风控规则引擎5.1 本地 BIN 查询服务内存索引与冷热数据分离数据量几万行的 BIN 表直接用 MySQL 查询也能扛得住常规业务量但把它做成一个本地进程内缓存服务能让接口延迟从毫秒级降到微秒级。这里我用的方案是 Python bisect模块建立有序索引配合一个运行时字典做精确匹配。思路是这样的全量 BIN 数据加载到内存里用 BIN 的字符串作为 key记录在字典里直接精确定位。但字典的问题在于“近似查询”——如果你拿到的卡号不足 6 位比如某些预授权接口只传前 4 位字典就无能为力。所以我会额外维护一个按 BIN 排序的列表用bisect做范围查找容忍长度不足时的模糊匹配。import bisect import json # 加载清洗后的数据为有序列表每个元素是 (bin, record_dict) class BinService: def __init__(self, data_list): # data_list: list of (bin_str, record_dict) self.bin_list sorted([item[0] for item in data_list]) self.record_map {item[0]: item[1] for item in data_list} def exact_query(self, card_number): 精确匹配卡号前6位查BIN bin_key card_number[:6] return self.record_map.get(bin_key) def fuzzy_query(self, card_prefix): 模糊匹配只给4-5位前缀时返回所有以该前缀开头的BIN记录 left bisect.bisect_left(self.bin_list, card_prefix) right bisect.bisect_right(self.bin_list, card_prefix \uffff) return [self.record_map[self.bin_list[i]] for i in range(left, right)] # 从MySQL全量加载数据示例代码省略连接细节 # data load_from_mysql(SELECT bin, card_name, card_type FROM card_bin WHERE data_version2020) # svc BinService(data) # 查询示例 # record svc.exact_query(6228481234567890) # if not record: # print(卡BIN未收录可能为新发卡或非银联卡)这套实现的取舍点有三个。第一exact_query是 O(1) 的字典查询适合风控引擎里对每一笔交易做实时判断第二fuzzy_query用bisect做有序列表的区间切片适合人工排查或后台批处理场景比如拿到一批残缺卡号想猜出完整 BIN 范围第三冷热分离体现在加载策略上——热数据最近 30 天有交易的 BIN加载到内存常驻冷数据历史遗留 BIN放在 MySQL 里按需查避免几万条记录全在内存里浪费空间。这里要特别说明bisect_right的边界是card_prefix \uffff这个 \uffff 是不可见字符但它的排序码比任何可见字符都大可以保证向右查找时不会漏掉以该前缀开头的所有 BIN。这个技巧我在别的模糊查询场景也用过比LIKE prefix%快得多。5.2 借贷记判定与交易限额差异化BIN 表驱动风控策略BIN 表在风控系统里的一个大用处是借贷记自动判别。很多风控规则对信用卡和借记卡的限额是分开的比如“单笔交易超过 5000 元就拦截”这条规则只对借记卡生效因为信用卡走的是信用额度风控逻辑完全不同。在交易流水接入时实时从 BIN 表里取出card_type然后决定走哪一组风控规则。常见的做法是在交易请求入口加一个中间层用 BIN 查询的结果填充交易上下文的字段。伪代码逻辑如下# 假设从支付网关拿到的raw_txn包含card_number card_bin svc.exact_query(raw_txn[card_number]) if card_bin is None: # 未匹配到BIN走保守策略按最高风险级别触发人工审核 risk_level HIGH else: if card_bin[card_type] 1: # 借记卡 risk_level MEDIUM if raw_txn[amount] 5000 else LOW elif card_bin[card_type] 2: # 贷记卡 risk_level LOW if raw_txn[amount] 20000 else HIGH else: risk_level HIGH这里的关键不是 if-else 本身而是它把“卡片属性”从“交易数据”中解耦了——风控规则不关心卡号长什么样只关心 BIN 表给出的标签是对是错。所以说这份 BIN 数据的质量直接决定了风控策略的准确率如果 BIN 表里card_type标签标注错误一条本应被放行的信用卡交易可能被当成借记卡触发限额拦截用户就会反馈“为什么我的卡刷不了”。银联官方发布的这份数据在发卡行名称和卡种标注上已经做了清洗但借贷记字段在各个历史版本里偶尔有错位导入后最好做一次抽样验证。5.3 对账系统里的 BIN 维表应用从交易流水反查发卡机构对账系统里的一个典型场景是月末需要对所有交易按发卡行分组统计手续费支出。交易流水里往往只有卡号没有发卡行名称这时候 BIN 表就成了唯一的关联键。但这里有一个效率问题如果对账 SQL 直接对千万级流水表执行LEFT JOIN card_bin ON LEFT(txn.card_no, 6) card_bin.bin这个 JOIN 会在全表范围做一次字符串截取和匹配性能极差。我通常的做法是先把流水里的卡号前 6 位提取出来放到一张临时表再用临时表去和card_bin关联这样能把字符串函数的开销控制在百万行级别而不是千万行级别。更进一步的优化是把card_bin表加载成内存临时表MySQL 的HEAP引擎但考虑到 BIN 表数据量不大普通 InnoDB 也够用重点是避免对大表做函数运算。这条血泪经验来自一次实际经历某个月对账跑了三个小时没跑完加了临时表后三分钟出结果差距就是这么明显。6. 数据验证与版本管理三层校验法和一个自检脚本的习惯数据导入后不是万事大吉BIN 表的质量验证需要单独走一遍。我习惯做三层校验第一层是数量校验对比源文件行数和数据库行数差异必须为 0第二层是抽样校验从表里随机取 20 条 BIN到银联在线查询页面或发卡行的公开接口交叉核实卡名、卡种和发卡行是否一致第三层是业务回归校验——拿一批真实的测试卡号跑查询服务确认能返回预期结果同时记录未命中的卡号并分析原因是 BIN 确实没收录还是查询逻辑有 bug。版本管理方面每次导入新数据都用data_version字段区分不建议直接 UPDATE 覆盖旧数据——BIN 数据的变更有时是改卡名不改 BIN有时是新增 BIN 段保留历史版本可以随时对比差异写一个对比查询就能精确看到哪些 BIN 是新增的、哪些卡名被修改了。从那以后我每次接新的支付项目都会把这份 BIN 数据导入、三层校验、写一个自检脚本挂在部署流程里强制跑一遍。数据这东西用之前验一次比用完之后发现问题再回头查要踏实得多。希望这份 2020 银联官方 BIN 数据能帮你把支付链路的地基打牢少一点“为什么这张卡查不出来”的深夜排查。本文还有配套的精品资源点击获取