简介:《SWIFT报文格式手册.doc》是一份面向银行国际结算人员、外贸单证操作者及金融从业者的2023年度SWIFT报文格式指南,重点梳理自2023年11月18日生效的报文更新,覆盖MT7XX系列新增、删除和修改情况,并围绕MT700/701开立跟单信用证逐一讲解字段含义、M/O强制与可选标识、格式要求和操作准则。包含1个doc文档,压缩包大小约414KB,便于查阅和打印。已有275人学习/下载,适合作为信用证处理、国际结算报文撰写和教学培训的基础参考。内容包括报文格式、屏幕格式、屏幕计算、来报解析类别,以及信用证开立、修改、通知流程中发报行与收报行的密押关系和安全传递要点,帮助读者快速掌握2023年SWIFT报文变化并规范实务操作。
1. SWIFT报文格式手册.doc:不是一份可读文档,而是一套字段级接口契约
做跨境支付、信用证或者银行间头寸调拨,多半见过这份SWIFT报文格式手册.doc。它不像普通说明书,更像一本字段级接口契约:业务场景对应哪条报文,字段放在哪、什么格式、选填还是必填,全在表格里。真正用到它,是联调、构造测试报文、解析上游来报和上线前回归。没它,一段MT103就像黑匣子;照着拆,才能还原业务语义。这篇笔记按实际接SWIFT报文的顺序来:怎么把手册读薄,怎么写解析器,最常翻车的五个细节,以及怎么把手册变成自动化校验规则。适合正在啃报文格式的工程师。
2. 读懂SWIFT报文格式手册:先把MT报文骨架拆清楚
拿到手册,先别急着翻字段定义页,先看目录。SWIFT报文格式手册.doc通常是按报文类型组织的,MT103和MT202各自成章。同一套字段号会反复出现,但含义跟着报文类型走,不能全局套用。先把目录里和你相关的MT圈出来,再进入单条报文的字段细节。
2.1 报文头、正文与尾部:先看{1:}到{5:}的物理结构
SWIFT MT报文长的样子不是一行平铺数据,而是五个花括号块组成的结构。用MT103举例,最直观:
{1:F01BANKCNBJAXXX0000000000} {2:I103BANKGB2LXXXXN3000} {3:{108:SAMPLE001}} {4: :20:REF20240617001 :23B:CRED :32A:240617USD100000,00 :50K:/12345678 ABC TRADING CO. LTD :59:/GB33BARCLAY2020 JOHN SMITH :70:/INV/INV-2024-0088 :71A:SHA -} {5:{CHK:123456789ABC}}{1:}是基础头,固定以F01开头,后面跟发报行标识。{2:}是应用头,最要紧的是第二位的字母,I代表输入到SWIFT网络的报文,O代表SWIFT网络下发的报文,这个差异直到联调阶段才容易暴露。{3:}是用户头,里面常见{108:}存放交易参考号,很多系统靠它做对账和去重。{4:}是正文字块,字段都在这里;手册里的“字段定义”也绝大多数针对这个块。{5:}是尾部,常见{CHK:}校验和,解析时无需关心。
手册的核心内容,就是对{4:}块里每条字段的格式描述。理解了物理结构之后,看任何MT报文都不会再被“一堆花括号”吓住,所有业务信息都在{4:}里排队等待解析。
2.2 MT103、MT202、MT199、MT940:对接前先分清四类报文
同一个字段标签在不同MT里含义可能完全不同。最典型的例子是50a和59a:MT103里50a是付款人、59a是收款人,但MT202里更多时候只关心58a,50a根本不存在。如果一开始就指望“同一套字段解析逻辑通吃”,很快会被退报信得晕头转向。
| 报文 | 中文名 | 典型业务 | 关键字段 |
|---|---|---|---|
| MT103 | 单笔客户汇款 | 跨境电汇、贸易货款 | 20、32A、50a、59a、70、71A |
| MT202 | 金融机构转账 | 代理行清算、头寸调拨 | 20、21、32A、57a、58a |
| MT199 | 自由格式报文 | 查询、退报说明、人工处理 | 20、21、79 |
| MT940 | 客户对账单 | 余额对账、入账流水 | 20、25、28、60F、61、62F |
一个支付项目通常先支持MT103,再接MT202;MT199只用于异常场景和联调救援。MT940表面上跟支付无关,但清结算核对往往靠它,如果你负责的是完整资金链路,建议顺手把MT940的字段也列进支持清单。读手册时,先确认自己负责的是哪一类,再决定要不要精读后面那些字段页。
2.3 字段标签命名:F20、32A、50a这些代号意味着什么
手册里字段标签有约定:两位数字是字段号,紧跟着的字母表示格式变体。比如32A固定是“起息日+币种+金额”,32C在别的报文里可能另有含义。同一个标签出现在不同MT中,业务含义由所在报文的字段页决定,不存在全局统一解释。
还有一个容易绕晕的约定:标签里大写和小写意义不同。32A的大写A是字段格式的一部分;50a的小写a表示“A/F/K三个变体任选其一”,具体用哪个看报文内容。常见做法是先把50A、50F、50K的字符集和行长都查出来,再决定用哪个变体,避免在50F里塞进50K才能放的中文公司名。
格式写法上,手册沿用SWIFT的格式简写:n是数字,a是字母,x是SWIFT字符集内任意字符,d是可带小数分隔符的数字。比如32A写成6n3a15d,含义是日期6位数字、币种3个字母、金额最多15位。多行字段用“4*35x”,表示最多4行、每行35个字符。把这张简写表记熟,读手册的速度能快一倍。
3. 写一个能跑通的SWIFT报文解析器:从.doc规范到代码
从手册到代码,有个不能跳过的中间步骤:先把.doc里的表格捞出来,变成结构化的字段规则。我一般按这个顺序走:转文本、扫目录、切块、建规则表,最后再写解析循环。顺序反了容易在报文样例上浪费时间。
3.1 先把.doc手册转成可搜索的文本,乱码坑一次说清
银行里的“SWIFT报文格式手册.doc”大多是内部从官方PDF再整理的,带着批注和历史痕迹。直接拿Word打开看没问题,想程序化检索就要先转纯文本。我的做法是用LibreOffice做批量化转换:
mkdir -p ./swift_manual_txt libreoffice --headless --convert-to txt:Text "./SWIFT报文格式手册.doc" -o ./swift_manual_txt/转换完第一件事不是看内容,而是检查编码。老.doc文件常以GBK存中文字符,转出来的txt在你的终端里可能是一堆“锟斤拷”。解决方法是先转一次码,或者用grep按字段号定位:
grep -n "32A" ./swift_manual_txt/SWIFT报文格式手册.txt | head -40如果grep结果全是乱码,再用iconv转成UTF-8:
iconv -f GBK -t UTF-8 ./swift_manual_txt/SWIFT报文格式手册.txt > swift_manual_utf8.txt注意:转换后的txt里表格线会变成一串加号和横线,字段名可能和格式描述串行。此时别直接拿整段文本去做自动抽取,先按字段号定位,逐条人工核对格式列,再进规则表。
另外,如果能拿到官方PDF版,建议直接以PDF为准。内部.doc经过多人编辑,容易出现“字段变了但备注没改”的情况;解析代码的注释里应该标注依据的手册版本,以免后续对接方拿着新版质问。
3.2 最小实现:用Python按{4:}块切出所有字段
拿到手册里的标签清单后,第一段代码建议做这件事:把{4:}正文块切成(tag, value)的列表。有了这个列表,后续所有格式校验、必填判断、入库映射都建立在统一的数据结构上。
import re def split_swift_text(block_text: str) -> list[tuple[str, str]]: """把 {4: 开头,} 结尾的正文块切成字段列表。""" if block_text.startswith("{4:"): block_text = block_text[len("{4:"):] if block_text.endswith("}"): block_text = block_text[:-1] fields = [] lines = block_text.replace("\r\n", "\n").split("\n") current_tag = None current_value: list[str] = [] for line in lines: line = line.strip() if not line: continue if line == "-": # SWIFT正文块以单独一行 '-' 结束 break m = re.match(r"^:([0-9]{2}[A-Z]?):(.*)$", line) if m: if current_tag is not None: fields.append((current_tag, "\n".join(current_value))) current_tag = m.group(1) current_value = [m.group(2).strip()] else: if current_tag is not None: current_value.append(line) if current_tag is not None: fields.append((current_tag, "\n".join(current_value))) return fields这段代码有三个关键点。第一,先把CRLF统一成LF,因为不同模拟器对行尾的处理不一致,统一后再切分才能稳定。第二,单独处理结束符“-”,否则它会被当成72或79字段的续行,污染整个字段表。第三,字段值是多行时,要一直追加到下一个“:标签:”出现才收口,这就是50a和59a能跨4行的原因。
跑一遍最简单的样例:
if __name__ == "__main__": sample = "{4:\n:20:REF001\n:32A:240617USD1000,00\n:50K:/ACC12345\nACME INC\n-}" for tag, value in split_swift_text(sample): print(tag, "=>", value[:40])期望输出是20对应REF001,32A对应240617USD1000,00,50K对应两行文本。如果输出里混入了“-”或空行,说明结束符处理或strip逻辑写错了位置。
3.3 解析32A:日期、币种、金额的字段格式校验
先把最常用的字段解析出来,32A是最典型的:6位日期、3位币种、最多15位金额,金额里用逗号当小数点。这就是手册里“6n3a15d”的直接落地:
def parse_32a(value: str) -> dict: """ 解析 32A,格式固定为 6n3a15d。 示例:240617USD1234,56 """ m = re.fullmatch(r"(\d{6})([A-Z]{3})([0-9,]+)", value) if not m: raise ValueError(f"32A 格式无法解析: {value}") date_part, currency, amount = m.groups() return { "date": f"20{date_part[0:2]}-{date_part[2:4]}-{date_part[4:6]}", "currency": currency, "amount": amount.replace(",", "."), }参数说明:年份只有两位,代码里默认补20xx。如果你的系统会处理跨世纪报文,建议把它做成可配置参数,不要写死。金额里的逗号必须转成小数点后再入库,否则落到Oracle或PostgreSQL里会被当成字符串,对账时按数字比较就会出错。
3.4 字段边界与转义规则:三个容易解析错的位置
第一是冒号。标签以“:32A:”分隔,而x类型字段本身允许半角冒号。如果字段值里出现“:59:”这种片段,一个简单的全文split会把它当成新字段,导致后面的内容全部错位。按行解析能避开大多数情况,因为正常报文不会在续行开头放冒号;但对方如果手工拼报文,就可能在换行处踩中,所以解析出异常长字段时要打日志。
第二是换行。50a、59a这类多行字段,手册写成4*35x。切分时如果见到换行就当作新字段,多行地址会被拆碎;反过来,直接把整块文本按行切,又会被续行干扰。只有“等下一个冒号标签出现才收口”的策略最稳。
第三是花括号。{4:}里的字段值理论上不能出现裸花括号,因为物理格式把它当块分隔符。联调时经常见到有人把地址里的全角括号写成半角,结果{5:}识别失败,整条报文被对端当作格式错误退回。这个坑在模拟器里未必会暴露,因为模拟器对括号的校验经常放水。
4. 对接SWIFT报文格式手册:五个常见坑与排查
这一章是从真实联调里攒出来的问题清单。每一条都按现象、原因、解决三个步骤拆开,方便直接对照排查。
4.1 坑一:MT与MX混用,字段和报文整个对不上
现象:合作行说“报文已经发出”,你按MT103去解析,结果拿到一串以<FIToFicstmrCdtTrf>开头的XML,一个冒号标签都见不到。
原因:SWIFT有MT和MX两套体系。MT基于ISO 15022,MX基于ISO 20022,两者表达同一笔汇款的方式完全不同。手册封面写着MT,但上游通道实际给的是MX,解析逻辑自然全部落空。
解决:对接前期先确认渠道给的是MT还是MX。解析入口处加一个前置判断:报文以{2:开头还是以XML命名空间开头,后续走不同解析分支。不要试图让一个解析器同时兼容两种格式,那会让字段映射到处是if-else。
4.2 坑二:字段可选性误判,必填校验放错位置
现象:本地系统校验MT103全部通过,发到对端却被退报,原因是50a缺失。本地库里50a明明允许为空。
原因:手册里每个字段标了M、O、C三种属性;M是必选,O是可选,C是条件必选。很多工程师看到“50a是付款人”,下意识把它当成全局必填,但MT202根本不含50a。反过来,MT103里50a是必选,自己的系统却用全局可选配置漏掉了。
解决:必填校验必须绑定到MT级别,不能做全局规则。我维护字段规则表时用三个键:字段标签、适用MT、条件表达式。条件表达式单独写函数,比如“挡板是付款人字段,当且仅当报文类型是MT103且存在50F/50A/50K之一时必填”。这样写出来的规则,每个MT都有自己那一份。
4.3 坑三:字符集与长度,中文和超长字段直接翻车
现象:50F字段放了中文公司名,对端解析出来全是问号;59a第四行被截断,收款人账号缺了两位。
原因:MT报文标准字符集里没有中文,x类型字段只接受SWIFT基本字符集。中文进入网络前需要做字符集过滤或全角转半角处理。另一个隐蔽原因是长度单位:手册里的35x是按字符数计算,但中间件经常按UTF-8字节数截断。一个中文占3字节,自然截错位置。
解决:对外发送的报文字段,先做全角转半角,剔除不在标准字符集里的字符;对内存储统一按字符数截断,并保留截断前后的日志。字符集问题在模拟器里很难暴露,因为模拟器校验宽松;所以上线前一定要拿真实网络样本跑一遍。
4.4 坑四:报文头方向差异,I/O开头不一样
现象:解析器按输入的{2:I103...}读取块内容,真实网络回执里却是{2:O103...},字段对不上,还报了时间字段非法。
原因:{2:}块里,I开头表示发送到SWIFT网络的输入报文,O开头表示SWIFT网络下发的输出报文,二者后边携带的地址、时间字段排列不同。手册多数情况下按O展示,部分模拟器却按I生成,解析时一旦忽略方向位,字段会全部错位。
解决:解析{2:}时先读出第二位是I还是O,再决定后续字段怎么切。路由、去重、审计都得用完整头信息,不要只截{4:}正文。建议在代码里把“方向”和“报文类型”存成两个独立字段,排查问题时能直接定位。
4.5 坑五:手册版本更新,字段定义被悄悄改掉
现象:去年跑得好好的校验规则,今年同一段报文过不了,报错指向20字段格式。
原因:SWIFT每年发布技术更新,可能调整可选性、新增代码值、甚至废弃某个字段。网盘里流传的旧版.doc不会自动更新,线上规则表还停在上一版,新旧版本一对照就出问题。遇到过字段标签直接作废、换成新标签的情况。
解决:手册要标版本号,规则表也要标“依据手册版本”。每年新规生效前,从官方网站拉差异清单,对照规则表逐条修改,再跑一遍历史报文回归。不要轻信“这份手册一直没变”,报文格式是每年有变化的。
5. 进阶用法:把手册做成自动化校验器,用“黄金报文”夹住回归
5.1 字段规则表:从手册描述到可执行的JSON
手册里“20”那一行写的是“16x必选”,翻译成规则表就是一个JSON条目。我习惯把规则表放在项目仓库里,跟着代码一起走版本控制,这样手册更新时能直接看到diff:
{ "20": {"required": true, "max_length": 16}, "32A": {"required": true, "regex": "^\\d{6}[A-Z]{3}[0-9,]+$"}, "50a": {"required": true, "variants": ["50A", "50F", "50K"]} }提示:JSON里写正则时,反斜杠要转义;如果规则多,建议单独建一个rules文件,不要硬塞进主代码文件里。
5.2 用规则表做批量校验
把前面写好的split_swift_text和这张规则表接在一起,就是一个最小可用的校验器:
def validate_swift(payload: str, rules: dict) -> list[str]: fields = split_swift_text(payload) errors = [] for tag, value in fields: rule = rules.get(tag) if rule is None: errors.append(f"未知字段: {tag}") continue if "regex" in rule and re.search(rule["regex"], value) is None: errors.append(f"{tag} 不符合格式: {value[:40]}") if "max_length" in rule and len(value) > rule["max_length"]: errors.append(f"{tag} 超长: {len(value)} 字符") return errors逻辑说明:先把未知字段找出来,这是最容易被忽略的坑;再做格式和长度校验。通用循环只做“能确定的事”,字段之间的联动校验,比如C类条件必选,需要单独写函数。不要让通用循环变成一堆业务if-else。
5.3 生产环境验证习惯:维护一组“黄金报文”
我从老同事那里学来的习惯:每个报文类型建一个samples目录,里面分“正例”和“反例”。正例从对端或模拟器拿真实报文,反例是故意缺字段、超长、坏格式的样本。每次改动解析器或更新规则表,跑一遍测试:
pytest test_swift_parser.py -q所有反例必须有断言,断言的是“一定会报错”,不是“应该没事”。这个习惯帮我拦下过两次版本更新引发的回归。如果你用C#维护同样规则,把校验逻辑写成DataAnnotations也能达到相同效果,关键是规则和报文样例一起进版本库,而不是只存代码。
这些年做报文对接,我最大的感受是:报文格式不难,难的是所有细节都来自手册而不是经验。每次对不上,先怀疑版本和方向,再怀疑字符集,最后才怀疑自己标签写错。按这个顺序排查,能少走很多弯路。希望这篇笔记能帮到你,也祝你那头联调一次过。
本文还有配套的精品资源,点击获取