Python ElementTreeモジュール:メソッド「find」、「findall」を使用するときに、XMLファイルの名前空間を無視して一致する要素を見つける方法


136

「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」などのメソッドを使用する場合、ネームスペースをどのように無視できますか?


18
あるtree.findall("xmlns:DEAL_LEVEL/xmlns:PAID_OFF", namespaces={'xmlns': 'http://www.test.com'})便利な十分な?
iMom0

どうもありがとう。私はあなたの方法を試し、それはうまくいくことができます。それは私のものよりも便利ですが、それでも少し厄介です。ElementTreeモジュールにこの問題を解決する適切なメソッドが他にないか、そのようなメソッドがまったくないか知っていますか?
KevinLeng

または試してくださいtree.findall("{0}DEAL_LEVEL/{0}PAID_OFF".format('{http://www.test.com}'))
Warf

Python 3.8では、名前空間にワイルドカードを使用できます。stackoverflow.com/a/62117710/407651
mzjn

回答:


62

XMLドキュメント自体を変更する代わりに、XMLドキュメントを解析して、結果のタグを変更するのが最善です。このようにして、複数の名前空間と名前空間エイリアスを処理できます。

from io import StringIO  # for Python 2 import from StringIO instead
import xml.etree.ElementTree as ET

# instead of ET.fromstring(xml)
it = ET.iterparse(StringIO(xml))
for _, el in it:
    prefix, has_namespace, postfix = el.tag.partition('}')
    if has_namespace:
        el.tag = postfix  # strip all namespaces
root = it.root

これは、ここでの議論に基づいています:http : //bugs.python.org/issue18304

更新: 名前空間がない場合でも、rpartition代わりにpartitionタグ名を取得するようにしますpostfix。したがって、あなたはそれを凝縮することができます:

for _, el in it:
    _, _, el.tag = el.tag.rpartition('}') # strip ns

2
この。これこれこれ。複数の名前空間が私の死をもたらすことになっていました。
Jess

8
わかりました。これはすばらしい方法であり、より高度ですが、それでもそうではありませんet.findall('{*}sometag')。また、「今回は名前空間を無視して検索を実行し、ドキュメントなどを再解析せずに名前空間情報を保持する」だけでなく、要素ツリー自体も変更します。まあ、その場合は、名前空間を削除した後、ノードが希望どおりであるかどうかを確認しながら、ツリーを反復処理して自分で確認する必要があります。
Tomasz Gandor 2014年

1
これは文字列を削除することで機能しますが、write(...)を使用してXMLファイルを保存すると、XML xmlns = " bla " の懇願から名前空間が消えます。アドバイスをお願いします
TraceKira

@TomaszGandor:名前空間を別の属性に追加できます。単純なタグ封じ込めテスト(このドキュメントにはこのタグ名が含まれていますか?)の場合、このソリューションは優れており、短絡する可能性があります。
Martijn Pieters

@TraceKira:この手法は、解析されたドキュメントから名前空間を削除します。これを使用して、名前空間を持つ新しいXML文字列を作成することはできません。名前空間の値を追加の属性に格納して(XMLツリーを文字列に戻す前に名前空間を元に戻す)、または元のソースから再解析して、削除されたツリーに基づいて変更を適用します。
Martijn Pieters

48

解析する前にxmlns属性をxmlから削除すると、ツリー内の各タグの前に名前空間が追加されなくなります。

import re

xmlstring = re.sub(' xmlns="[^"]+"', '', xmlstring, count=1)

5
これは多くの場合うまくいきましたが、それから複数の名前空間と名前空間エイリアスに出くわしました。これらのケースを処理する別のアプローチについては、私の回答を参照してください。
nonagon 2014

47
-1解析の前に正規表現を介してxmlを操作するのは間違っています。場合によっては機能することもありますが、これはトップ投票の回答にはならず、専門的なアプリケーションでは使用しないでください。
マイク

1
XML構文解析ジョブに正規表現を使用することは本質的に不健全であるという事実は別として、名前空間プレフィックスを無視するため、これは多くのXMLドキュメントでは機能しません。スペース)と=等号の周り。
Martijn Pieters

はい、それは速くて汚いです、しかし、それは間違いなく単純なユースケースのための最もエレガントなソリューションです、ありがとう!
rimkashox

18

これまでの回答では、明示的に名前空間の値をスクリプトに入れています。より一般的な解決策として、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

15
1つしかないと仮定するには多すぎるnamespace
Kashyap

これは、ネストされたタグが異なる名前空間を使用できることを考慮していません。
Martijn Pieters

15

これは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 list(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

更新:list()イテレーターが機能するように追加(Python 3に必要)


14

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ブロックの外の無関係なコードの動作を変更しないことです。ericspodのバージョンを使用した後、無関係なライブラリでエラーが発生し、expatを使用していたため、これを作成しました。


これは甘くて健康的です!私の日を救った!+1
AndreasT 2018

Python 3.8(他のバージョンでテストされていない)では、これは私には機能しないようです。ソースを見ると動作するはずですが、ソースコードxml.etree.ElementTree.XMLParserはどういうわけか最適化されており、モンキーパッチexpatはまったく効果がありません。
Reinderien

ああ、そう。:barnyさんのコメント@を参照してくださいstackoverflow.com/questions/13412496/...
Reinderien

5

エレガントな文字列フォーマット構文も使用できます。

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)

2

使用ElementTreeしていていないcElementTree場合は、次のように置き換えることで、Expatにネームスペース処理を無視させることができますParserCreate()

from xml.parsers import expat
oldcreate = expat.ParserCreate
expat.ParserCreate = lambda encoding, sep: oldcreate(encoding, None)

ElementTree呼び出しによってExpatを使おうとしますParserCreate()が、名前空間セパレーター文字列を提供しないオプションを提供しません。上記のコードはそれを無視させますが、これが他のものを壊す可能性があるという警告が出されます。


文字列処理に依存しないため、これは他の現在の回答よりも優れた方法です
lijat

3
python 3.7.2(およびおそらく以前の)AFAICTでは、cElementTreeの使用を回避することはもはや不可能であるため、この回避策は不可能である可能性があります:-(
barny

1
cElemTreeは非推奨ですが、Cアクセラレータで行われている型のシャドウがあります。Cコードはexpatを呼び出していないので、このソリューションは壊れています。
ericspod

@barnyそれはまだ可能です、ElementTree.fromstring(s, parser=None)私はそれにパーサーを渡そうとしています。
EST

2

これには遅れるかもしれませんre.subが、良い解決策だとは思いません。

ただし、書き換えxml.parsers.expatはPython 3.xバージョンでは機能しません。

主な犯人は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コードのどこかでモジュールを2回リロードまたはインポートすると、次のような奇妙なエラーが発生する場合に、try ステートメントが役立ちます。

  • 最大再帰深度を超えました
  • AttributeError:XMLParser

ところで、etreeソースコードは本当に厄介に見えます。


1

nonagonの回答関連する質問に対するmzjnの回答を組み合わせてみましょう。

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

この関数を使用して:

  1. イテレータを作成して、名前空間と解析済みツリーオブジェクトの両方を取得します

  2. 作成したイテレーターを反復処理して、後でそれぞれに渡すfind()、iMom0によってサジェストされたとおりにfindall()呼び出すことができる名前空間ディクテーションを取得します。

  3. 解析されたツリーのルート要素オブジェクトと名前空間を返します。

ソースXMLや結果として得られる解析されたxml.etree.ElementTree出力のいずれの操作も含まれていないため、これはすべての面で最良のアプローチだと思います。

私はまた、このパズルの本質的な部分を提供することでbarnyの答えを信用したいと思います(これにより、反復子から解析済みルートを取得できます)。それまで、実際にアプリケーションでXMLツリーを2回トラバースしました(名前空間を取得するために、ルートの2番目)。


それを使用する方法を見つけたが、それは私のために動作しません、私はまだ出力に名前空間を見ます
taiko

1
OPの質問に対するiMom0のコメントをご覧ください。この関数を使用すると、解析されたオブジェクトと、find()およびでそれをクエリする手段の両方を取得できますfindall()。それらのメソッドに名前空間のdict from parse_xml()をフィードし、クエリで名前空間のプレフィックスを使用するだけです。例:et_element.findall(".//some_ns_prefix:some_xml_tag", namespaces=xml_namespaces)
z33k
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.