简介:2020年4月版银行卡BIN号规范数据以压缩包形式发布,面向金融风控、支付结算、数据分析等需要识别发卡行与卡片类型的从业者。数据覆盖信用卡、借记卡、农民工卡、跨行转账卡、非标卡及单位结算卡等分类,可用于本地建表、关联查询、反欺诈识别和客户画像。压缩包共7个文件,含5个Excel表格和2个SQL脚本:Excel便于人工排序筛选与分析核对,SQL脚本可直接导入MySQL或PostgreSQL,快速生成bin_number、bank_name、card_type等字段的标准化表,整体仅1.4MB。已有856人学习下载,在银行卡归属行识别与交易校验场景中具备参考价值。使用时可依据银行卡前6位BIN快速匹配发卡机构,也可借助SQL完成定期更新维护,为交易安全校验和风险管理提供基础数据支撑。
1. 最新银行卡BIN号数据是什么:先搞清楚你缺哪块
做支付对账的人最怕碰到这种场景:一笔交易拦在风控规则里,工单转过来,查了半天发现是发卡行识别不出来,本地库里根本没有这张卡的BIN记录。多数时候不是没数据,而是本地那张BIN号表还是两个季度前的快照,新发卡行、新卡种全都没跟上。这里说的BIN号,就是银行卡号前6到8位识别码,它决定这张卡属于哪个卡组织、由哪家银行发出、是借记卡还是贷记卡。标题里的「最新银行卡bin号 excel mysql pg」,拆开看就是一条完整链路:把整理好的BIN号清单从Excel出发,分别灌进MySQL和PostgreSQL,让交易和风控系统用得上。这篇文章按我实际推进的顺序来写,适合正在做支付接入、交易路由、风控规则维护、以及要维护多套数据库环境的同学。
2. 把BIN号整理成Excel清单:字段设计、去重与CSV导出
2.1 先定字段:一张BIN号表至少要有这9列
很多从公开渠道抓回来的BIN数据,原始格式往往是网页表格或者文本文件,字段乱且名称不统一。直接进库之前,第一步是把它整理成一张标准Excel清单。我这里有一套固定字段,跑过多次数据迁移,按这套来,两个数据库端到端不会出大乱子。
| 字段名 | 类型建议 | 必填 | 说明 |
|---|---|---|---|
| bin_number | VARCHAR(16) | 必填 | 银行卡识别码,6到8位数字,一律按文本处理 |
| card_brand | VARCHAR(32) | 必填 | 卡组织枚举:VISA/MASTERCARD/UNIONPAY/AMEX等 |
| card_type | VARCHAR(16) | 必填 | 借记卡/贷记卡/准贷记卡 |
| bank_name | VARCHAR(64) | 必填 | 发卡行名称 |
| bank_code | VARCHAR(16) | 选填 | 发卡行行号或联行号 |
| card_level | VARCHAR(32) | 选填 | 卡等级:普卡/金卡/白金/钻石 |
| country_code | VARCHAR(8) | 选填 | 国家或地区ISO代码 |
| source_version | VARCHAR(32) | 必填 | 数据源版本标记,比如2025Q3 |
| update_time | DATETIME | 必填 | 这条记录生效的时间 |
有几个字段的选择理由必须说清楚。bin_number用 VARCHAR 而不是数字,因为BIN号不参与加减乘除,只做前缀匹配,存成数字纯属给自己加戏,还容易丢前导零。bank_name 和 bank_code 要分开,银行改名、合并是常事,代码不变名称变,拆开之后对账还能靠 code 关联。source_version 这一列最容易被人忽略,但它是后面排查「新旧版本混用」的唯一依据,没有它出了问题只能认栽。card_brand 和 card_type 是两个维度:同一个卡组织既有借记卡也有贷记卡,别混成一列。
2.2 去重与变更处理:同一BIN号出现在两个版本里怎么办
整理Excel时最常遇到的就是重复。同一张卡,上季度版本里有记录,新版本里也有,bin_number 完全一样,但 bank_name 或者 card_level 可能变了。直接删会造成信息丢失,不删进库又会主键冲突。
先用公式把重复项标记出来,这一步最直观:
=COUNTIF(A:A, A2)>1逻辑很简单:筛选结果为 TRUE 的行,就是这个BIN号在整表里出现了不止一次。公式好用在于,你能先看到重复记录各自带了什么字段,再判断是直接删还是更新。如果是纯重复(每列都一样),保留一行即可;如果是同一BIN号在不同版本里银行归属变了,那不是脏数据,而是BIN回收、变更发卡行的正常情况,处理原则是:以 source_version 较新的为准,旧记录不硬删,归档到一张历史表里留底。
清理完重复,还要顺手处理几类脏数据:全角空格和不可见字符,用 TRIM 和 CLEAN 函数过一遍;银行名称里「股份有限公司」「有限责任公司」这类后缀,有的源带、有的不带,最好统一口径;括号和引号都换成半角,不然后面CSV转义会坑到你。
2.3 导出CSV而不是直接贴库:三个必须检查的设置
Excel整理好后,下一步不是直接连数据库贴,而是先另存为CSV。直接贴库是新手才会干的事,临时测一下可以,正式环境没人敢让Excel直连数据库。导出CSV前,请务必检查三个设置,缺一个后面就翻车。
第一个是强制文本格式。选中所有数据列,单元格格式设为「文本」,尤其是 bin_number 这一列。Excel会在某一刻悄悄把看起来像数字的内容转成科学计数法,6到8位的BIN当前不会溢出,但表里如果还带了示例卡号或者完整卡号列,16位以上数字就会变成 6.22E+18,这列数据就算废了。
第二个是编码。Excel自带「CSV UTF-8」另存时通常带BOM,MySQL的 LOAD DATA 读它,第一列会多出一个看不见的\ufeff,导致首个BIN号永远匹配不上。我一般不用Excel直接存,而是用Python读一遍Excel再统一输出CSV,控制编码和换行符:
import pandas as pd df = pd.read_excel('bin_source.xlsx', dtype={'bin_number': str, 'bank_code': str}) df = df.drop_duplicates(subset='bin_number', keep='last') df['is_valid'] = df.get('is_valid', 1) df = df[['bin_number', 'card_brand', 'card_type', 'bank_name', 'bank_code', 'card_level', 'country_code', 'is_valid', 'source_version']] df.to_csv('bin_info_2025q3.csv', index=False, encoding='utf-8')这里 dtype 参数强制把 bin_number 读成字符串,避免pandas自动推断成int64;列顺序固定,是为了后面 MySQL 和 PG 的导入语句都不用再调列映射。第三个是换行符。pandas默认输出\n,Excel另存是\r\n,写数据库导入语句时要知道自己文件是什么换行,MySQL端LINES TERMINATED BY写错,轻则多出一列,重则整文件导入失败。
3. 导入MySQL:建表、LOAD DATA 与临时表合并
3.1 建表:BIN号为什么用VARCHAR当主键
MySQL这边我一般先建一个独立库,避免和业务表混在一起。建表语句如下:
CREATE DATABASE IF NOT EXISTS pay_base DEFAULT CHARSET utf8mb4; CREATE TABLE pay_base.bin_info ( bin_number VARCHAR(16) NOT NULL COMMENT '银行卡识别码,6到8位,按文本处理', card_brand VARCHAR(32) NOT NULL COMMENT '卡组织枚举:VISA/MASTERCARD/UNIONPAY/AMEX', card_type VARCHAR(16) NOT NULL COMMENT '借记卡/贷记卡/准贷记卡', bank_name VARCHAR(64) NOT NULL COMMENT '发卡行名称', bank_code VARCHAR(16) DEFAULT NULL COMMENT '发卡行行号', card_level VARCHAR(32) DEFAULT NULL COMMENT '卡等级', country_code VARCHAR(8) DEFAULT NULL COMMENT 'ISO国家代码', is_valid TINYINT NOT NULL DEFAULT 1 COMMENT '是否在用:1在用 0已废弃', source_version VARCHAR(32) NOT NULL COMMENT '数据源版本标记', update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (bin_number), KEY idx_brand (card_brand) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;几个设计点说明一下。bin_number 做主键,是因为业务查询永远是通过卡号前缀去精确命中的,主键直接覆盖,不需要多余回表。VARCHAR(16) 不是拍脑袋,识别码目前最长8位,留一倍余量足够,太长反而浪费索引空间。is_valid 字段用来标记废弃BIN,而不是直接删行,因为历史流水可能还挂在旧BIN上,删了就查不到对应关系。card_brand 上建普通索引,是为了后面做统计报表,不建也能跑,但分组统计会全表扫,几万行无所谓,几十万行就有感知了。
如果是接已有的库,注意原表可能已经有 bin_number 列且不是主键,这时候别直接改表结构,先导出一份旧数据备份,再扩列加主键,避免在线上生产表上做高风险DDL。
3.2 LOAD DATA INFILE 完整导入:参数、权限与常见中断
CSV准备好了,最顺的导入方式就是 MySQL 的 LOAD DATA。完整语句:
LOAD DATA INFILE '/opt/bin_data/bin_info_2025q3.csv' INTO TABLE pay_base.bin_info CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (bin_number, card_brand, card_type, bank_name, bank_code, card_level, country_code, is_valid, source_version) SET update_time = NOW();参数逐个解释:CHARACTER SET utf8mb4必须和CSV实际编码一致,文件是UTF-8就写utf8mb4,不小心写成latin1,银行名会出现乱码且不容易察觉;FIELDS TERMINATED BY ','是列分隔符,如果你CSV用的是分号就得改;OPTIONALLY ENCLOSED BY '"'处理带引号的字段,银行名称里有逗号时靠它保命;IGNORE 1 LINES跳过CSV表头;最后列清单要和CSV列顺序完全一致,顺序不对,数据错位比导入失败更可怕,因为库里看起来是满的,实际匹配全错。
权限问题是最常见的拦路虎。MySQL 8 默认开了secure-file-priv,服务端 LOAD DATA 只能读指定目录的数据文件,可以先查一下再决定文件放哪:
SHOW VARIABLES LIKE 'secure_file_priv';结果是目录就把CSV放进去,结果是空值就无限制。如果你没有服务端文件系统的访问权限,就改用LOAD DATA LOCAL INFILE,它走客户端上传,但需要客户端连接时加--local-infile=1参数,MySQL 8 默认这个开关是关的。生产环境里,我优先用服务端版本,LOCAL版本在跨机房、大文件场景下速度差不少。
3.3 导入核对与增量更新:stage表加ON DUPLICATE KEY UPDATE
直接LOAD DATA进正式表,有个风险:文件里有一行主键冲突,整个导入就中断,前面导入的行全部留下,库变成半新半旧。我从不直接LOAD正式表,而是先建一张一模一样的stage表,导到stage里,核对无误后再合并进正式表。
CREATE TABLE pay_base.bin_info_stage LIKE pay_base.bin_info; LOAD DATA INFILE '/opt/bin_data/bin_info_2025q3.csv' INTO TABLE pay_base.bin_info_stage CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (bin_number, card_brand, card_type, bank_name, bank_code, card_level, country_code, is_valid, source_version) SET update_time = NOW();合并更新用 INSERT ... ON DUPLICATE KEY UPDATE:
INSERT INTO pay_base.bin_info (bin_number, card_brand, card_type, bank_name, bank_code, card_level, country_code, is_valid, source_version, update_time) SELECT bin_number, card_brand, card_type, bank_name, bank_code, card_level, country_code, is_valid, source_version, NOW() FROM pay_base.bin_info_stage ON DUPLICATE KEY UPDATE card_brand = VALUES(card_brand), card_type = VALUES(card_type), bank_name = VALUES(bank_name), bank_code = VALUES(bank_code), card_level = VALUES(card_level), country_code = VALUES(country_code), is_valid = VALUES(is_valid), source_version = VALUES(source_version), update_time = NOW();这样做的价值在于:如果 stage 表导入时报错,正式表完全不受影响,修好文件重跑就行;合并阶段即使有冲突,也是按版本直接覆盖,不会中断。导完后跑三句核对SQL,确认行数和版本分布正常:
SELECT COUNT(*) FROM pay_base.bin_info; SELECT source_version, COUNT(*) FROM pay_base.bin_info GROUP BY source_version; SELECT card_brand, card_type, COUNT(*) FROM pay_base.bin_info GROUP BY card_brand, card_type;第一句看总行数是否和源文件一致,第二句确认库里到底混了几个版本,第三句快速判断卡组织分布是否符合预期。如果总行数对不上,大概率是CSV里本身有重复行或者stage表没清空。
4. 导入PostgreSQL:COPY、upsert 与两库同步
4.1 PG建表与COPY:权限模型和MySQL差在哪
PostgreSQL这边结构大同小异,但权限模型完全是另一套逻辑。建表语句:
CREATE TABLE pay_base.bin_info ( bin_number VARCHAR(16) PRIMARY KEY, card_brand VARCHAR(32) NOT NULL, card_type VARCHAR(16) NOT NULL, bank_name VARCHAR(64) NOT NULL, bank_code VARCHAR(16), card_level VARCHAR(32), country_code VARCHAR(8), is_valid BOOLEAN NOT NULL DEFAULT TRUE, source_version VARCHAR(32) NOT NULL, update_time TIMESTAMPTZ NOT NULL DEFAULT NOW() );对比MySQL版本,差异点:不需要 ENGINE 和 COLLATE,PG默认就是事务性表;BOOLEAN 直接用 bool 类型;TIMESTAMPTZ 自带时区,写入时自动转UTC存储,查询时按会话时区返回。核心差异在导入权限上。PG服务端COPY命令读的是数据库服务器上的文件,普通用户没有pg_read_server_files权限会直接报 permission denied。如果你只是临时导入,用\copy更省事,它是psql客户端命令,读的是客户端本机文件,不需要服务端文件权限:
-- psql内执行:读客户端本地文件 \copy pay_base.bin_info FROM '/opt/bin_data/bin_info_2025q3.csv' WITH (FORMAT csv, HEADER true, DELIMITER ',', ENCODING 'UTF8');服务端版本长这样,适合把文件放在数据库服务器上的批量场景:
COPY pay_base.bin_info FROM '/opt/bin_data/bin_info_2025q3.csv' WITH (FORMAT csv, HEADER true, DELIMITER ',', ENCODING 'UTF8');两者性能差异不算大,几十万行都是秒级完成。区别在于:\copy 适合开发机、临时环境随手用;COPY 适合自动化脚本里稳定跑,但前提是数据库账号有权限。数据库账号是业务应用自己用的,别给它开pg_read_server_files,建议单独建一个数据维护账号,专门用来跑这类导入任务。
4.2 用临时表和ON CONFLICT做PG版幂等更新
PG没有 MySQLON DUPLICATE KEY UPDATE,但它的INSERT ... ON CONFLICT更灵活,可以直接写条件。我的标准操作是:建stage表、灌数据、合并、删stage。
CREATE TABLE pay_base.bin_info_stage (LIKE pay_base.bin_info INCLUDING DEFAULTS); \copy pay_base.bin_info_stage FROM '/opt/bin_data/bin_info_2025q3.csv' WITH (FORMAT csv, HEADER true, DELIMITER ',', ENCODING 'UTF8');合并语句:
INSERT INTO pay_base.bin_info AS b (bin_number, card_brand, card_type, bank_name, bank_code, card_level, country_code, is_valid, source_version, update_time) SELECT s.bin_number, s.card_brand, s.card_type, s.bank_name, s.bank_code, s.card_level, s.country_code, s.is_valid, s.source_version, NOW() FROM pay_base.bin_info_stage s ON CONFLICT (bin_number) DO UPDATE SET card_brand = EXCLUDED.card_brand, card_type = EXCLUDED.card_type, bank_name = EXCLUDED.bank_name, bank_code = EXCLUDED.bank_code, card_level = EXCLUDED.card_level, country_code = EXCLUDED.country_code, is_valid = EXCLUDED.is_valid, source_version = EXCLUDED.source_version, update_time = NOW() WHERE b.source_version IS DISTINCT FROM EXCLUDED.source_version;关键在最后这个 WHERE 子句:只有新版本和库里已有版本不一致时,才更新 update_time。不加的话,每次全量重跑都会把所有行的 update_time 刷一遍,后面排查「这条记录到底什么时候更新的」就失去意义了。这一点和MySQL不同,MySQL的ON DUPLICATE KEY UPDATE没有这种条件能力,只能在SET里写update_time = IF(VALUES(source_version) <> source_version, NOW(), update_time)来实现类似效果,但可读性差很多。
合并完成后,删掉stage表,避免下次误用旧数据:
DROP TABLE pay_base.bin_info_stage;4.3 双库并行维护:同一份CSV怎么同时喂给MySQL和PG
实际环境里,MySQL和PG通常不是二选一,而是并存:业务库在MySQL,数据分析在PG,或者不同机房各用一套。BIN号数据是同一份,两边都要保持最新,不然同一个卡号在不同库查出来的发卡行不一样,审计都说不清。
我的做法是让CSV成为唯一中间产物,一份文件喂两个库。先跑Python脚本从Excel生成标准CSV,然后分别执行MySQL的 stage+LOAD 和 PG 的 stage+\copy 两条链路。不要试图用SQLAlchemy的to_sql一把梭,几十万行还能忍,上百万行明显变慢,而且它定义字段类型时容易和你手工建的表结构不一致,后续踩坑成本远高于省下的那点代码量。
双库同步最大的风险不是导入本身,而是两边导入完成后没人校验。我固定会在两个库各跑一条相同逻辑的汇总SQL,人工比一下数字:
SELECT source_version, COUNT(*) FROM pay_base.bin_info GROUP BY source_version ORDER BY source_version;MySQL和PG都可以执行这句(PG把反引号去掉)。输出的数字一致,才认为这次更新闭环了。如果两边数字不一致,优先检查是不是有一边stage表没清空导致重复导入。
5. BIN号数据维护避坑实录:长度识别、版本混用和编码翻车
5.1 坑一:BIN识别码不止6位,按6位匹配会漏掉新卡
现象:某张卡走VISA渠道,本地BIN表里按卡号前6位查得到,业务系统却不认,日志显示匹配记录的发卡行不对。原因:卡组织标准里识别码并不总固定在6位,近年的发卡方案已经出现7位、8位识别码,银联卡也有一部分按7位分配,只存6位等于把一批新卡全切掉了。解决:表结构里 bin_number 按 VARCHAR(16) 存,匹配时按卡号前8、7、6位逐级往下试,取最长命中,而不是只截前6位。这条规则对所有库都适用,和数据表本身无关,属于业务代码层必须做的调整。
5.2 坑二:新旧版本混用,上半月用新表下半月用旧表
现象:同一个月里,一批交易按新发卡行识别,另一批交易查出来是旧银行,风控规则对同一张卡的判断结果前后不一致,工单来回踢。原因:两个环境的BIN表更新步骤不同步,或者有人手动往生产表里刷了一个旧CSV,把新版本覆盖了。解决:source_version 字段必须在每次导入时强制写入,并在合并阶段用版本号做比较,只允许新版本覆盖旧版本;更新任务固定写成脚本,MySQL和PG同一套逻辑,分别跑、分别校验,杜绝手工SQL。另外建议在正式表上加一层视图,业务查询走视图,表结构怎么改都不影响上层应用。
5.3 坑三:Excel科学计数法毁掉一整列卡号
现象:从Excel导出的CSV里,卡号列变成6.22E+18,导入后所有长卡号尾数全是0,匹配永远失败。原因:Excel默认把超过11位的数字转成科学计数法,数字精度在17位左右就开始丢,16位银行卡号正好踩在危险区。解决:源数据里所有卡号、BIN号列在Excel里全部设为文本格式再处理;用Python读Excel时,dtype 必须显式指定为 str;导入前抽查CSV里是否有E+字符。这个坑看起来低级,但几乎每换一个数据源就会复发一次,必须在流程里固定检查项。
5.4 坑四:LOAD DATA一行报错,前面导入的行全留下
现象:LOAD DATA执行到一半,报ERROR 1062: Duplicate entry,然后整个语句终止,但库里的数据已经是半新半旧状态。原因:CSV里有重复BIN号,而目标表主键是 bin_number,默认行为是直接报错并停止。解决:不要直接LOAD正式表,先导stage表,stage表导入时加IGNORE关键字跳过冲突行:
LOAD DATA INFILE '/opt/bin_data/bin_info_2025q3.csv' IGNORE INTO TABLE pay_base.bin_info_stage ...然后对stage表做一次分组统计,找出重复来源,决定保留哪条,再合并正式表。这样任何异常都发生在stage层,正式表永远只有完整的新版本或完整的旧版本,不会有中间态。
5.5 坑五:中文银行名乱码,源头在CSV编码不在SQL
现象:PG查 bank_name 显示æµ·å¾...之类的乱码,MySQL那边同一个CSV却是正常中文。原因:CSV文件本身是GBK或GB2312编码,MySQL的CHARACTER SET utf8mb4跟实际编码不匹配时往往直接报错,但PG的COPY在某些环境下不报错,硬把GBK字节按UTF8解码,就存入了乱码。解决:所有CSV统一从源头转成UTF-8,我的习惯是用Python转,不用Excel另存,因为转换完用file命令或者直接head -c 100 bin.csv看一眼也能确认;导入语句里编码参数固定写 UTF8,两个库都统一,乱码问题基本消失。
6. 把BIN号表变成业务能力:匹配、缓存与自动更新
6.1 前缀匹配算法:识别码长度不齐时按最长优先
表建好、数据进库,只是第一步,真正让BIN号发挥价值的是业务代码里的匹配逻辑。最稳的前缀匹配是贪心算法:先从卡号最长的8位开始查,查不到就试7位,再试6位:
def match_bin(card_no: str, cache: dict) -> dict: for n in (8, 7, 6): key = card_no[:n] info = cache.get(key) if info: return info return None这段代码的前提是缓存里有按 bin_number 为key的记录。注意不要去数据库里执行WHERE '6228481234567890' LIKE CONCAT(bin_number, '%'),这个写法在MySQL和PG里都会让索引失效,全表扫描一次,几万行还能扛,并发一上来就雪崩。正确姿势是上面这种,应用层截位、字典查询、逐级降位。识别码长度这块,银联老卡基本6位能命中,新发卡方案和VISA、MC的8位识别码,靠这个循环就能兜住。
6.2 缓存与自动更新链路
BIN号全量数据量其实很小,几十万行撑死几十MB,完全可以全量加载到应用内存里。启动时从数据库查出所有is_valid = 1的记录,构建成字典;日常查询走内存,命中率极高,数据库端几乎零压力。更新时不需要重启服务,加载完新表后,标记一个版本号,下次查询用新的dict,旧dict等正在处理的请求结束后再释放。
自动更新链路我固定成三条命令:从源头下载新CSV、分别灌入MySQL和PG的stage表、合并并校验版本计数。全程不手工操作,避免哪次忘了更新测试环境。这一套跑顺之后,BIN号数据就不再是「偶尔手动更一次」的运维杂活,而是和业务系统同步演进的底层能力,发卡行一换,交易链路立刻感知。
过来人的教训是:早期我只更生产库、漏了测试库,某次联调用新卡测试,上游识别到了新发卡行,测试环境却一查一个空,双方互相甩锅排查了半天,最后发现是两边BIN表版本不一致。那之后,所有环境强制同一套更新脚本、同一个source_version,校验不过不上线。希望帮到你。
本文还有配套的精品资源,点击获取