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 秒实验
- 把
tds:换成t:,同时把xmlns:tds改成xmlns:t,看还能不能解析。 - 在信封里再加一层
xmlns,让同一个前缀在内层指向另一个 URI,看内层标签变成了什么。 - 把
Header删掉,看根的子节点数从 2 变成 1,但Body的 Clark 记法不变。
常见误区
- 以为
s:里的s是标签名的一部分。 它只是别名, 真正的身份是「命名空间 URI + 本地名」。 - 以为可以字符串替换前缀。 实测直接把
xmlns:s和tds:改坏,报unbound prefix。 - 以为 XML 一定比 JSON 好。 同样的字段 XML 大了将近一倍, 它换来的是 schema 和命名空间,不是体积优势。
自测三问(点开对答案)
1. `{http://…}Body` 这个写法叫什么?为什么代码里必须这么写? —— **Clark 记法**。 解析器把「命名空间 + 本地名」连成一个字符串当键,**只写 `Body` 匹配不上任何节点**。 2. 为什么用字符串替换 `s:` 会失败? —— 因为 `s:` 也出现在 `xmlns:s=` 和 `tds:` 里,**替换会把声明和别的标签名一起改坏**, 结果前缀没被声明,报 `unbound prefix`。 3. ONVIF 报文里 `s:` 和 `soap:` 混用会不会出问题? —— 不会。**只要它们声明的 URI 一样,解析结果完全相同。**混用只影响可读性,不影响语义。下一节看 WSDL:把「有哪些方法、参数什么类型」写成一份机器能读的说明书。
- ☐ (本节完,下一节 06.01.04 WSDL简介)