我自制的解决方案可能是:
import lxml.etree as ET
def tag2text(node, sar):
"""Replace element in `sar.keys()` to text in `sar.values()`."""
for elem, text in sar.items():
for ph in node.xpath(f'.//{elem}'):
ph.tail = text + ph.tail if ph.tail is not None else text
ET.strip_elements(node, elem, with_tail=False)
上面的解决方案在工作:
xml = ET.fromstring("""<root><a><c>111</c>
<b>sdfsf<c>111</c>ddd</b>fff</a>
<c>111</c><c/><emb><c><c>EMB</c></c></emb></root>""")
tag2text(xml, {'c': '[C]'})
转换此输入:
<?xml version="1.0" encoding="utf-8"?>
<root>
<a><c>111</c><b>sdfsf<c>111</c>ddd</b>fff</a>
<c>111</c>
<c/>
<emb>
<c>
<c>EMB</c>
</c>
</emb>
</root>
进入这个输出:
<?xml version="1.0" encoding="utf-8"?>
<root>
<a>[C]<b>sdfsf[C]ddd</b>fff</a>
[C]
[C]
<emb>[C]</emb>
</root>
看起来不错。
任何更好、更简单、更高效、更 lxml-ic 或更 pythonic 的解决方案?
使用
iter
和单遍替换:
for node in xml.iter(sar):
r = sar[node.tag] + (node.tail or '')
if (p := node.getprevious()) is not None:
p.tail = (p.tail or '') + r
else:
p = node.getparent()
p.text = (p.text or '') + r
node.getparent().remove(node)
使用
iterparse
并在构建时更新树:
ctx = iterparse(BytesIO(doc), tag= sar)
for _, node in ctx:
r = sar[node.tag] + (node.tail or '')
if (p := node.getprevious()) is not None:
p.tail = (p.tail or '') + r
else:
p = node.getparent()
p.text = (p.text or '') + r
node.getparent().remove(node)
几乎唯一的区别是迭代部分。两者都比原来的更复杂,因为它们基本上必须手动完成
strip_elements
的工作。
两者产生相同的结果:
<root>
<a>[C]<b>sdfsf[C]ddd</b>fff</a>
[C]
[C]
<emb>
[C]
</emb>
</root>
XML 转换可以由 XSLT 处理,saxonche 有一个用于最新版本 XSLT 3 的 Python 包,它允许基于 XPath 的评估,因此 Python/XSLT 3 版本将是:
from saxonche import *
xslt = '''
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
exclude-result-prefixes="#all"
expand-text="yes">
<xsl:param name="replacement-map" static="yes" as="map(xs:string, xs:string)" select="map { 'c' : '[C]' }"/>
<xsl:param name="elements-to-replace" as="element()*">
<xsl:evaluate context-item="." xpath="(($replacement-map => map:keys()) ! ('//' || .)) => string-join(' | ')"/>
</xsl:param>
<xsl:template match="$elements-to-replace">
<xsl:sequence select="$replacement-map(local-name())"/>
</xsl:template>
<xsl:mode on-no-match="shallow-copy"/>
</xsl:stylesheet>
'''
with PySaxonProcessor(license=False) as saxon:
xslt30_processor = saxon.new_xslt30_processor()
xslt30_executable = xslt30_processor.compile_stylesheet(stylesheet_text=xslt)
xslt30_executable.set_cwd('.')
xslt30_executable.transform_to_file(source_file='sample1.xml', output_file='result1.xml')
我意识到除了 lxml 之外的另一个包不符合主题使用 lxml 的要求,但我认为对于大多数 XML 转换来说值得考虑 XSLT。