ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

银行卡BIN号数据落地:Excel到MySQL/PostgreSQL导入与查询服务封装

银行卡BIN号数据落地:Excel到MySQL/PostgreSQL导入与查询服务封装

简介:这份资源面向金融数据分析、风控开发及数据库运维人员,提供2020年4月25日版银行卡BIN号数据集合,用于识别发卡行、卡片类型及归属机构,支撑交易验证、反欺诈与客户分析等场景。压缩包共7个文件,以5个xls表格和2个sql脚本为主,xls覆盖农民工卡、跨行转账卡、非标卡、单位结算卡等分类卡表,sql脚本可直接导入MySQL或PostgreSQL,包体约1.4MB,轻量易用。已有859人学习下载,适合需要快速搭建BIN号查询表或补充测试数据的开发者。读者可借助Excel完成排序筛选与公式统计,也可将数据导入数据库构建bank_cards表,结合SQL实现发卡行分布、卡种占比等查询,并参考更新思路定期维护BIN号列表。

1. 银行卡BIN号数据落地:从Excel到MySQL/PostgreSQL的完整链路

做支付、风控或者清结算系统的开发者,大概率都遇到过同一个需求:给一串银行卡号,判断它属于哪家银行、什么卡种、是不是借记卡。这个判断的核心依据就是银行卡BIN号,也就是卡号前6到8位。我最近拆了一份银行卡BIN号数据资源,里面同时提供了Excel、MySQL和PostgreSQL三种格式,覆盖了国内主流银行的借记卡和信用卡BIN段。拿到手第一件事不是急着导入,而是先搞清楚数据长什么样、字段怎么对应、不同数据库的导入方式有什么区别。这份资源适合正在做支付路由、银行卡识别、风控规则引擎的开发者,也适合需要快速搭建BIN查询服务但又不想从零整理数据的人。下面按实际落地流程,从数据结构、导入操作到查询封装和二次改造,一步步拆开讲。

2. 数据结构拆解:Excel字段与数据库表设计的对应关系

2.1 Excel原始字段说明

拿到Excel文件后,先别急着写导入脚本,花几分钟看清楚列的含义。这份资源的Excel通常包含以下几列:BIN号(6到8位数字)、银行名称、银行标识码、卡种(借记卡/信用卡/准贷记卡)、卡号长度。有些版本还会带上银行英文名和客服电话,但核心字段就是前五个。

这里有个容易翻车的地方:BIN号在Excel里经常被存成科学计数法或者数值格式,导致前导零丢失。比如某些BIN以0开头,Excel默认会把它当数字处理,0就没了。我一般会先用文本编辑器打开CSV版本确认原始格式,或者在Excel里把BIN列直接设成文本格式再另存。

# 先用head命令快速看一眼CSV前几行,确认BIN列有没有被截断 head -5 bank_bin.csv # 输出示例: # bin,bank_name,bank_code,card_type,card_length # 621226,某大型商业银行,ICBC,借记卡,19 # 622202,某大型商业银行,ICBC,借记卡,19

如果发现BIN列长度不对,比如621226变成了621226.0或者6222变成了6222,那就说明格式有问题,需要先做清洗。

2.2 数据库表结构设计

Excel看完了,接下来设计数据库表。MySQL和PostgreSQL的建表语句略有差异,但核心字段一致。我一般会这样建:

-- MySQL建表语句 CREATE TABLE bank_bin ( id INT AUTO_INCREMENT PRIMARY KEY, bin VARCHAR(8) NOT NULL COMMENT '银行卡BIN号,6-8位', bank_name VARCHAR(64) NOT NULL COMMENT '银行名称', bank_code VARCHAR(16) DEFAULT NULL COMMENT '银行标识码', card_type VARCHAR(16) DEFAULT NULL COMMENT '卡种:借记卡/信用卡/准贷记卡', card_length TINYINT DEFAULT NULL COMMENT '卡号总长度', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_bin (bin), KEY idx_bank_code (bank_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='银行卡BIN号对照表';
-- PostgreSQL建表语句 CREATE TABLE bank_bin ( id SERIAL PRIMARY KEY, bin VARCHAR(8) NOT NULL, bank_name VARCHAR(64) NOT NULL, bank_code VARCHAR(16), card_type VARCHAR(16), card_length SMALLINT, created_at TIMESTAMP DEFAULT NOW(), CONSTRAINT uk_bin UNIQUE (bin) ); CREATE INDEX idx_bank_code ON bank_bin (bank_code);

两个版本的区别主要在自增主键的写法上,MySQL用AUTO_INCREMENT,PostgreSQL用SERIAL。字符集方面MySQL建议用utf8mb4,避免银行名称里有生僻字导致乱码。BIN字段用VARCHAR而不是INT,就是为了防止前导零丢失,这个坑我在早期项目里踩过,当时用INT存BIN,结果所有0开头的卡号全部匹配失败,排查了半天才发现是类型问题。

注意:如果BIN号有8位的,VARCHAR(8)刚好够用;如果未来可能扩展到更长,建议直接上VARCHAR(12),留点余量。

2.3 数据量级与索引策略

这份资源的BIN数据量通常在几千到几万条之间,具体取决于覆盖的银行和卡种范围。对于这个量级,单表完全够用,不需要分库分表。查询模式也很固定:要么按BIN精确匹配,要么按银行标识码查列表。所以索引策略很简单:BIN列加唯一索引,bank_code列加普通索引。

如果查询QPS比较高,比如支付路由场景下每笔交易都要查一次,可以考虑在应用层加一层本地缓存,用Caffeine或者Guava Cache把全量BIN数据加载到内存里,定时刷新。这个后面在查询封装那章会展开讲。

3. 数据导入实战:Excel转MySQL与PostgreSQL的两种路径

3.1 路径一:Excel直接导入数据库

最省事的方式是用数据库自带的导入工具。MySQL Workbench和Navicat都支持直接导入Excel文件,PostgreSQL可以用pgAdmin的导入功能或者COPY命令。但这种方式有几个限制:字段类型需要手动映射,BIN列如果被识别成数值类型就会丢前导零,而且大批量导入时速度慢。

我一般会先把Excel另存为CSV,然后用命令行导入,这样可控性更强。

# MySQL命令行导入CSV mysql -u root -p your_database -e " LOAD DATA LOCAL INFILE 'bank_bin.csv' INTO TABLE bank_bin FIELDS TERMINATED BY ',' ENCLOSED BY '\"' LINES TERMINATED BY '\n' IGNORE 1 ROWS (bin, bank_name, bank_code, card_type, card_length); "
# PostgreSQL命令行导入CSV psql -U postgres -d your_database -c " COPY bank_bin(bin, bank_name, bank_code, card_type, card_length) FROM '/path/to/bank_bin.csv' WITH (FORMAT csv, HEADER true, DELIMITER ','); "

MySQL的LOAD DATA LOCAL INFILE需要确保local_infile参数是开启的,否则会报错。PostgreSQL的COPY命令要求文件在服务器本地路径下,如果是远程连接,需要先把文件传到服务器上,或者用\copy命令在客户端执行。

提示:导入前先用SELECT COUNT(*)确认一下源文件行数,导入后再查一次目标表行数,两边对不上就说明有行被跳过了,通常是字段里的逗号或者换行符导致的。

3.2 路径二:Python脚本清洗后写入

如果Excel格式比较乱,比如有合并单元格、多sheet、或者BIN列格式不统一,那就得上Python脚本做清洗。我一般用pandas读Excel,清洗完再通过SQLAlchemy写入数据库。

import pandas as pd from sqlalchemy import create_engine # 读取Excel,强制BIN列为字符串 df = pd.read_excel('bank_bin.xlsx', dtype={'BIN号': str}) # 清洗:去掉空格、补零到6位、过滤无效行 df['BIN号'] = df['BIN号'].str.strip().str.zfill(6) df = df[df['BIN号'].str.match(r'^\d{6,8}$')] # 重命名列,和数据库字段对应 df = df.rename(columns={ 'BIN号': 'bin', '银行名称': 'bank_name', '银行标识码': 'bank_code', '卡种': 'card_type', '卡号长度': 'card_length' }) # 写入MySQL engine = create_engine('mysql+pymysql://root:password@localhost:3306/your_database?charset=utf8mb4') df.to_sql('bank_bin', engine, if_exists='append', index=False, method='multi', chunksize=1000) # 写入PostgreSQL # engine = create_engine('postgresql+psycopg2://postgres:password@localhost:5432/your_database') # df.to_sql('bank_bin', engine, if_exists='append', index=False, method='multi', chunksize=1000)

这段脚本的关键点有三个:第一,dtype={'BIN号': str}强制BIN列按字符串读取,避免科学计数法;第二,zfill(6)把不足6位的BIN补零,比如1234补成001234;第三,正则过滤掉不符合6到8位数字格式的脏数据。method='multi'和chunksize=1000配合使用,可以批量插入,比逐条INSERT快很多。

3.3 导入后的数据校验

导入完成后,别急着上线,先做几项校验。第一,查总行数是否和源文件一致。第二,随机抽几个BIN号,手动核对银行名称和卡种是否正确。第三,检查有没有重复BIN,虽然建了唯一索引,但导入时如果用了IGNORE或者ON CONFLICT DO NOTHING,重复数据会被静默跳过,需要确认跳过的数量是否合理。

-- 校验总行数 SELECT COUNT(*) FROM bank_bin; -- 检查重复BIN SELECT bin, COUNT(*) as cnt FROM bank_bin GROUP BY bin HAVING cnt > 1; -- 随机抽样核对 SELECT * FROM bank_bin ORDER BY RANDOM() LIMIT 10;

PostgreSQL用RANDOM(),MySQL用RAND()。如果发现重复BIN,大概率是源数据里同一家银行的不同卡种共用了BIN段,这种情况需要根据业务规则决定保留哪一条,或者把卡种字段改成多值。

4. 查询服务封装:从SQL到应用层缓存的完整实现

4.1 基础查询与模糊匹配

数据导入后,最简单的用法就是写SQL查。按BIN精确匹配:

SELECT bank_name, bank_code, card_type FROM bank_bin WHERE bin = '621226' LIMIT 1;

但实际业务中,拿到的卡号往往是完整卡号,需要先截取前6位或前8位再查。而且有些BIN是6位,有些是8位,查询时需要先试8位再试6位。

-- 先按8位查,查不到再按6位查 SELECT bank_name, bank_code, card_type FROM bank_bin WHERE bin = LEFT('6212261234567890', 8) OR bin = LEFT('6212261234567890', 6) ORDER BY LENGTH(bin) DESC LIMIT 1;

这个查询的逻辑是:优先匹配更长的BIN,因为8位BIN比6位BIN更精确。ORDER BY LENGTH(bin) DESC保证8位的结果排在前面。

4.2 应用层缓存方案

如果每次查询都走数据库,在支付路由这种高频场景下数据库压力会很大。我一般会在应用层加一层本地缓存,把全量BIN数据加载到内存里,用HashMap或者ConcurrentHashMap存储,key是BIN号,value是银行信息对象。

// Java示例:用ConcurrentHashMap做本地缓存 @Component public class BankBinCache { private final Map<String, BankBinInfo> cache = new ConcurrentHashMap<>(); @Autowired private BankBinMapper bankBinMapper; @PostConstruct public void init() { List<BankBinInfo> list = bankBinMapper.selectAll(); for (BankBinInfo info : list) { cache.put(info.getBin(), info); } } public BankBinInfo query(String cardNo) { // 先试8位 String bin8 = cardNo.substring(0, Math.min(8, cardNo.length())); BankBinInfo info = cache.get(bin8); if (info != null) return info; // 再试6位 String bin6 = cardNo.substring(0, Math.min(6, cardNo.length())); return cache.get(bin6); } // 定时刷新,比如每天凌晨重新加载 @Scheduled(cron = "0 0 3 * * ?") public void refresh() { cache.clear(); init(); } }

这个方案的核心思路是空间换时间。几千到几万条数据占用的内存很小,但查询速度从毫秒级降到微秒级。定时刷新保证数据不会太旧,如果BIN数据更新频繁,可以把刷新间隔缩短到小时级。

4.3 多数据库适配的注意事项

这份资源同时提供了MySQL和PostgreSQL两个版本,实际选哪个取决于项目现有的技术栈。如果团队已经在用PostgreSQL,直接用PG版本就行,不用为了一个BIN表单独引入MySQL。两个版本在查询语法上的差异很小,主要注意这几点:MySQL的LIMIT在PostgreSQL里也支持,但OFFSET写法略有不同;字符串拼接MySQL用CONCAT(),PostgreSQL用||;日期函数差异较大,但BIN查询场景基本用不到。

如果项目需要同时支持两种数据库,建议在ORM层做适配,比如用MyBatis的databaseIdProvider,或者JPA的Dialect自动处理。不要在手写SQL里硬编码数据库特有的语法,否则迁移时会很痛苦。

5. 避坑与排查:BIN数据导入查询中的常见问题

5.1 BIN号前导零丢失

现象:导入后发现某些BIN号查不到,比如以0开头的BIN在数据库里变成了不带0的短数字。

原因:Excel或者CSV在读取时把BIN列识别成了数值类型,前导零被自动去掉。

解决:读取时强制指定字符串类型,数据库字段用VARCHAR,导入前用zfill补零。如果已经导入错了,用UPDATE语句补零:UPDATE bank_bin SET bin = LPAD(bin, 6, '0') WHERE LENGTH(bin) < 6;

5.2 银行名称乱码

现象:查询结果里银行名称显示为问号或者方块。

原因:数据库字符集和客户端字符集不一致,常见于MySQL默认latin1的情况。

解决:建表时指定utf8mb4,连接字符串里加charset=utf8mb4,导入前确认CSV文件本身是UTF-8编码。如果是GBK编码的CSV,需要先用iconv转码。

5.3 同一BIN对应多个卡种

现象:一个BIN号在表里有多条记录,卡种分别是借记卡和信用卡。

原因:部分银行确实存在同一BIN段下既有借记卡又有信用卡的情况,或者数据源本身有重复。

解决:根据业务需求决定策略。如果只是判断归属行,取任意一条即可;如果需要判断卡种,可能需要结合卡号长度或者其他规则进一步区分。建表时不要对BIN加唯一索引,或者加联合唯一索引(bin, card_type)。

5.4 导入速度慢

现象:几万条数据导入花了好几分钟。

原因:逐条INSERT没有用批量提交,或者autocommit是开启的。

解决:用批量插入,MySQL的LOAD DATA或者INSERT INTO ... VALUES (...), (...), ...,PostgreSQL的COPY。如果用ORM,设置batch_size。导入前临时关闭autocommit,导入后手动提交。

5.5 查询时BIN长度不一致

现象:有的卡号能查到,有的查不到。

原因:数据源里BIN有6位也有8位,查询时只截取了固定长度。

解决:查询逻辑改成先试8位再试6位,或者把数据统一成6位存储,查询时也只取6位。我一般倾向于保留原始长度,查询时做兼容。

6. 二次开发与扩展:从BIN查询到银行卡识别服务的改造思路

拿到这份BIN数据后,大多数人的第一反应是够用了,但实际业务跑一段时间就会发现,光靠BIN判断归属行还不够。比如风控场景需要知道卡种是借记卡还是信用卡,路由场景需要知道银行是否支持某条通道,清结算场景需要知道银行的清算代码。这些信息BIN表里不一定都有,需要做扩展。

我一般会这样改造:在现有bank_bin表基础上,加几个扩展字段,比如support_online(是否支持线上支付)、settle_code(清算行号)、bank_level(银行等级)。这些字段的数据来源可以是人工维护,也可以从其他数据源关联过来。表结构改完后,查询服务返回的对象也要相应扩展,但对外接口保持兼容,新增字段用可选返回。

另一个改造方向是加一层BIN数据的版本管理。BIN号不是一成不变的,银行会不定期新增或停用BIN段。如果直接覆盖旧数据,出了问题没法回滚。我的做法是加一个version字段和effective_date字段,每次导入新数据时生成一个新版本,查询时默认取最新版本,需要查历史版本时指定version。这样即使新数据有问题,也能快速切回旧版本。

-- 加版本字段后的表结构 ALTER TABLE bank_bin ADD COLUMN version INT DEFAULT 1; ALTER TABLE bank_bin ADD COLUMN effective_date DATE DEFAULT CURRENT_DATE; -- 查询时取最新版本 SELECT * FROM bank_bin WHERE bin = '621226' AND version = (SELECT MAX(version) FROM bank_bin) LIMIT 1;

还有一个实用技巧是给BIN查询加一个降级策略。当数据库不可用时,自动切换到本地缓存的上一份数据,保证核心支付链路不中断。这个降级开关可以用配置中心控制,也可以根据数据库健康检查结果自动触发。

验证改造效果的方法很简单:准备一批测试卡号,覆盖6位BIN、8位BIN、借记卡、信用卡、不存在的BIN这几种情况,跑一遍查询看返回结果是否符合预期。再模拟一次数据库宕机,看降级逻辑是否生效。我一般会在上线前把这几项写成自动化测试用例,每次改完BIN数据或者查询逻辑都跑一遍。

从那以后我每次导入新的BIN数据前,都会先用脚本跑一遍格式校验和抽样核对,确认没问题再往生产库导。这个习惯帮我省了好几次半夜爬起来修数据的麻烦。希望帮到你。

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

RELATED READING

延伸阅读

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