
简介这份资源面向需要银行网点数据的PHP开发者与数据采集学习者提供全国银行列表的抓取与落地实现。数据覆盖行号、网点名称、电话、地址等字段共123464条记录可用于金融类系统开发、网点查询功能搭建或数据清洗练习。压缩包共3个文件约3.97MB包含一份PHP程序文件、一份已导出的SQL数据文件以及一份说明文档PHP程序基于5.5.38环境通过curl配合正则表达式抓取数据SQL文件可直接导入MySQL使用省去重复采集的时间成本。目前已有912人学习下载。读者可从中获得完整的采集思路、可运行的脚本代码以及现成的数据表结构既能直接用于项目也适合作为爬虫与正则匹配的实战参考便于快速理解从请求到入库的完整链路。1. 银行行号数据这件事难点从来不在“抓”第一次接到“获取全国银行列表”这个需求时多数人的第一反应是写个爬虫把行号、网点名称、电话、地址抓下来存库就完事。真动手才发现坑根本不在抓取本身而在数据源的选择、字段口径的统一、以及后续增量更新的稳定性。银行行号也叫联行号、CNAPS 号是 12 位数字编码用于跨行支付清算一个网点对应一个行号全国总量在十几万到二十万条量级且每天都有网点新增、撤并、迁址。你要做的不是一次性导出而是维护一份能持续对齐的“银行网点主数据”。这套东西适合做支付路由、对公开户校验、发票抬头匹配、批量代付的团队也适合需要本地化银行字典的后端和数据分析同学。下面把我实际落地过的路径拆开讲包括数据从哪来、字段怎么清洗、增量怎么跑、哪些地方最容易翻车。2. 数据源选型官方口径、聚合接口与自建字典的取舍2.1 三种主流来源的真实差异做银行列表绕不开三类数据源。第一类是清算机构对外公布的参与机构信息权威性最高字段规范但通常只给到机构层级网点级明细不一定全更新节奏也偏慢。第二类是第三方数据服务商的聚合接口按查询次数计费字段齐全、更新快缺点是行号与网点名称的对应关系偶尔滞后撤并网点可能还挂着。第三类是自己维护字典从多个来源交叉比对后落库前期投入大但长期最可控。我一般会这样组合以官方口径作为基准表用聚合接口做增量补充和校验最终落到自己的字典表里。判断一个来源能不能用看四个指标——覆盖率是否含村镇银行、农信社、字段完整度电话、地址是否为空、更新频率日更还是月更、以及行号准确性能否与清算报文对上。覆盖率低于 90% 的来源直接放弃做主表。2.2 表结构设计与字段口径统一字段口径不统一是后期最大的返工来源。同一个“地址”有的来源给的是注册地址有的是营业地址电话有的是总机有的是对公柜台。落库前必须定死语义。下面是我常用的建表语句字段注释写清楚口径后面接任何来源都往这个结构上映射。CREATE TABLE bank_branch ( bank_code VARCHAR(20) NOT NULL COMMENT 银行大类编码如 ICBC, bank_name VARCHAR(64) NOT NULL COMMENT 银行名称如中国工商银行, branch_no VARCHAR(12) NOT NULL COMMENT 联行号/行号12位数字主键, branch_name VARCHAR(128) NOT NULL COMMENT 网点全称含支行/分理处层级, province VARCHAR(32) DEFAULT NULL COMMENT 省/直辖市, city VARCHAR(32) DEFAULT NULL COMMENT 地级市, district VARCHAR(32) DEFAULT NULL COMMENT 区县, address VARCHAR(256) DEFAULT NULL COMMENT 营业地址非注册地址, phone VARCHAR(64) DEFAULT NULL COMMENT 对公联系电话多个用逗号分隔, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0撤并 2待核实, source VARCHAR(32) NOT NULL COMMENT 数据来源标识, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (branch_no), KEY idx_bank_city (bank_code, city), KEY idx_name (branch_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT银行网点主数据;branch_no做主键是刻意的行号天然唯一用它做幂等写入最省事。status字段别省撤并网点不能物理删除否则历史订单对不上。source记录来源方便出问题时回溯是哪一批数据带进来的脏值。address明确写“营业地址”避免不同来源混入注册地址导致同一网点出现两条记录。2.3 采集与清洗的最小可跑流程不管数据来自接口还是文件统一走“拉取 → 解析 → 映射 → 校验 → 入库”五步。下面是一个 Python 清洗脚本的骨架重点看字段映射和校验逻辑而不是具体某个来源的解析细节。import re import pymysql # 行号必须是12位纯数字这是硬校验不满足直接丢弃 BRANCH_NO_RE re.compile(r^\d{12}$) def normalize_phone(raw): 把各种分隔符统一成逗号去掉分机号后缀 if not raw: return None parts re.split(r[;,、/\s], raw.strip()) cleaned [] for p in parts: p re.sub(r[-—]?\d{1,5}$, , p) if p.count(-) 2 else p p p.strip() if p: cleaned.append(p) return ,.join(cleaned) if cleaned else None def map_record(src): 把来源字段映射到主数据结构来源不同只改这里 branch_no str(src.get(cnaps_code, )).strip() if not BRANCH_NO_RE.match(branch_no): return None # 行号非法丢弃并记日志 return { bank_code: src.get(bank_type, ).strip().upper(), bank_name: src.get(bank_name, ).strip(), branch_no: branch_no, branch_name: src.get(branch_name, ).strip(), province: src.get(province), city: src.get(city), district: src.get(district), address: src.get(address), phone: normalize_phone(src.get(phone)), status: 1, source: src.get(_source, unknown), } def upsert(conn, rec): 按行号幂等写入存在则更新不存在则插入 sql INSERT INTO bank_branch (bank_code,bank_name,branch_no,branch_name,province,city,district,address,phone,status,source) VALUES (%(bank_code)s,%(bank_name)s,%(branch_no)s,%(branch_name)s,%(province)s, %(city)s,%(district)s,%(address)s,%(phone)s,%(status)s,%(source)s) ON DUPLICATE KEY UPDATE branch_nameVALUES(branch_name), addressVALUES(address), phoneVALUES(phone), statusVALUES(status), sourceVALUES(source) with conn.cursor() as cur: cur.execute(sql, rec)normalize_phone里那个分机号处理逻辑是血泪经验很多来源的电话字段带“-8021”这种分机直接入库后前端展示会串味所以对含两个以上连字符的号码做尾部截断。map_record把来源差异全部收敛在一个函数里换数据源只改这一处不动入库逻辑。upsert用ON DUPLICATE KEY UPDATE保证重复跑不会产生脏数据这是增量更新的基础。3. 增量更新与行号校验让字典长期可用3.1 增量比对策略与调度一次性全量导入只是起点真正决定这套字典能不能长期用的是增量机制。我的做法是每天拉一次全量快照与库内做 diff而不是依赖来源的增量接口——增量接口的“变更”定义各家不一样有的只给新增不给撤并反而更难对齐。diff 逻辑很简单来源里有、库里没有的插入两边都有但关键字段网点名称、地址、电话不一致的更新并记录变更日志库里有、来源里没有的标记为待核实连续 N 天未出现再置为撤并。def diff_and_apply(conn, incoming): incoming: {branch_no: record} 来源全量快照 with conn.cursor(pymysql.cursors.DictCursor) as cur: cur.execute(SELECT branch_no, branch_name, address, phone FROM bank_branch WHERE status1) local {r[branch_no]: r for r in cur.fetchall()} to_insert [v for k, v in incoming.items() if k not in local] to_update [] for k, v in incoming.items(): if k in local: old local[k] if (old[branch_name] ! v[branch_name] or old[address] ! v[address] or old[phone] ! v[phone]): to_update.append(v) missing [k for k in local if k not in incoming] for rec in to_insert: upsert(conn, rec) for rec in to_update: upsert(conn, rec) if missing: with conn.cursor() as cur: cur.executemany( UPDATE bank_branch SET status2 WHERE branch_no%s, [(m,) for m in missing]) conn.commit() return len(to_insert), len(to_update), len(missing)调度上全量快照建议放在凌晨低峰单次十几万条 diff 在普通 MySQL 上几分钟能跑完。missing不直接删先置status2给人工或二次来源确认留窗口这是后悔药。3.2 行号校验的三种手段行号错了支付路由就会打到错误网点后果比数据缺失严重得多。校验分三层格式校验12 位纯数字前几位对应银行大类、来源交叉校验两个独立来源同一行号指向同一网点名称才算可信、以及业务校验拿真实清算报文里的行号反查字典命中率低于阈值就告警。格式校验在入库时做交叉校验在合并阶段做业务校验放在上线后做监控。校验层触发时机判定规则失败处理格式校验入库前12 位数字前缀匹配银行大类丢弃并记日志交叉校验多源合并两源网点名称相似度 ≥ 0.9标记待核实业务校验上线后报文行号命中率 ≥ 99%触发告警排查3.3 电话与地址字段的清洗边界电话和地址是最容易“看起来对、用起来错”的字段。地址不要做过度标准化把“XX路 88 号”强行改成“XX路88号”反而可能和来源对不上保留原始形态、只做去空格和全半角统一即可。电话字段要区分对公和对私很多来源混在一起我的处理是只保留看起来像固话或对公手机的号码明显是个人手机号的比如来源标注为联系人手机单独存一列不混进主电话字段。这一步没有通用规则得按来源逐个看样本别指望一个正则打天下。4. 避坑与排查那些让字典“看着全、用着错”的细节4.1 坑一行号前导零被吞现象导入后部分行号变成 11 位支付路由报“行号不存在”。原因来源是 Excel 或 CSV行号列被当成数字读取前导零丢失。解决读取时强制按字符串处理pandas 用dtype{cnaps_code: str}CSV 用csv.reader而非DictReader的自动类型推断入库前再过一遍 12 位正则。4.2 坑二同一网点多条记录现象按网点名称查询返回两三条行号却不同。原因来源里同时存在旧行号和迁址后的新行号或不同来源对同一网点给了不同编码。解决以行号为主键天然去重但要在合并阶段按“网点名称 地址相似度”做聚类把疑似同一网点的记录标出来人工确认别自动合并。4.3 坑三撤并网点未及时下线现象代付打款到已撤并网点被退回。原因来源更新滞后或增量逻辑只处理新增不处理消失。解决坚持全量 diffmissing置status2并设观察期同时接入业务侧失败报文反查把真实失败的行号优先置为撤并。4.4 坑四电话字段混入客服热线现象大量网点电话都是同一个 95 开头号码。原因来源把总行客服电话填到了每个网点。解决统计电话字段的重复度出现频次异常高的号码列入黑名单清洗时置空而不是保留避免误导用户。4.5 坑五字符集导致网点名称乱码现象部分村镇银行名称显示为问号或方块。原因来源文件是 GBK入库连接是 utf8mb4中间没转码。解决读取时显式指定编码入库连接串加charsetutf8mb4建表也用 utf8mb4三处必须一致缺一处就翻车。5. 把字典用起来查询接口与命中率验证的一个小技巧字典建好只是半成品真正体现价值的是查询侧。我一般会封一个按行号、按名称、按城市三个维度的查询接口其中按名称查询要做模糊匹配和拼音首字母支持因为用户输入“工行北京海淀”这种口语化关键词是常态。下面这个查询函数把三个维度揉在一起优先精确命中行号其次名称前缀最后城市过滤返回结果按匹配度排序。def search_branch(conn, keywordNone, branch_noNone, cityNone, limit20): 三维度查询行号精确 名称模糊 城市过滤 if branch_no: with conn.cursor(pymysql.cursors.DictCursor) as cur: cur.execute(SELECT * FROM bank_branch WHERE branch_no%s AND status1, (branch_no,)) return cur.fetchall() sql SELECT * FROM bank_branch WHERE status1 params [] if keyword: sql AND (branch_name LIKE %s OR bank_name LIKE %s) params [f%{keyword}%, f%{keyword}%] if city: sql AND city%s params.append(city) # 名称越短通常越接近用户想找的网点用它做粗排序 sql ORDER BY CHAR_LENGTH(branch_name) ASC LIMIT %s params.append(limit) with conn.cursor(pymysql.cursors.DictCursor) as cur: cur.execute(sql, params) return cur.fetchall()ORDER BY CHAR_LENGTH(branch_name)这个排序技巧不完美但实用用户搜“海淀支行”短名称的网点大概率就是目标长名称的往往是“海淀支行营业部”这类下级。真要做得更准可以加一列拼音首字母用LIKE hd%兜底。验证字典质量我有个习惯每周抽 200 条真实业务报文里的行号反查字典统计命中率和字段完整率把未命中的行号单独存一张表作为下周数据源比对的输入。这个动作坚持三个月字典的准确率会从“能用”变成“敢用”。别指望一次导入就一劳永逸银行网点数据是活的维护它的过程本身就是这套方案的一部分。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。