使用 Python 中的 'ElementTree' 解析带有命名空间的 XML

199

我有以下XML,我想使用Python的 ElementTree 进行解析:

<rdf:RDF xml:base="http://dbpedia.org/ontology/"
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:owl="http://www.w3.org/2002/07/owl#"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
    xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
    xmlns="http://dbpedia.org/ontology/">

    <owl:Class rdf:about="http://dbpedia.org/ontology/BasketballLeague">
        <rdfs:label xml:lang="en">basketball league</rdfs:label>
        <rdfs:comment xml:lang="en">
          a group of sports teams that compete against each other
          in Basketball
        </rdfs:comment>
    </owl:Class>

</rdf:RDF>

我想找到所有的owl:Class标签,然后提取其中所有rdfs:label实例的值。我正在使用以下代码:

tree = ET.parse("filename")
root = tree.getroot()
root.findall('owl:Class')

由于命名空间,我得到了以下错误。

SyntaxError: prefix 'owl' not found in prefix map

我试着阅读了http://effbot.org/zone/element-namespaces.htm上的文档,但由于上述XML中有多个嵌套命名空间,所以我仍然无法让它正常工作。

请告诉我如何更改代码以查找所有owl:Class标签。


1
自Python 3.8起,可以在find()findall()findtext()中使用命名空间通配符。请参见https://dev59.com/I7voa4cB1Zd3GeqP1VK-#62117710。 - mzjn
7个回答

264

您需要为 .find(), findall()iterfind() 方法提供一个明确的命名空间字典:

namespaces = {'owl': 'http://www.w3.org/2002/07/owl#'} # add more as needed

root.findall('owl:Class', namespaces)

前缀仅在您传递的namespaces参数中查找。这意味着您可以使用任何命名空间前缀;API会分离出owl:部分,在namespaces字典中查找相应的命名空间URL,然后更改搜索以查找XPath表达式{http://www.w3.org/2002/07/owl}Class。当然,您也可以自己使用相同的语法:

root.findall('{http://www.w3.org/2002/07/owl#}Class')

另请参阅ElementTree文档中的使用命名空间解析XML部分

如果您可以切换到lxml,事情会变得更好;该库支持相同的ElementTree API,但在元素上通过.nsmap属性收集命名空间,并且通常具有更好的命名空间支持。


10
谢谢。你知道如何直接从XML中获取命名空间,而不是硬编码吗?或者如何忽略它?我尝试使用findall('{*}Class'),但在我的情况下不起作用。 - Kostanos
7
您需要自己扫描树以查找xmlns属性;正如答案中所述,lxml会为您完成此操作,而xml.etree.ElementTree模块则不会。但是如果您正在尝试匹配特定(已硬编码)的元素,则还要尝试在特定命名空间中匹配特定元素。该命名空间在文档之间不会像元素名称一样更改。因此,您可以将其与元素名称一起硬编码。 - Martijn Pieters
15
@Jon:register_namespace 仅影响序列化,而不影响搜索。 - Martijn Pieters
5
有用的小提示:如果使用 cElementTree 而非 ElementTree,则无法将命名空间作为关键字参数传递给 findall 方法,而是需要将其作为普通参数传递,例如使用 ctree.findall('owl:Class', namespaces) - egpbos
2
@Bludwarf:文档确实提到了它(现在,如果您写下时没有),但您必须非常仔细地阅读它们。请参见使用命名空间解析XML部分:其中有一个比较例子,展示了在未使用和使用namespace参数的findall的区别,但是该参数在元素对象部分中并未被提及作为方法参数之一。 - Wilson F
显示剩余6条评论

69
以下是使用lxml进行操作,而无需硬编码命名空间或扫描文本以获取命名空间的方法(正如Martijn Pieters所提到的):
from lxml import etree
tree = etree.parse("filename")
root = tree.getroot()
root.findall('owl:Class', root.nsmap)

更新:

5年后我仍然遇到了这个问题的不同变体。像我上面展示的那样,lxml对此有所帮助,但并非在所有情况下都适用。评论者可能在合并文档方面使用这种技术时有一定的观点,但我认为大多数人只是在搜索文档时遇到了困难。

以下是另一个案例以及我如何处理它:

<?xml version="1.0" ?><Tag1 xmlns="http://www.mynamespace.com/prefix">
<Tag2>content</Tag2></Tag1>

没有前缀的xmlns表示无前缀标签使用此默认命名空间。这意味着在搜索Tag2时,需要包含命名空间才能找到它。但是,lxml创建了一个键为None的nsmap条目,我找不到一种方法来搜索它。因此,我创建了一个新的命名空间字典,就像这样

namespaces = {}
# response uses a default namespace, and tags don't mention it
# create a new ns map using an identifier of our choice
for k,v in root.nsmap.iteritems():
    if not k:
        namespaces['myprefix'] = v
e = root.find('myprefix:Tag2', namespaces)

3
完整的命名空间 URL 就是 你应该硬编码的命名空间标识符。本地前缀 (owl) 可以在文件之间更改。因此,执行这个答案建议的操作是一个非常糟糕的主意。 - Matti Virkkunen
2
@MattiVirkkunen 如果猫头鹰的定义在文件之间可以更改,那么我们应该使用每个文件中定义的定义而不是硬编码吗? - Loïc Faure-Lacroix
1
@LoïcFaure-Lacroix:通常XML库会让你抽象出那部分内容。你甚至不需要知道或关心文件本身使用的前缀,你只需为解析定义自己的前缀或使用完整的命名空间名称即可。 - Matti Virkkunen
这个答案至少让我能够使用find函数了。不需要自己创建前缀。我只需执行key = list(root.nsmap.keys())[0],然后将key添加为前缀即可:root.find(f'{key}:Tag2', root.nsmap)。 - Eelco van Vliet

46

注意:这是一个有用的回答,适用于Python的ElementTree标准库,而不使用硬编码的命名空间。

要从XML数据中提取命名空间的前缀和URI,可以使用ElementTree.iterparse函数,仅解析命名空间开始事件(start-ns):

>>> from io import StringIO
>>> from xml.etree import ElementTree
>>> my_schema = u'''<rdf:RDF xml:base="http://dbpedia.org/ontology/"
...     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
...     xmlns:owl="http://www.w3.org/2002/07/owl#"
...     xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
...     xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
...     xmlns="http://dbpedia.org/ontology/">
... 
...     <owl:Class rdf:about="http://dbpedia.org/ontology/BasketballLeague">
...         <rdfs:label xml:lang="en">basketball league</rdfs:label>
...         <rdfs:comment xml:lang="en">
...           a group of sports teams that compete against each other
...           in Basketball
...         </rdfs:comment>
...     </owl:Class>
... 
... </rdf:RDF>'''
>>> my_namespaces = dict([
...     node for _, node in ElementTree.iterparse(
...         StringIO(my_schema), events=['start-ns']
...     )
... ])
>>> from pprint import pprint
>>> pprint(my_namespaces)
{'': 'http://dbpedia.org/ontology/',
 'owl': 'http://www.w3.org/2002/07/owl#',
 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#',
 'xsd': 'http://www.w3.org/2001/XMLSchema#'}

然后可以将该字典作为参数传递给搜索函数:

root.findall('owl:Class', my_namespaces)

2
这对于我们中没有访问lxml且不想硬编码命名空间的人很有用。 - delrocco
1
我在这行代码 filemy_namespaces = dict([node for _, node in ET.iterparse(StringIO(my_schema), events=['start-ns'])]) 中遇到了错误:ValueError: write to closed。你有什么想法是怎么出错的吗? - Yuli
可能错误与io.StringIO类有关,它拒绝ASCII字符串。我已经使用Python3测试了我的代码。将Unicode字符串前缀'u'添加到示例字符串中,它也可以在Python 2(2.7)中工作。 - Davide Brunato
1
这正是我一直在寻找的!谢谢你! - tjwrona1992
不,iterparse()与find/findall/finditer无关。它使用XML解析器迭代树节点,包括命名空间声明的开始和结束(因此作用域)。 - Davide Brunato
显示剩余3条评论

7

我一直在使用类似的代码,并发现阅读文档总是值得的...像往常一样!

findall() 只会查找当前标签的直接子元素。所以,并不是真正的“所有”。

如果你正在处理大型和复杂的 XML 文件,那么尝试使用以下方法可能会更加值得,这样子子元素等也会被包括进去。 如果你知道 XML 中元素的位置,那么我想这没问题!只是觉得这值得记住。

root.iter()

参考:https://docs.python.org/3/library/xml.etree.elementtree.html#finding-interesting-elements "Element.findall() 仅查找当前元素的直接子元素中具有指定标签的元素。Element.find() 查找第一个具有指定标签的子元素,而 Element.text 可以访问元素的文本内容。Element.get() 可以访问元素的属性:"


3
据我个人观点,ElementTree文档有些不清晰且容易误解。可以使用elem.findall(".//X")来获取所有子孙节点,而不是elem.findall("X")。请注意不要更改原意并使翻译通俗易懂。 - mzjn

7
为了以命名空间格式获取命名空间,例如{myNameSpace},您可以执行以下操作:
root = tree.getroot()
ns = re.match(r'{.*}', root.tag).group(0)

这样,您可以在代码中稍后使用它来查找节点,例如使用字符串插值(Python 3)。
link = root.find(f"{ns}link")

3
这基本上是Davide Brunato的答案,但我发现他的答案存在严重问题,至少在我的Python 3.6安装中,其默认命名空间为空字符串。我从他的代码中提取出一个函数,并对其进行了改进,以下是我的代码:
from io import StringIO
from xml.etree import ElementTree
def get_namespaces(xml_string):
    namespaces = dict([
            node for _, node in ElementTree.iterparse(
                StringIO(xml_string), events=['start-ns']
            )
    ])
    namespaces["ns0"] = namespaces[""]
    return namespaces

其中ns0只是空命名空间的占位符,您可以将其替换为任何随意的字符串。

然后我执行:

my_namespaces = get_namespaces(my_schema)
root.findall('ns0:SomeTagWithDefaultNamespace', my_namespaces)

它也可以正确处理使用默认命名空间的标签。

1
我的解决方案基于@Martijn Pieters的评论:

register_namespace只影响序列化,而不是搜索。

因此,这里的诀窍是使用不同的字典进行序列化和搜索。
namespaces = {
    '': 'http://www.example.com/default-schema',
    'spec': 'http://www.example.com/specialized-schema',
}

现在,注册所有的命名空间以进行解析和编写:
for name, value in namespaces.iteritems():
    ET.register_namespace(name, value)

在搜索(find()findall()iterfind())时,我们需要一个非空的前缀。将这些函数传递给修改后的字典(这里我修改了原始字典,但必须在注册命名空间之后才能进行此操作)。

self.namespaces['default'] = self.namespaces['']

现在,find() 函数系列可以与 default 前缀一起使用:
print root.find('default:myelem', namespaces)

但是
tree.write(destination)

默认命名空间中的元素不使用任何前缀。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接