📹 第六章 SOAP及gSOAP实战
📄 一句话概括

XML 的标签身份是「命名空间 + 本地名」,前缀只是别名,随时能换。 但换前缀不能用字符串替换——s: 这两个字符在 xmlns:s 和 tds: 里都有。

📝 本节和前面几节的关系

上一节把 SOAP 报文当成「HTTP 的体」,这一节把体拆开看。 ONVIF 报文里最容易被忽略的是 xmlns,读不懂它就读不懂为什么同一个 Body 有不同的含义。

一、元素、属性、文本,三样东西

成分 样子 注意
元素 <Model>IPC-2000</Model> 必须有闭合标签,或写成自闭合 <X/>
属性 <X name="Model"> 值必须带引号,单双都行
文本 标签之间的内容 空白也算文本,会被解析器保留

第三行是拼报文时的常见坑:缩进和换行都会变成文本节点, 如果代码里靠「子节点下标」取值,缩进会把它数错。 对策是按标签名找,不按下标找——这也是 find("{ns}Body") 比 list(root)[1] 可靠的原因。

二、命名空间:前缀只是别名

先记住一句话:s: 里的 s 什么都不是。 它只是一张「别名表」里的键,值才是真正的身份。

部分 例子 说明
声明 xmlns:s="http://www.w3.org/2003/05/soap-envelope" 把 s 绑到某个 URI
使用 <s:Body> 按 s 查表,得到完整 URI
真实身份 {http://…}Body 解析器给出的「命名空间 + 本地名」

那个花括号写法叫 Clark 记法, Python 的 ElementTree、Java 的 DOM、C# 的 XmlReader 都用它表示「带命名空间的标签」。 所以代码里写 find("Body") 永远找不到, 必须写 find("{http://…}Body")——这是 SOAP 解析第一大坑(06.01.05 还会遇到)。

三、换前缀实验:三行结果一模一样

Demo 把同一个信封用 s:、soap:、默认命名空间三种写法各写一遍,分别解析:

写法 解析出的根 一样吗
<s:Envelope xmlns:s="…"> {…}Envelope 一样
<soap:Envelope xmlns:soap="…"> {…}Envelope 一样
<Envelope xmlns="…"> {…}Envelope 一样

三行输出完全相同,这就是「前缀只是别名」的直接证据。 ONVIF 报文里 s:/soap:/env: 混着出现,就是这个原因——不同厂家的代码生成器习惯不同, 但指向的 URI 是同一个。

四、反例:字符串替换会把文档改坏

「那我把 s: 全部换成 soap: 不就行了?」——不行,我实测过,直接坏。

原文 替换 s: → soap: 之后 后果
s:Envelope soap:Envelope 正确
xmlns:s= xmlnsoap:s= 声明废了
tds:GetDeviceInformation tdsoap:GetDeviceInformation 标签名改了

报错是 ParseError: unbound prefix—— 因为 soap 这个前缀从来没有被声明过(声明被改成了 xmlnsoap:s)。 根因:s: 这两个字符在 xmlns:s 和 tds: 里都出现过, 字符串替换分不清「这是前缀」还是「这是名字的一部分」。 正确做法有两条:整段重写(Demo 就是这么干的),或者用解析器把树读出来、改完再写回去。 绝不手工 replace。

五、XML 比 JSON 大多少

同一份设备信息(5 个字段),两种格式实测:

格式 字节 相对
XML 223 1.00 倍
JSON 114 0.51 倍

XML 比 JSON 大了将近一倍,因为每个字段名要写两遍(开标签 + 闭标签), 而 JSON 的键只写一遍。 那 SOAP 为什么还选 XML?因为它有 schema(能声明类型)、有 命名空间(能合并多份规范)、 有 成熟的校验工具。这三条在 2000 年是硬需求,现在依然有用——只是不再是非它不可。

六、动手 Demo:XML 的命名空间,SOAP 全靠它

Demo 用 xml.etree.ElementTree 解析一个最小的 SOAP 信封,然后做两件事: 换前缀看结果变不变、用字符串替换把文档改坏看报什么错。 第二个实验是故意做错的,为了让报错亲眼见一次。

xml = ('<?xml version="1.0" encoding="UTF-8"?>'
       '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"'
       ' xmlns:tds="http://www.onvif.org/ver10/device/wsdl">'
       '<s:Header/><s:Body><tds:GetDeviceInformation/></s:Body>'
       '</s:Envelope>')
root = ET.fromstring(xml)
print(root.tag)        # {http://www.w3.org/2003/05/soap-envelope}Envelope

# 反面教材:字符串替换会把 xmlns:s 和 tds: 一起改坏
try:
    ET.fromstring(xml.replace("s:", "soap:"))
except Exception as e:
    print(type(e).__name__, e)   # ParseError: unbound prefix

实跑输出:

--------------------------------------------------------------------
XML 的命名空间,SOAP 全靠它
--------------------------------------------------------------------
**SOAP 信封里到处是 `xmlns`,不搞清命名空间就读不懂 ONVIF 的报文。**

**先看一个最小的 SOAP 信封,三个部分:**

  部分        标签                作用
  ------------------------------------------------------------
  信封        `Envelope`          最外层,声明命名空间
  头          `Header`            可选,放认证、超时这类信息
  体          `Body`              真正的方法调用或返回

  **实测解析结果**:根标签是 `{http://www.w3.org/2003/05/soap-envelope}Envelope`,
  **带花括号的那个前缀就是命名空间。**
  **XML 里的 `s:` 只是「前缀」,不是标签的一部分**。
  **真正的身份是「命名空间 + 本地名」,前缀可以随便换。**

**换三个前缀,整段重写 XML,看解析结果变不变:**

  写法                      解析出的根                                          一样吗
  ----------------------------------------------------------------------------------------
  前缀 `s`                  {http://www.w3.org/200…                             一样
  前缀 `soap`               {http://www.w3.org/200…                             一样
  前缀 `(默认)`           {http://www.w3.org/200…                             一样

  **三行的标签完全一样**(都是 `{…}Envelope`),**证明前缀只是别名。**
  **这就是为什么 ONVIF 报文里 `s:`/`soap:`/`env:` 混着出现也不影响解析。**

**但换前缀有个坑:不能拿字符串替换 `s:` 去改。**
  **实测**:把 `s:` 全局换成 `soap:`,报的是 `ParseError:unbound prefix: line 1, column 38`
  **看一眼被替换坏的属性就明白了**:`xmlns:s=` 变成了 `xmlnsoap:s=`,**`tds:` 变成了 `tdsoap:`。**
  **原因**:`s:` 这两个字符在 `xmlns:s` 和 `tds:` 里都出现过,
  **字符串替换分不清「前缀」和「名字的一部分」。**
  **所以改前缀要整段重写、或用解析器重建树**,绝不手工 replace。

**XML 和 JSON 的体积差多少**(同一份设备信息):

  格式            字节        相对
  ----------------------------------------
  XML             223         1.00 倍
  JSON            114         0.51 倍

  **XML 比 JSON 大了将近一倍**,因为每个字段名要写两遍(开标签 + 闭标签)。
  **但 SOAP 还是选 XML**,因为它有 schema、有命名空间、
  **能表达「这个字段是什么类型」,这是 2000 年那个年代最看重的。**

输出里那个 {http://www.w3.org/2003/05/soap-envelope}Envelope 就是 Clark 记法, 代码里取节点时要一模一样地写,差一个字符都取不到。 「解析出的根」那一列被截到 22 个字符(后面加了省略号),否则 57 个字符会把表格撑破。 三行截断后长得一样,正好说明结论。 反面教材那一段的报错带了行号和列号, column 38 正好落在 xmlnsoap:s= 上——报错位置就是证据位置。

30 秒实验

  1. 把 tds: 换成 t:,同时把 xmlns:tds 改成 xmlns:t,看还能不能解析。
  2. 在信封里再加一层 xmlns,让同一个前缀在内层指向另一个 URI,看内层标签变成了什么。
  3. 把 Header 删掉,看根的子节点数从 2 变成 1,但 Body 的 Clark 记法不变。

常见误区

  1. 以为 s: 里的 s 是标签名的一部分。 它只是别名, 真正的身份是「命名空间 URI + 本地名」。
  2. 以为可以字符串替换前缀。 实测直接把 xmlns:s 和 tds: 改坏,报 unbound prefix。
  3. 以为 XML 一定比 JSON 好。 同样的字段 XML 大了将近一倍, 它换来的是 schema 和命名空间,不是体积优势。
自测三问(点开对答案) 1. `{http://…}Body` 这个写法叫什么?为什么代码里必须这么写? —— **Clark 记法**。 解析器把「命名空间 + 本地名」连成一个字符串当键,**只写 `Body` 匹配不上任何节点**。 2. 为什么用字符串替换 `s:` 会失败? —— 因为 `s:` 也出现在 `xmlns:s=` 和 `tds:` 里,**替换会把声明和别的标签名一起改坏**, 结果前缀没被声明,报 `unbound prefix`。 3. ONVIF 报文里 `s:` 和 `soap:` 混用会不会出问题? —— 不会。**只要它们声明的 URI 一样,解析结果完全相同。**混用只影响可读性,不影响语义。

下一节看 WSDL:把「有哪些方法、参数什么类型」写成一份机器能读的说明书。

相关笔记