在PowerBuilder里手写XML序列化——没有现成库的年代怎么拼报文
在PowerBuilder里手写XML序列化——没有现成库的年代怎么拼报文
PB 9.0原生没有XML序列化,但医保接口的对接需要XML格式的报文。没有库、没有现成的工具——全靠字符串拼接和OLE调用MSXML自己造一套。这篇记录在PB里从零手写XML编解码的过程:转义字符处理、属性拼接、嵌套元素递归、节点解析。
文章目录
- 在PowerBuilder里手写XML序列化——没有现成库的年代怎么拼报文
-
- 一、PB和XML之间的一道鸿沟
- 二、XML生成——递归构建节点树
- 三、XML解析——MSXML反向抽取
- 四、XML生成 vs 解析的策略选择
- 五、亮点总结
- 六、适用场景
- 七、扩展方向
- 八、结语
一、PB和XML之间的一道鸿沟
医保平台升级后,接口格式从定长报文改成了XML。医院HIS是PB写的,服务端是Java,中间的数据报文要用XML传。
问题来了——PB没有 XmlSerializer,没有 XDocument,没有 JAXB。只有两样东西可以用:字符串拼接和OLE调用MSXML。
最简单的方案是字符串拼XML,但写了几十条接口后发现——不同地方的字符串拼法不一样,有的没做转义导致 & 变成非法字符报错,有的嵌套多层全塞在一个方法里改不动。必须抽象出一套通用的工具。
二、XML生成——递归构建节点树
// nv_xml_builder - 自定义类,递归构建XML
// 实例变量
string is_xml // 最终XML字符串
boolean ib_indent // 是否格式化缩进
integer ii_level // 当前缩进层级
// constructor
constructor:
is_xml = '<?xml version="1.0" encoding="UTF-8"?>~r~n'
ii_level = 0
end constructor
// 设置是否格式化
public subroutine of_setformatted(boolean ab_formatted)
ib_indent = ab_formatted
end subroutine
// 生成缩进
private function of_indent returns string
string ls_indent = ""
int i
if not ib_indent then return ""
for i = 1 to ii_level
ls_indent += " "
next
return ls_indent
end function
// 打开节点(带属性)
public subroutine of_open_tag(string as_name, string as_attrs[])
is_xml += of_indent() + "<" + as_name
int i
for i = 1 to upperbound(as_attrs) step 2
is_xml += " " + as_attrs[i] + '="' + as_attrs[i+1] + '"'
next
is_xml += ">~r~n"
ii_level++
end subroutine
// 写入节点值(处理转义)
public subroutine of_write_tag(string as_name, string as_value)
is_xml += of_indent() + "<" + as_name + ">"
is_xml += of_escape(as_value)
is_xml += "</" + as_name + ">~r~n"
end subroutine
// 关闭节点
public subroutine of_close_tag(string as_name)
ii_level--
is_xml += of_indent() + "</" + as_name + ">~r~n"
end subroutine
// 获取最终XML
public function of_getxml returns string
return is_xml
end function
// XML转义:& < > " '
private function of_escape(string as_str) returns string
as_str = replace(as_str, "&", "&")
as_str = replace(as_str, "<", "<")
as_str = replace(as_str, ">", ">")
as_str = replace(as_str, '"', """)
as_str = replace(as_str, "'", "'")
return as_str
end function
五个方法覆盖了XML生成的所有场景:开标签、写值、关标签、属性、转义。调用时按XML结构逐级嵌套:
nv_xml_builder lxml
lxml = create nv_xml_builder
string ls_attrs[]
ls_attrs[1] = "infno"
ls_attrs[2] = "1101"
ls_attrs[3] = "msgid"
ls_attrs[4] = "20260525120001"
lxml.of_open_tag("TCS101Message", ls_attrs)
ls_attrs[1] = "" // 子节点无属性
ls_attrs[2] = ""
lxml.of_open_tag("MessageHead", ls_attrs)
lxml.of_write_tag("SenderId", "T46100009000")
lxml.of_write_tag("ReceiverId", "T46100008000")
lxml.of_write_tag("SendTime", "20260525120001")
lxml.of_close_tag("MessageHead")
lxml.of_open_tag("MessageBody", ls_attrs)
// 递归写入业务数据
lxml.of_write_nodefromdw(dw_person, "personInfo", ...)
lxml.of_close_tag("MessageBody")
lxml.of_close_tag("TCS101Message")
string ls_xml = lxml.of_getxml()
生成的XML:
<?xml version="1.0" encoding="UTF-8"?>
<TCS101Message infno="1101" msgid="20260525120001">
<MessageHead>
<SenderId>T46100009000</SenderId>
<ReceiverId>T46100008000</ReceiverId>
<SendTime>20260525120001</SendTime>
</MessageHead>
<MessageBody>
...
</MessageBody>
</TCS101Message>
三、XML解析——MSXML反向抽取
XML的生成可以用字符串拼,但解析不行——字符串split对付不了嵌套。PB的做法是调用Windows内置的MSXML组件:
// nv_xml_reader - 基于OLE MSXML的XML解析器
OLEObject iole_xml
constructor:
iole_xml = create OLEObject
iole_xml.ConnectToNewObject("Msxml2.DOMDocument.4.0")
end constructor
// 加载XML字符串
public subroutine of_loadxml(string as_xml)
ole_xml.async = false
try
ole_xml.loadxml(as_xml)
if ole_xml.parseerror.errorcode <> 0 then
messagebox("解析错误", ole_xml.parseerror.reason)
end if
catch (Exception e)
messagebox("加载失败", e.getmessage())
end try
end subroutine
// 读取节点值
public function of_getelement(string as_path) returns string
OLEObject lnode
try
lnode = ole_xml.selectSingleNode(as_path)
if not isnull(lnode) then
return lnode.text
end if
catch (Exception e)
end try
return ""
end function
// 读取属性
public function of_getattribute(string as_path, string as_attr) returns string
OLEObject lnode
try
lnode = ole_xml.selectSingleNode(as_path)
if not isnull(lnode) then
return lnode.getAttribute(as_attr)
end if
catch (Exception e)
end try
return ""
end function
// 读取节点列表
public function of_getnodes(string as_path, ref OLEObject a_nodes[]) returns int
OLEObject lnodelist, lnode
int li_count = 0, i
try
lnodelist = ole_xml.selectnodes(as_path)
for i = 0 to lnodelist.length - 1
li_count++
a_nodes[li_count] = lnodelist.item(i)
next
catch (Exception e)
end try
return li_count
end function
解析医保接口返回的XML报文:
nv_xml_reader lreader
lreader = create nv_xml_reader
// 接口返回的XML
lreader.of_loadxml(as_response)
// 读公共头
string ls_code = lreader.of_getelement("//output/result/code")
string ls_msg = lreader.of_getelement("//output/result/message")
// 读业务数据——人员列表
OLEObject a_nodes[]
int li_count
li_count = lreader.of_getnodes("//output/personList/personInfo", a_nodes)
int i
for i = 1 to li_count
string ls_name = a_nodes[i].selectSingleNode("aac003").text
string ls_idcard = a_nodes[i].selectSingleNode("aac002").text
// 逐条入库...
next
XPath表达式 //output/personList/personInfo 直接定位到列表节点,selectSingleNode 取子节点值。MSXML的优势是有完整的XPath和DOM支持,比用字符串split硬解析强悍太多。
四、XML生成 vs 解析的策略选择
| 场景 | 方案 | 原因 |
|---|---|---|
| 生成XML发给对方 | 递归类拼字符串 | 性能好,纯PB代码无外部依赖 |
| 解析对方返回的XML | OLE MSXML | XPath查询方便,DOM操作比字符串split稳健 |
| 内部系统间交换 | 递归类双向处理 | 不用OLE,部署简单 |
生成用字符串拼是因为性能——一次请求可能生成几十个节点的XML,纯字符串拼接比OLE快。解析用MSXML是因为稳健——对方返回的XML嵌套深度不确定,XPath+DOM比手写字符串解析靠谱得多。
五、亮点总结
✅ XML生成纯PB代码——字符串递归拼装,零外部依赖
✅ XML解析借力MSXML——XPath+DOM,支持复杂嵌套查询
✅ 五字符转义全处理——& < > " ' 五个XML特殊字符逐个替换
✅ 双模式策略——生成用字符串(快),解析用MSXML(稳)
✅ 完整调用链——从PB DataWindow到XML报文到接口对接,全链路覆盖
六、适用场景
- PB老系统需要对接XML格式的外部接口(医保平台、银行接口、政务平台)
- 字符串拼接生成XML——适合输出报文给对方
- MSXML解析XML——适合处理对方返回的复杂嵌套响应
七、扩展方向
- DTD/Schema校验——加载XML前先用MSXML验证结构合法性
- CDATA支持——处理包含特殊字符的长文本字段
- 流式解析——超大XML文件用SAX解析替代DOM,避免内存撑爆
- 命名空间支持——处理带
xmlns:前缀的XML报文
八、结语
在PB生态里造XML工具——这个选择本身就是一个权衡。字符串拼接快但转义容易漏,MSXML功能全但部署有依赖。2014年的选择是两套并存——生成用自制类(快)、解析用MSXML(稳)。放到今天,所有的现代语言都自带了XML序列化,但在那个PB主导的年代,这套工具就是医院HIS和医保平台之间所有数据交换的管道。