「findall」のメソッドを使用して、ElementTreeモジュール内のソースxmlファイルのいくつかの要素を見つけたい。
ただし、ソースxmlファイル(test.xml)には名前空間があります。サンプルとしてxmlファイルの一部を切り捨てます。
<?xml version="1.0" encoding="iso-8859-1"?>
<XML_HEADER xmlns="http://www.test.com">
<TYPE>Updates</TYPE>
<DATE>9/26/2012 10:30:34 AM</DATE>
<COPYRIGHT_NOTICE>All Rights Reserved.</COPYRIGHT_NOTICE>
<LICENSE>newlicense.htm</LICENSE>
<DEAL_LEVEL>
<PAID_OFF>N</PAID_OFF>
</DEAL_LEVEL>
</XML_HEADER>
サンプルpythonコードは次のとおりです。
from xml.etree import ElementTree as ET
tree = ET.parse(r"test.xml")
el1 = tree.findall("DEAL_LEVEL/PAID_OFF") # Return None
el2 = tree.findall("{http://www.test.com}DEAL_LEVEL/{http://www.test.com}PAID_OFF") # Return <Element '{http://www.test.com}DEAL_LEVEL/PAID_OFF' at 0xb78b90>
名前空間「{http://www.test.com}」があるため、機能しますが、各タグの前に名前空間を追加するのは非常に不便です。
「find」、「findall」などのメソッドを使用するときに、名前空間を無視するにはどうすればよいですか?
解析する前にxmlns属性をxmlから削除すると、ツリー内の各タグの前に名前空間が追加されなくなります。
import re
xmlstring = re.sub(' xmlns="[^"]+"', '', xmlstring, count=1)
XMLドキュメント自体を変更する代わりに、それを解析してから結果のタグを変更するのが最善です。この方法で、複数の名前空間と名前空間エイリアスを処理できます。
from StringIO import StringIO
import xml.etree.ElementTree as ET
# instead of ET.fromstring(xml)
it = ET.iterparse(StringIO(xml))
for _, el in it:
if '}' in el.tag:
el.tag = el.tag.split('}', 1)[1] # strip all namespaces
root = it.root
これは、ここでの議論に基づいています: http://bugs.python.org/issue18304
これまでの回答では、名前空間の値を明示的にスクリプトに入れていました。より一般的な解決策として、xmlから名前空間を抽出します。
import re
def get_namespace(element):
m = re.match('\{.*\}', element.tag)
return m.group(0) if m else ''
そしてfindメソッドで使用します:
namespace = get_namespace(tree.getroot())
print tree.find('./{0}parent/{0}version'.format(namespace)).text
これはnonagonの答えの拡張であり、属性から名前空間も取り除きます。
from StringIO import StringIO
import xml.etree.ElementTree as ET
# instead of ET.fromstring(xml)
it = ET.iterparse(StringIO(xml))
for _, el in it:
if '}' in el.tag:
el.tag = el.tag.split('}', 1)[1] # strip all namespaces
for at in el.attrib.keys(): # strip namespaces of attributes too
if '}' in at:
newat = at.split('}', 1)[1]
el.attrib[newat] = el.attrib[at]
del el.attrib[at]
root = it.root
Ericspodによる回答の改善:
解析モードをグローバルに変更する代わりに、withコンストラクトをサポートするオブジェクトでこれをラップできます。
from xml.parsers import expat
class DisableXmlNamespaces:
def __enter__(self):
self.oldcreate = expat.ParserCreate
expat.ParserCreate = lambda encoding, sep: self.oldcreate(encoding, None)
def __exit__(self, type, value, traceback):
expat.ParserCreate = self.oldcreate
これは次のように使用できます
import xml.etree.ElementTree as ET
with DisableXmlNamespaces():
tree = ET.parse("test.xml")
この方法の利点は、withブロック外の無関係なコードの動作を変更しないことです。また、expicsを使用するericspodのバージョンを使用した後、無関係のライブラリでエラーが発生したため、これを作成することになりました。
エレガントな文字列フォーマット構成も使用できます。
ns='http://www.test.com'
el2 = tree.findall("{%s}DEAL_LEVEL/{%s}PAID_OFF" %(ns,ns))
または、PAID_OFFがツリーの1つのレベルにのみ表示されることが確実な場合:
el2 = tree.findall(".//{%s}PAID_OFF" % ns)
ElementTree
ではなくcElementTree
を使用している場合、ParserCreate()
を置き換えることにより、Expatにネームスペース処理を無視させることができます。
from xml.parsers import expat
oldcreate = expat.ParserCreate
expat.ParserCreate = lambda encoding, sep: oldcreate(encoding, None)
ElementTree
はParserCreate()
を呼び出してExpatを使用しようとしますが、名前空間区切り文字列を提供しないオプションを提供しません。上記のコードでは無視されますが、他の問題が発生する可能性がありますので注意してください。
nonagonの答え と関連する質問への mzjn'aの答えを組み合わせてみましょう :
def parse_xml(xml_path: Path) -> Tuple[ET.Element, Dict[str, str]]:
xml_iter = ET.iterparse(xml_path, events=["start-ns"])
xml_namespaces = dict(prefix_namespace_pair for _, prefix_namespace_pair in xml_iter)
return xml_iter.root, xml_namespaces
この関数を使用して:
名前空間と解析されたツリーオブジェクトの両方を取得するイテレータを作成します。
作成されたイテレータを反復処理して、名前空間ディクショナリを取得します。このディクショナリは、iMom0で推測されたように、各find()
またはfindall()
呼び出し で後で渡すことができます 。
ソースXMLの操作も結果の解析xml.etree.ElementTree
出力の関与も一切ないため、これはあらゆる点で最良のアプローチだと思います。
また、 barnyの答え このパズルの重要な部分(イテレータから解析されたルートを取得できる)を提供したことも評価したいと思います。それまでは、アプリケーションでXMLツリーを実際に2回走査しました(名前空間を取得するために1回、ルートに対して2回目)。
私はこれに遅れるかもしれませんが、re.sub
は良い解決策ではないと思います。
ただし、Python 3.xバージョンでは書き換えxml.parsers.expat
は機能しません。
主な原因は、xml/etree/ElementTree.py
ソースコードの下部を参照してください
# Import the C accelerators
try:
# Element is going to be shadowed by the C implementation. We need to keep
# the Python version of it accessible for some "creative" by external code
# (see tests)
_Element_Py = Element
# Element, SubElement, ParseError, TreeBuilder, XMLParser
from _elementtree import *
except ImportError:
pass
それはちょっと悲しいです。
解決策は、最初にそれを取り除くことです。
import _elementtree
try:
del _elementtree.XMLParser
except AttributeError:
# in case deleted twice
pass
else:
from xml.parsers import expat # NOQA: F811
oldcreate = expat.ParserCreate
expat.ParserCreate = lambda encoding, sep: oldcreate(encoding, None)
Python 3.6でテスト済み。
Try try
ステートメントは、コードのどこかでモジュールを2回リロードまたはインポートすると、次のような奇妙なエラーが発生する場合に便利です。
ちなみに、etreeのソースコードは本当に面倒です。