使用 ElementTree 和 Python 覆盖 XML 文件时保留现有命名空间

Keep Existing Namespaces when overwriting XML file with ElementTree and Python

我有一个 XML 格式如下的文件

<?xml version="1.0" encoding="utf-8"?>
<foo>
   <bar>
      <bat>1</bat>
   </bar>
   <a>
      <b xmlns="urn:schemas-microsoft-com:asm.v1">
         <c>1</c>
      </b>
   </a>
</foo>

我想将 bat 的值更改为“2”并将文件更改为:

<?xml version="1.0" encoding="utf-8"?>
<foo>
   <bar>
      <bat>2</bat>
   </bar>
   <a>
      <b xmlns="urn:schemas-microsoft-com:asm.v1">
         <c>1</c>
      </b>
   </a>
</foo>

我通过这样做打开这个文件

tree = ET.parse(filePath)
root = tree.getroot()

然后我将 bat 的值更改为“2”并像这样保存文件:

tree.write(filePath, "utf-8", True, None, "xml")

bat 的值成功更改为 2,但是 XML 文件现在看起来像这样。

<?xml version="1.0" encoding="utf-8"?>
<foo xmlns:ns0="urn:schemas-microsoft-com:asm.v1">
   <bar>
      <bat>2</bat>
   </bar>
   <a>
      <ns0:b>
         <ns0:c>1</ns0:c>
      </ns0:b>
   </a>
</foo>

为了解决名为 ns0 的命名空间的问题,我在解析文档之前执行了以下操作

ET.register_namespace('', "urn:schemas-microsoft-com:asm.v1")

这摆脱了 ns0 命名空间,但 xml 文件现在看起来像这样

<?xml version="1.0" encoding="utf-8"?>
<foo xmlns="urn:schemas-microsoft-com:asm.v1">
   <bar>
      <bat>2</bat>
   </bar>
   <a>
      <b>
         <c>1</c>
      </b>
   </a>
</foo>

我该怎么做才能得到我需要的输出?

据我所知,没有办法通过 xml.etree.ElementTree 方法来实现您的目标。通过深入研究 xml.etree 源代码和 xml 规范,我发现库的行为并没有错,也没有不合理。无论如何它不允许你正在寻找的输出。

要使用该库实现您的目标,您必须自定义渲染行为。为了最好地满足您的需求,我编写了以下 render 函数。

from xml.etree import ElementTree as ET
from re import findall, sub

def render(root, buffer='', namespaces=None, level=0, indent_size=2, encoding='utf-8'):
    buffer += f'<?xml version="1.0" encoding="{encoding}" ?>\n' if not level else ''
    root = root.getroot() if isinstance(root, ET.ElementTree) else root
    _, namespaces = ET._namespaces(root) if not level else (None, namespaces)
    for element in root.iter():
        indent = ' ' * indent_size * level
        tag = sub(r'({[^}]+}\s*)*', '', element.tag)
        buffer += f'{indent}<{tag}'
        for ns in findall(r'{[^}]+}', element.tag):
            ns_key = ns[1:-1]
            if ns_key not in namespaces: continue
            buffer += ' xmlns' + (f':{namespaces[ns_key]}' if namespaces[ns_key] != '' else '') + f'="{ns_key}"'
            del namespaces[ns_key]
        for k, v in element.attrib.items():
            buffer += f' {k}="{v}"'
        buffer += '>' + element.text.strip() if element.text else '>'
        children = list(element)
        for child in children:
            sep = '\n' if buffer[-1] != '\n' else ''
            buffer += sep + render(child, level=level+1, indent_size=indent_size, namespaces=namespaces)
        buffer += f'{indent}</{tag}>\n' if 0 != len(children) else f'</{tag}>\n'
    return buffer

通过向上述 render() 函数提供您的 xml 输入数据如下:

data =\ 
'''<?xml version="1.0" encoding="utf-8"?>
<foo>
   <bar>
      <bat>1</bat>
   </bar>
   <a>
      <b xmlns="urn:schemas-microsoft-com:asm.v1">
         <c>1</c>
      </b>
   </a>
</foo>'''

root = ET.ElementTree(ET.fromstring(data))
ET.register_namespace('', "urn:schemas-microsoft-com:asm.v1")
print(render(root))

它打印出您正在寻找的输出:

<?xml version="1.0" encoding="utf-8" ?>
<foo>
  <bar>
    <bat>1</bat>
  </bar>
  <a>
    <b xmlns="urn:schemas-microsoft-com:asm.v1">
      <c>1</c>
    </b>
  </a>
</foo>

使用包 lxml 可以帮助解决您的问题。这里提供了 original/modified xml 文件和 python 代码(使用 lxml)包的示例,namespace/xml 结构不变: