web-dev-qa-db-ja.com

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

「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」などのメソッドを使用するときに、名前空間を無視するにはどうすればよいですか?

114
KevinLeng

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

import re

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

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

48
nonagon

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

これは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
14
barny

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のバージョンを使用した後、無関係のライブラリでエラーが発生したため、これを作成することになりました。

7
lijat

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

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)
4
tzp

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

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

ElementTreeParserCreate()を呼び出してExpatを使用しようとしますが、名前空間区切り文字列を提供しないオプションを提供しません。上記のコードでは無視されますが、他の問題が発生する可能性がありますので注意してください。

2
ericspod

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

この関数を使用して:

  1. 名前空間と解析されたツリーオブジェクトの両方を取得するイテレータを作成します。

  2. 作成されたイテレータを反復処理して、名前空間ディクショナリを取得します。このディクショナリは、iMom0で推測されたように、各find()またはfindall()呼び出し で後で渡すことができます

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

ソースXMLの操作も結果の解析xml.etree.ElementTree出力の関与も一切ないため、これはあらゆる点で最良のアプローチだと思います。

また、 barnyの答え このパズルの重要な部分(イテレータから解析されたルートを取得できる)を提供したことも評価したいと思います。それまでは、アプリケーションでXMLツリーを実際に2回走査しました(名前空間を取得するために1回、ルートに対して2回目)。

0
z33k

私はこれに遅れるかもしれませんが、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回リロードまたはインポートすると、次のような奇妙なエラーが発生する場合に便利です。

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

ちなみに、etreeのソースコードは本当に面倒です。

0
est