ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

银行卡BIN数据从Excel到MySQL:建表、清洗、导入与查询优化实战

银行卡BIN数据从Excel到MySQL:建表、清洗、导入与查询优化实战

简介:这份资源是银联官方于2020年4月25日发布的银行卡BIN数据集合,面向支付系统开发、风控建模、金融数据分析等场景的开发者与研究人员,用于卡号识别、发卡行匹配及卡类型校验等业务需求。压缩包共7个文件,以xls表格与sql脚本为主,另含少量系统隐藏文件,整体约1.12MB,其中sql文件已整理为9868条记录,可直接导入MySQL使用,省去手工清洗环节。数据字段覆盖银行卡BIN、BIN长度、发卡行、银行卡名称、银行卡类型及卡号长度等维度,并区分标准卡表、农民工卡表、跨行转账卡表、单位结算卡卡表等类别,便于按业务场景筛选调用。目前已有3491人学习下载,适合需要权威、齐全BIN库的支付与数据从业者直接取用。

1. 银行卡BIN数据落地:从Excel到MySQL,我踩过的那些坑

做支付风控、收单路由或者账务清结算的同学,大概率都绕不开一个东西——银行卡BIN数据。所谓BIN,就是卡号前6到8位,它决定了这张卡的发卡行、卡种(借记卡/贷记卡)、卡组织(银联/visa等)以及大致的地理归属。标题里提到的“银行卡bin数据(Excel+MySQL)-2020最新最全-银联官方发布”,本质上就是一份以Excel为载体、最终要落到MySQL里做实时查询的BIN基础库。它解决的核心问题是:当一笔交易进来,你只有一串卡号,怎么在毫秒级判断出它该走哪条路由、该不该拦截、手续费怎么算。

我最早接触这块是在一个收单系统里,当时用的是一个几百行的Excel手工维护,结果某次营销活动期间,一批新发的乡村振兴卡BIN没录进去,导致路由全走错,那叫一个血泪教训。后来才明白,BIN数据不是“有就行”,而是“全、准、快”三件事缺一不可。这篇文章适合两类人:一是刚接手支付系统、需要快速搭建BIN查询服务的新手;二是已经在用Excel但被性能和数据更新折磨的熟手。我会把从Excel清洗到MySQL建表、导入、查询优化的完整路径拆开讲,参数怎么设、坑在哪,都给你说明白。

2. 先搞懂BIN数据的字段结构和来源逻辑

2.1 一条BIN记录到底该有哪些字段

很多人拿到Excel第一反应是“卡号前6位+银行名”就完事了,真到用的时候才发现不够。一个能支撑生产查询的BIN表,至少需要这些字段:BIN号(6-8位)、卡组织标识、发卡行名称、发卡行简称、卡种(借记/贷记/准贷记)、卡品牌(如银联标准卡、visa经典卡)、卡号长度、校验规则(是否Luhn)、以及生效状态和更新时间。其中卡号长度和校验规则经常被忽略,但它们在卡号合法性预校验阶段能挡掉大量脏数据。

我一般会把BIN号存成字符串而不是整数,因为有些BIN以0开头,存整数会丢前导零,这个坑新手几乎必踩。另外发卡行名称和简称要分开,简称用于前端展示,全称用于对账文件匹配。卡组织标识建议用枚举值而不是中文,比如UNIONPAY、VISA、MASTERCARD,这样后续做路由规则时不用做字符串匹配。

2.2 银联官方发布的数据和第三方聚合数据的差异

标题里强调“银联官方发布”,这点很关键。银联官方发布的BIN数据通常以Excel或CSV形式按批次更新,字段规范、覆盖国内卡最全,但更新频率不固定,可能是季度或半年。第三方聚合数据(比如一些支付服务商整理的)更新快、覆盖境外卡多,但字段口径不统一,经常出现同一家银行多个名称的情况。

我的做法是:以官方数据为主干,第三方数据做补充,但补充时一定要做归一化。比如“中国工商银行”和“工行”要映射到同一个机构编码。归一化表可以单独建一张bank_alias表,用别名去关联主表。这样既保证了权威性,又不会因为第三方数据字段混乱而污染主库。

2.3 为什么最终要落到MySQL而不是继续用Excel

Excel做BIN查询的极限大概在几千行、每天几百次查询,再往上就撑不住了。MySQL的优势在于:支持索引、支持并发、支持事务更新,而且可以和你的业务系统在同一个数据库实例里做JOIN。更重要的是,BIN数据需要频繁更新(新卡种、新发卡行),MySQL可以用INSERT ... ON DUPLICATE KEY UPDATE做增量合并,而Excel每次都要全量替换,容易出错。

但也不是说Excel没用。Excel适合做数据清洗和人工核对,我通常会在Excel里用公式把官方数据和第三方数据做一次比对,标记出差异行,人工确认后再导入MySQL。这个“Excel清洗+MySQL查询”的组合,是目前性价比最高的方案。

3. 从Excel到MySQL:建表、清洗、导入的完整操作

3.1 建一张能扛住生产查询的BIN表

先看建表语句。我一般会这样设计:

CREATE TABLE `bank_bin` ( `id` int NOT NULL AUTO_INCREMENT, `bin` varchar(8) NOT NULL COMMENT '卡号前6-8位', `org_code` varchar(16) NOT NULL COMMENT '卡组织编码,如UNIONPAY', `bank_name` varchar(64) NOT NULL COMMENT '发卡行全称', `bank_short` varchar(32) DEFAULT NULL COMMENT '发卡行简称', `card_type` tinyint NOT NULL DEFAULT '0' COMMENT '0未知 1借记 2贷记 3准贷记', `card_brand` varchar(32) DEFAULT NULL COMMENT '卡品牌', `card_len` tinyint DEFAULT NULL COMMENT '卡号长度', `luhn_check` tinyint NOT NULL DEFAULT '1' COMMENT '是否校验Luhn 1是 0否', `status` tinyint NOT NULL DEFAULT '1' COMMENT '1有效 0失效', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_bin` (`bin`), KEY `idx_bank` (`bank_name`), KEY `idx_org` (`org_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='银行卡BIN基础表';

这里有几个参数值得说。bin字段用varchar(8)而不是char(6),因为现在很多卡是8位BIN,留足空间。唯一索引uk_bin保证同一个BIN不会重复插入,配合后面的ON DUPLICATE KEY UPDATE就能做增量更新。card_type用tinyint而不是枚举字符串,查询时比较快。luhn_check默认1,因为绝大多数银联卡都走Luhn校验,少数境外卡可能不走,需要单独标记。

注意:字符集一定要用utf8mb4,有些银行全称里带生僻字,用utf8会插入失败。

3.2 用Python清洗Excel里的脏数据

官方Excel虽然规范,但直接导入还是会遇到空行、合并单元格、BIN号带空格等问题。我一般用pandas做一次清洗:

import pandas as pd # 读取Excel,指定sheet名和表头行 df = pd.read_excel('unionpay_bin_2020.xlsx', sheet_name='BIN', header=2) # 去掉完全为空的行 df = df.dropna(how='all') # BIN号转字符串并去空格,补足6位 df['bin'] = df['BIN号'].astype(str).str.strip().str.zfill(6) # 过滤掉非数字的BIN df = df[df['bin'].str.match(r'^\d{6,8}$')] # 卡种映射 card_type_map = {'借记卡': 1, '贷记卡': 2, '准贷记卡': 3} df['card_type'] = df['卡种'].map(card_type_map).fillna(0).astype(int) # 只保留需要的列 df = df[['bin', '卡组织', '发卡行名称', '发卡行简称', 'card_type', '卡品牌', '卡号长度']] df.columns = ['bin', 'org_code', 'bank_name', 'bank_short', 'card_type', 'card_brand', 'card_len'] # 去重,按bin保留第一条 df = df.drop_duplicates(subset=['bin'], keep='first') df.to_csv('bin_cleaned.csv', index=False, encoding='utf-8-sig')

这段代码的关键点:header=2是因为官方Excel前两行往往是标题和说明,实际表头在第三行,这个要按你拿到的文件调整。zfill(6)补零很重要,有些BIN在Excel里被识别成数字,前导零丢了。drop_duplicates按bin去重,因为官方数据偶尔会有重复行。最后导出CSV时用utf-8-sig,这样用Excel打开不会乱码。

3.3 用LOAD DATA批量导入并做增量更新

清洗完的CSV导入MySQL,最快的方式是LOAD DATA LOCAL INFILE:

LOAD DATA LOCAL INFILE '/path/to/bin_cleaned.csv' INTO TABLE bank_bin CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (bin, org_code, bank_name, bank_short, card_type, card_brand, card_len) SET status = 1, luhn_check = 1, update_time = NOW();

这里IGNORE 1 LINES跳过CSV表头。SET子句给没在CSV里出现的字段赋默认值。但这是全量导入,如果表里已有数据,会因为唯一索引冲突而报错。所以增量更新要用另一套:

LOAD DATA LOCAL INFILE '/path/to/bin_cleaned.csv' INTO TABLE bank_bin CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (bin, org_code, bank_name, bank_short, card_type, card_brand, card_len) SET status = 1, luhn_check = 1, update_time = NOW() ON DUPLICATE KEY UPDATE org_code = VALUES(org_code), bank_name = VALUES(bank_name), bank_short = VALUES(bank_short), card_type = VALUES(card_type), card_brand = VALUES(card_brand), card_len = VALUES(card_len), status = 1, update_time = NOW();

ON DUPLICATE KEY UPDATE是MySQL特有的语法,当bin冲突时执行更新而不是报错。VALUES()函数取的是本次要插入的值。这样每次拿到新数据,直接跑一遍就能完成增量合并,不用先删表再重建。

注意:LOAD DATA LOCAL需要在MySQL客户端和服务端都开启local_infile,生产环境如果禁用,可以改用mysqlimport或者用Python的executemany分批插入。

4. 查询性能与数据更新:让BIN服务跑得又快又稳

4.1 精确匹配和前缀匹配的索引策略

BIN查询最常见的两种场景:一是精确匹配6位BIN,二是用卡号前8位去匹配(因为有些卡BIN是8位)。精确匹配直接走uk_bin唯一索引,速度极快。但前缀匹配就麻烦了,比如你只有6位BIN,但表里存的是8位,这时候WHERE bin LIKE '622848%'会导致索引失效。

我的做法是:查询时先取卡号前6位做精确匹配,如果没命中,再取前8位匹配。SQL可以写成:

SELECT bank_name, card_type, org_code FROM bank_bin WHERE bin = LEFT('6228481234567890', 6) OR bin = LEFT('6228481234567890', 8) LIMIT 1;

但这样OR条件可能走全表扫描。更好的方式是建一个冗余字段bin6,存卡号前6位,然后对bin6建索引。查询时先用bin6精确匹配,命中后再看bin是否匹配8位。这样能保证走索引。

4.2 用Redis做一级缓存降低MySQL压力

BIN数据的特点是读多写少,而且单条记录很小,非常适合放缓存。我一般会在MySQL前面加一层Redis,key用bin:622848,value存序列化后的JSON。查询时先查Redis,没命中再查MySQL并回写。

import redis import json import pymysql r = redis.Redis(host='localhost', port=6379, db=0) def get_bin_info(card_no): bin6 = card_no[:6] cache_key = f'bin:{bin6}' cached = r.get(cache_key) if cached: return json.loads(cached) # 查MySQL conn = pymysql.connect(host='localhost', user='root', password='', db='pay') cursor = conn.cursor(pymysql.cursors.DictCursor) cursor.execute("SELECT * FROM bank_bin WHERE bin=%s AND status=1", (bin6,)) row = cursor.fetchone() conn.close() if row: r.setex(cache_key, 3600, json.dumps(row, default=str)) return row

setex的3600秒是过期时间,BIN数据更新频率低,1小时过期足够。如果更新频繁,可以在数据导入后主动删除相关key,而不是等过期。注意json.dumps要加default=str,因为datetime类型不能直接序列化。

4.3 数据版本管理和灰度更新

BIN数据更新最怕的是“新数据有问题,旧数据已覆盖”。我一般会保留一个bank_bin_history表,每次更新前把当前数据快照进去。这样万一新数据有误,可以快速回滚。

CREATE TABLE bank_bin_history LIKE bank_bin; ALTER TABLE bank_bin_history ADD COLUMN version VARCHAR(16); INSERT INTO bank_bin_history SELECT *, '2020Q4' FROM bank_bin;

更新时先导入到临时表bank_bin_temp,核对无误后再用RENAME TABLE原子替换。这样对线上查询几乎无感知。灰度更新则是先更新一台从库,观察查询错误率,没问题再推全量。

5. 避坑指南:BIN数据落地时最容易翻车的5个地方

5.1 BIN号前导零丢失导致查询不到

现象:明明Excel里有“012345”这个BIN,导入MySQL后查不到。原因:Excel把BIN列识别成数字,前导零被自动去掉,导入时变成“12345”。解决:在Excel里先把BIN列格式设为文本,或者用Python读取时指定dtype={'BIN号': str},导入前统一zfill(6)。

5.2 卡种映射错误导致路由走错

现象:一笔借记卡交易被当成贷记卡,走了不同的手续费规则。原因:官方Excel里卡种字段可能是“借记卡”“储蓄卡”“Debit”混用,映射表没覆盖全。解决:建一张card_type_map表,把所有可能的写法都列进去,映射不到的统一标记为0(未知),人工复核后再更新。

5.3 唯一索引冲突导致批量导入中断

现象:LOAD DATA跑到一半报Duplicate entry,后面的数据全没进去。原因:CSV里有重复BIN,或者表里已有相同BIN。解决:导入前用drop_duplicates去重,导入时用ON DUPLICATE KEY UPDATE而不是INSERT IGNORE,因为后者会静默丢弃冲突行,你根本不知道哪些没进去。

5.4 字符集不匹配导致银行名乱码

现象:查询出来的银行名是“中国工商银行”变成“涓浗宸ュ晢閾惰”。原因:CSV是GBK编码,MySQL表是utf8mb4,导入时没指定字符集。解决:LOAD DATA时加CHARACTER SET gbk,或者导入前用Python统一转成utf-8。最稳妥的是建表时就指定utf8mb4,导入时也指定utf8mb4。

5.5 缓存和数据库不一致导致查询结果漂移

现象:更新了BIN数据,但部分查询还是旧结果。原因:Redis缓存没失效,或者多台应用服务器本地缓存没同步。解决:更新数据后主动删除Redis相关key,或者用发布订阅通知所有节点清缓存。如果用了本地缓存,设置较短的过期时间(比如5分钟),牺牲一点命中率换一致性。

6. 进阶技巧:用BIN数据做路由决策和风险拦截

BIN数据用好了,不只是查银行名。我后来把它接入了路由引擎和风控规则,效果很明显。比如路由方面,可以根据org_code和card_type动态选择通道:银联借记卡走低费率通道,visa贷记卡走外卡通道。风控方面,可以标记某些BIN为高风险(比如曾经出现过盗刷的卡段),在交易入口直接拦截。

具体实现上,我会在bank_bin表里加两个字段:route_code(路由通道编码)和risk_level(风险等级,0-3)。这两个字段不来自官方数据,而是根据业务经验手工维护。更新时用单独的UPDATE语句,不覆盖官方字段。

UPDATE bank_bin SET route_code = 'UNIONPAY_DEBIT', risk_level = 0 WHERE org_code = 'UNIONPAY' AND card_type = 1; UPDATE bank_bin SET risk_level = 3 WHERE bin IN ('622848', '622609') AND status = 1;

验证方法也很简单:拿一批历史交易卡号,跑一遍路由决策,对比实际通道,看准确率。我一般会抽样1000条,准确率低于99%就要查原因。另外,BIN数据不是一成不变的,新卡种、新发卡行会不断出现,所以我会设一个每月一次的巡检任务,用LEFT JOIN找出交易表里有但BIN表里没有的卡号前6位,人工确认后补录。

最后说个我自己的习惯:每次更新BIN数据前,一定先在测试库跑一遍全量查询,确认没有性能下降和结果异常,再推生产。这个习惯帮我挡掉了至少三次因为数据格式问题导致的线上故障。BIN数据看着简单,但它是支付系统的地基,地基不稳,上面盖什么都会晃。希望帮到你。

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

RELATED READING

延伸阅读

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