尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

银行卡BIN数据处理:从Excel清洗到MySQL导入与风控应用

银行卡BIN数据处理:从Excel清洗到MySQL导入与风控应用 简介2020年4月银联官方发布的银行卡BIN数据已整理为电子表格与数据库脚本两种形式合计收录九千八百六十八条银行卡BIN记录可直接用于开发联调、测试数据准备或业务查询。面向支付系统开发者、金融风控人员及数据分析师适用于银行卡BIN识别、发卡行信息校验、卡种分类统计、支付表单自动填充等场景也可用于构建本地银行卡号查询服务或生成测试卡号。压缩包共七个文件包含五个按卡类划分的电子表格、一个可直接导入的数据库脚本及一个系统文件包体仅一点一二MB下载与部署都很轻量。已有三千四百八十九人学习下载数据字段覆盖银行卡BIN、BIN长度、发卡行、银行卡名称、银行卡类型、银行卡长度等核心信息可满足批量比对、查询统计和业务系统集成需求。数据源自银联官方发布权威性与完整度高电子表格适合人工筛选核对数据库脚本免去手动整理流程导入后即可使用为涉及银行卡信息处理的项目节省大量数据准备时间。1. 银行卡BIN数据支付系统里最不该拍脑袋查的六位数字银行卡 BIN 是卡号里信息密度最高的一段数字前 6 位或 8 位直接决定了这张卡是谁发的、属于借记卡还是贷记卡、走的是银联还是其他卡组织。很多人做支付对账时默认“查一下发卡行就行”真到风控规则和通道费率计算环节才发现 BIN 表比想象中更细同一个银行不同卡种对应不同 BIN 段判断错了就可能把贷记卡当成借记卡处理。这份 2020 银联官方口径的银行卡 BIN 数据Excel MySQL 双格式把借记卡、贷记卡、准贷记卡的 BIN 段整理成了可以直接查询的结构化表适合做支付回调校验、电商风控、对账核查的工程师直接拿来落地也适合刚接触支付系统、想搞懂卡 BIN 和卡种映射关系的新手当参考底稿。2. 字段口径与数据体检2020 银联 BIN 表拿到手先别急着导库2.1 核心字段与看表顺序卡BIN、发卡行、卡种三列不能乱打开这份资源里的 Excel 或 MySQL 文件先别急着导数据首要是把表头字段的口径对齐。常见的 BIN 数据表字段大致包含卡 BIN、卡种名称、卡种类型、发卡行行号、发卡行名称、卡组织、卡号长度等。前两列看着简单实际坑最多卡 BIN 有 6 位和 8 位两种写法卡种类型也有“借记卡”“贷记卡”“准贷记卡”“预付卡”四种常见枚举字段含义不对齐后面所有查询都是白做。字段名示例值说明bin622200卡号前缀可能是 6 位或 8 位card_name工商银行灵通卡银联登记的卡产品名称card_type借记卡借记卡/贷记卡/准贷记卡/预付卡bank_name中国工商银行发卡行名称card_org银联卡组织归属card_len16该卡 BIN 对应的卡号长度看表顺序我一般是先看 card_type 列有没有脏数据再看 bin 列是不是统一长度最后抽几个已知的银行卡号做命中测试。比如手边有一张 6222 开头的储蓄卡先在 Excel 里筛选 bin 622200确认发卡行和卡种跟手头实卡一致再谈下一步导入 MySQL。先做人工抽检再批量导入能避免把整张表搬到库里才发现口径不对的尴尬。2.2 在 Excel 里做快速体检筛选排序去重三个动作表结构没问题之后在 Excel 里做一遍体检三个动作就够筛选、排序、去重。筛选是针对 card_type 字段看枚举值是否干净有没有出现“借记”“贷计卡”这类手工录入错误排序是针对 bin 字段按数字序排一遍肉眼扫一下有没有明显断档和超长值去重是针对 bincard_name 组合防止同一 BIN 出现多行冲突数据。具体操作上选中整张表后在“数据”菜单里点筛选card_type 列下拉框会列出所有枚举值逐个点开看数量即可。排序时注意 bin 列如果存成了文本格式排序结果跟数值排序不同需要先确认列格式统一。去重不要直接点“删除重复项”覆盖原表我习惯复制一份到新 sheet 再操作这样原始数据留底后面发现误删还有后悔药。做完这三步这份 2020 银联 BIN 数据在 Excel 侧的质量基本就有数了。如果筛选出来卡种枚举值超过预期或者 bin 列格式五花八门宁可花时间先清洗再入库。血泪经验是清洗步骤省下来的时间会在后面写查询脚本时加倍还回去。2.3 卡种字段的业务含义借记卡、贷记卡、准贷记卡怎么区分银行卡 BIN 数据里最容易被低估的是 card_type 字段。同样是 6222 开头的 BIN借记卡和贷记卡的清算逻辑完全不同借记卡走实时扣款交易结果直接反映在账户余额上贷记卡走信用额度涉及账单日和还款日风控关注点也不一样。所以 BIN 表里 1 和 0 两个枚举值对应到业务系统里可能是两套完全不同的渠道号和费率配置。准贷记卡就更特殊它介于借记卡和贷记卡之间既有储蓄账户又允许透支在部分银行的旧系统里仍然广泛存在。如果你做的业务是电商支付或商户收单判断错卡种可能导致通道选择错误、手续费计算偏差甚至清算对账不平。这也是为什么我在 2.1 里强调先看 card_type 枚举——它是这张表在业务侧价值最高的字段之一。2.4 从 Excel 到 MySQL 的迁移路线Excel 适合人看MySQL 适合机器查。这份资源本身给了 MySQL 格式但拿到手后你大概率需要按自己的业务结构调整表结构。我一般走的标准路线是先把 Excel 检查完另存为 CSV UTF-8 格式再用 LOAD DATA 导入 MySQL导入后再在库内做二次校验。具体建表和导入语句在下一章展开这里先明确一个原则不要把 Excel 里的列原封不动全搬进 MySQL只留你业务真正会查的字段比如 bin、card_type、bank_name、is_credit 这些多余字段只会拖慢索引和查询速度。3. 从 Excel 导入 MySQL建表、LOAD DATA 与索引一起搞定3.1 建表语句字段类型选型与默认值设计导入 MySQL 前先建表。BIN 数据的查询场景基本都是精确匹配和前缀匹配不是数值运算所以 bin 字段必须用 VARCHAR不能用 INT。原因很简单BIN 以 0 开头的数字串很常见比如 043210用 INT 存会把前导零吃掉查询匹配直接失败。发卡行名称用 VARCHAR(64) 足够了太长浪费存储太短装不下二级分行名称。下面是具体的建表语句我用 utf8mb4 字符集避免中文乱码CREATE DATABASE IF NOT EXISTS bank_bin DEFAULT CHARSET utf8mb4; CREATE TABLE IF NOT EXISTS bank_bin ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 自增主键仅内部用, bin VARCHAR(8) NOT NULL COMMENT 卡BIN段6位或8位, card_name VARCHAR(32) DEFAULT COMMENT 卡产品名称来源于银联登记信息, card_type VARCHAR(16) NOT NULL DEFAULT 借记卡 COMMENT 借记卡/贷记卡/准贷记卡/预付卡, bank_code VARCHAR(12) DEFAULT NULL COMMENT 发卡行行号联行号口径, bank_name VARCHAR(64) NOT NULL COMMENT 发卡行名称, card_org VARCHAR(16) DEFAULT 银联 COMMENT 卡组织归属默认银联, card_len TINYINT UNSIGNED DEFAULT 16 COMMENT 卡号长度多为16或19, is_credit TINYINT(1) DEFAULT 0 COMMENT 是否贷记卡1是0否导入后用于快速判断, updated_at DATE DEFAULT NULL COMMENT 数据更新时间留空表示老BIN段, UNIQUE KEY uk_bin (bin), KEY idx_bank_name (bank_name), KEY idx_card_type (card_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT银联银行卡BIN数据表;表设计里有几个细节值得说清楚。bin 字段定义为 VARCHAR(8)兼容老的 6 位 BIN 和新的 8 位 BINis_credit 默认值为 0对应 MySQL 里常见的“mysql设置默认值为0”的写法这样导入 CSV 时如果该列为空自动落 0不会因为 NULL 影响后续判断条件。唯一键 uk_bin 保证一个 BIN 只有一行防止同一卡段重复出现导致统计翻倍。如果你在别处见过把 bin 当 INT 建的库建议趁早改掉这是绕不过去的坑。3.2 LOAD DATA 导入从 Excel 导出 CSV 到 MySQL 的完整链路表建好后把 Excel 另存为 CSV 是导入 MySQL 前的标准动作。在 Excel 里选“另存为”文件格式挑 CSV UTF-8逗号分隔这一步把 xlsx 的二进制格式转成纯文本MySQL 才能读。导入前用文本编辑器打开 CSV 看一眼首行确认表头、字段顺序、分隔符是否符合预期。常见问题是 Excel 里如果有公式或合并单元格导出 CSV 后会出现空行和多余逗号建议先复制粘贴成纯值再做另存。面是 LOAD DATA 的完整语句我把表头一行跳过字段按顺序映射LOAD DATA LOCAL INFILE /tmp/bin_data.csv INTO TABLE bank_bin CHARACTER SET utf8mb4 FIELDS TERMINATED BY , OPTIONALLY ENCLOSED BY IGNORE 1 LINES (bin, card_name, card_type, bank_code, bank_name, card_org, card_len, is_credit, updated_at) SET is_credit IF(is_credit , 0, is_credit), updated_at NULLIF(updated_at, ), card_len IF(card_len , 16, card_len);这段导入逻辑里FIELDS TERMINATED BY , 指定 CSV 的列分隔符OPTIONALLY ENCLOSED BY 处理字段值里可能出现的引号IGNORE 1 LINES 跳过表头。最关键的是 SET 部分——CSV 导入时经常有空字符串直接用 NULL 存进去会影响查询效率我做了三层兜底is_credit 空值补 0updated_at 空值转 NULLcard_len 空值补默认 16。这样导入后不需要再做一轮全表 UPDATE省一步是一步。导入完成后立刻跑一遍行数验证SELECT COUNT(*) AS total_rows FROM bank_bin;如果行数和 Excel 里的数据行数对不上先排查 CSV 末尾有没有空行再检查是不是有字段里含逗号导致列错位。LOAD DATA 导入速度快但出错时看错误日志比逐行排查更有效率MySQL 会在终端直接输出 warning 数量用 SHOW WARNINGS 查看具体原因。SHOW WARNINGS;3.3 索引设计与查询验证建表时已经给 bin 建了唯一索引但业务查询不全是按 bin 等值匹配还有按发卡行查、按卡种筛选、按卡号长度统计这类场景。所以我一般会在此基础上补两个辅助索引ALTER TABLE bank_bin ADD KEY idx_card_len (card_len), ADD KEY idx_bin_card_type (bin, card_type);idx_card_len 用于“这个卡号长度下有哪些 BIN”的统计idx_bin_card_type 是联合索引覆盖“按 BIN 定位后直接带出卡种”的查询避免回表。索引不是越多越好BIN 表的数据量通常在一万到几万行普通等值查询即使没索引也很快真正有意义的是联合索引和排序场景。验证索引是否生效用 EXPLAIN 看执行计划EXPLAIN SELECT bin, bank_name, card_type FROM bank_bin WHERE bin 622200;执行计划里 ref 显示 const 或者 eq_ref说明索引被用上了。如果显示 type 为 ALL就是全表扫描需要回头检查索引是不是没建成功。这一步验证做完基本可以确认 BIN 表已经可以正式服务查询场景了。数据导库这关过了后面接代码才有底。4. 查询实战把 BIN 表接进卡种识别与风控自查4.1 Python 查 BINpymysql 的查询封装BIN 数据导进 MySQL 后最常用的调用场景就是输入卡号、返回卡种和发卡行。我习惯用 pymysql 写一个独立的查询函数参数传卡号返回值直接是字典方便业务侧使用。注意查询时必须用参数化 SQL卡号是外部输入直接拼接 SQL 字符串等于把 SQL 注入的口子留给攻击者。import pymysql DB_CONFIG { host: 127.0.0.1, port: 3306, user: root, password: your_pass, database: bank_bin, charset: utf8mb4, autocommit: True, } def query_bin_info(card_no: str): # 卡号至少 6 位才可能提取 BIN小于 6 位直接返回 None if not card_no or len(card_no) 6: return None bin_part card_no[:6] # 默认取前 6 位作为 BIN sql SELECT bin, card_name, card_type, bank_name, is_credit FROM bank_bin WHERE bin %s LIMIT 1 conn pymysql.connect(**DB_CONFIG) try: with conn.cursor() as cur: cur.execute(sql, (bin_part,)) row cur.fetchone() if not row: return None return { bin: row[0], card_name: row[1], card_type: row[2], bank_name: row[3], is_credit: row[4], } except pymysql.MySQLError as e: # 打印错误码和消息便于排查网络或权限问题 print(fMySQL error {e.args[0]}: {e.args[1]}) return None finally: conn.close()这个函数的关键点在几点。bin_part 取前 6 位但如果表里有 8 位 BIN 数据直接用 6 位去匹配可能匹配到老的 6 位记录而错过 8 位新记录这一点在第五章会专门展开。pymysql.MySQLError 捕获后打印 e.args[0] 和 e.args[1]error 号码能帮你分清是连接问题、权限问题还是 SQL 语法问题不至于对着一个空返回值瞎猜。conn.close() 放在 finally 里保证连接一定释放这是随手写代码最容易漏掉的地方。调用方式很简单info query_bin_info(6222001234567890) print(info) # 输出示例{bin: 622200, card_name: 工商银行灵通卡, card_type: 借记卡, ...}4.2 SQL 侧批量识别JOIN 订单表带出卡种信息单卡查询适合接口调用做对账或数据分析时效率太低。批量场景下我一般直接把 BIN 表 LEFT JOIN 到业务订单表上用 LEFT(卡号, 6) 作为关联条件。MySQL 排序和分页都交给 SQL 层完成不在 Python 里做二次过滤。SELECT p.order_no, p.card_no, LEFT(p.card_no, 6) AS bin_part, b.bank_name, b.card_type, b.is_credit FROM pay_orders p LEFT JOIN bank_bin b ON b.bin LEFT(p.card_no, 6) WHERE p.pay_time 2024-01-01 00:00:00 ORDER BY p.order_no DESC LIMIT 200;JOIN 时注意两个性能点。LEFT(p.card_no, 6) 每个订单行都算一次订单量大时这个计算开销不可忽略更好的做法是在订单表冗余一列 bin_part业务写入时算好存进去再给 bin_part 建索引JOIN 就能走索引。LEFT JOIN 的语义是保留订单侧全量数据BIN 表匹配不到的就显示 NULL这种记录就是需要人工核查的“未识别卡”后面可以单独捞出来分析。4.3 风控自查按卡段聚合异常交易风控场景里更关心“哪个 BIN 段的交易量异动”。同一个发卡行下不同卡种风险特征差别很大贷记卡套现场景明显多于借记卡。所以聚合维度要落到 bin card_type不是只按银行名聚合。SELECT b.bin, b.bank_name, b.card_type, COUNT(*) AS tx_cnt, SUM(p.amount) AS total_amount FROM pay_orders p LEFT JOIN bank_bin b ON b.bin LEFT(p.card_no, 6) WHERE p.pay_time 2024-06-01 GROUP BY b.bin, b.bank_name, b.card_type HAVING tx_cnt 100 ORDER BY tx_cnt DESC;这条 SQL 把近一个月的交易按 BIN 聚合找出交易笔数超过 100 的卡段配合发卡行和卡种一起看能快速定位异常集中的卡段。GROUP BY 里带上 bank_name 和 card_type 是刻意做的——虽然 bin 本身能决定这两个字段但输出结果里直接带着省掉一次 JOIN 回表。HAVING 在 GROUP BY 之后过滤跟 WHERE 的执行顺序完全不同条件里有聚合函数时只能放 HAVING。这种查询跑完后把结果导出 Excel 存档是常规操作Python 里用 pandas 的 to_excel 一行搞定import pandas as pd # 把 4.3 的 SQL 查询结果存到 DataFrame df pd.read_sql(sql, conn) df.to_excel(bin_risk_check.xlsx, indexFalse)导出的 Excel 方便给运营同事做二次筛查也保留了自助分析的原数据。到这里BIN 表已经从静态文件变成了一个支撑查询和风控判断的活跃数据源。5. 避坑与常见问题BIN 数据用起来会踩的五个坑5.1 坑一6 位 BIN 和 8 位 BIN 混存查询时漏掉新卡现象用卡号前 6 位查 BIN 表一部分新版银行卡返回空结果换成 8 位查询又能命中。原因银联在 2019 年前后将 BIN 位数从 6 位扩展到 8 位2020 年的数据里同时存在 6 位和 8 位两种口径。老卡沿用 6 位 BIN新发卡使用 8 位 BIN。如果表里两种长度都保留查询时只取 6 位就会错过新卡段。解决查询逻辑改成优先匹配 8 位匹配不到再降级到 6 位。SQL 里可以这样处理WHERE bin IN (LEFT(card_no, 8), LEFT(card_no, 6)) ORDER BY LENGTH(bin) DESC LIMIT 1优先返回更长的 BIN 记录。如果业务侧只需要一种口径建议导入时统一截断或统一补位但 8 位新卡段截成 6 位会丢信息我一般保留双长度用程序做降级匹配。5.2 坑二发卡行字段混入分行名称聚合统计被拉散现象按 bank_name 分组统计交易时同一个银行出现几十个不同名称比如“中国工商银行北京分行”“工商银行上海分行”各自成行。原因银联 BIN 数据里 bank_name 存在总行和分行混合登记的情况部分卡 BIN 关联到的发卡行名称粒度是分行级不是总行级。解决建表时增加一个 bank_short_name 字段导入后用规则统一映射。可以用 SQL 做个半自动清洗UPDATE bank_bin SET bank_short_name CASE WHEN bank_name LIKE %工商银行% THEN 工商银行 WHEN bank_name LIKE %建设银行% THEN 建设银行 ELSE bank_name END;注意 UPDATE 语法里 ELSE 分支不能省否则匹配不到规则的行会被置成空字符串。实际业务里我还会保留一个 bank_code 字段做精确关联比名称关联靠谱得多。5.3 坑三2020 年的表识别不了 2021 年后新发的卡现象输入一张 2022 年新发行的银行卡号BIN 表里查不到记录。原因BIN 数据是时效性数据2020 银联官方口径覆盖的是截至 2020 年的卡段。银行新发的卡产品、联名卡、数字银行卡都会注册新 BIN旧表里当然没有。解决把这份资源当作基础版本生产环境必须有增量更新机制。我一般每年至少同步一次银联最新的 BIN 发布文件或者在业务查询接口里对未命中的 BIN 做记录日志积累一个“未知 BIN”表定期人工核对补录。没有更新机制的 BIN 表用一年以上识别率会明显下降。5.4 坑四CSV 导入 MySQL 中文乱码现象LOAD DATA 导入后bank_name 字段显示乱码比如“涓埚”这样的字符。原因Excel 另存的 CSV 虽然选了 UTF-8但 Windows 版 Excel 的 UTF-8 编码可能带 BOM或者原始数据本身是 GBK 编码。LOAD DATA 语句里如果没有显式指定 CHARACTER SETMySQL 会按表的默认字符集解释两边对不上就乱码。解决导入前用文本编辑器确认 CSV 编码导入语句里强制加CHARACTER SET utf8mb4。如果 CSV 是 GBK可以先转成 UTF-8 再导用 iconv 命令最直接iconv -f GBK -t UTF-8 bin_data.csv bin_data_utf8.csv5.5 坑五BIN 归属变更线上还在用旧映射现象某银行卡段原来的发卡行是 A 银行一段时间后交易数据显示该卡段交易到了 B 银行的清算系统。原因银行合并、卡产品迁移、联名卡合作到期都会导致 BIN 段归属变化。BIN 数据不是静态字典它像路由表一样跟着金融机构的业务走。解决把 BIN 表落地成“版本化管理”。我给每张 BIN 表增加 updated_at 字段导入时记录数据批次时间线上系统优先引用最新版本。变更发生时用 UPDATE 语句修正归属并记录变更日志不要直接删除旧记录“查不到”和“查到旧归属”完全是两种排查路径。6. 进阶用法用连接池和 Flask 搭一个卡 BIN 自助查询接口最后这个例子是把 BIN 表从“自己能查”升级成“团队能用”。在 Flask 服务里引入 dbutils 连接池避免每次请求都新建 MySQL 连接连接建立和销毁的开销在高频查询下非常可观。代码里我还会把查询结果落一份 Excel 导出接口方便业务同学自助拉数据。from flask import Flask, request, jsonify import pymysql from dbutils.pooled_db import PooledDB import pandas as pd app Flask(__name__) POOL PooledDB( creatorpymysql, maxconnections10, mincached2, blockingTrue, host127.0.0.1, port3306, userroot, passwordyour_pass, databasebank_bin, charsetutf8mb4, ) app.route(/bin/info, methods[GET]) def bin_info(): card_no request.args.get(card_no, ) if len(card_no) 6: return jsonify({code: 1, msg: card_no 至少 6 位}) conn POOL.connection() try: with conn.cursor() as cur: cur.execute( SELECT bin, card_name, card_type, bank_name, is_credit FROM bank_bin WHERE bin IN (LEFT(%s, 8), LEFT(%s, 6)) ORDER BY LENGTH(bin) DESC LIMIT 1 , (card_no, card_no)) row cur.fetchone() if not row: return jsonify({code: 2, msg: BIN 未命中}) return jsonify({ code: 0, bin: row[0], card_name: row[1], card_type: row[2], bank_name: row[3], is_credit: row[4], }) finally: conn.close()这个接口里有两个细节值得留意。WHERE 条件里用IN (LEFT(%s, 8), LEFT(%s, 6))配合ORDER BY LENGTH(bin) DESC LIMIT 1正好把第五章避坑部分提到的 6/8 位查询问题在接口层解决了每次请求从连接池取连接用完归还maxconnections 控制在 10低频内部工具这个量级足够连接数开太大反而拖累 MySQL 自身性能。接口跑起来后我习惯再追加一个批量导出的路由用 pandas 把 SQL 查询结果转成 DataFrame直接写 Excel 返回给前端下载。这里的 to_excel 依赖 openpyxl 引擎首次使用前pip install openpyxl装一下就行。app.route(/bin/export, methods[GET]) def bin_export(): conn POOL.connection() try: df pd.read_sql(SELECT bin, bank_name, card_type FROM bank_bin ORDER BY bin, conn) output_path /tmp/bin_export.xlsx df.to_excel(output_path, indexFalse) return jsonify({code: 0, path: output_path}) finally: conn.close()从那以后我每次接新的 BIN 数据文件都强制走一遍“Excel 体检 → 清洗 → 建表 → 导库 → 查询验证”全流程6 位 8 位兼容逻辑、发卡行名称归一化、增量更新提醒一个环节都不省。这套习惯帮我少踩了太多隐蔽的坑尤其那种线上跑了几个月才发现识别率不对的翻车现场代价远比想象中大。BIN 表看着是静态数据用好了就是支付链路里最扎实的地基。希望帮到你。本文还有配套的精品资源点击获取
返回列表