lxml.objectify

187 阅读24分钟

lxml.objectify

作者

: Stefan Behnel : Holger Joukl

lxml 通过自定义 Element 实现支持类似于 Amara 绑定库或 gnosis.xml.objectify 的替代 API。 主要思想是将 XML 的使用隐藏在普通 Python 对象后面,有时称为数据绑定。 它允许您像处理普通的 Python 对象层次结构一样使用 XML。

访问 XML 元素的子元素会部署对象属性访问。 如果有多个同名的子元素,可以使用切片和索引。 Python 数据类型会自动从 XML 内容中提取,并可供普通 Python 操作员使用。

要设置和使用 objectify,您需要 lxml.etree 模块和 lxml.objectify

>>> from lxml import etree
>>> from lxml import objectify

lxml.objectify API接口

lxml.objectify 中,元素树提供了一个 API,它尽可能地模拟普通 Python 对象树的行为。

通过对象属性访问元素

objectify API 背后的主要思想是将 XML 元素访问隐藏在通常的对象属性访问模式后面。 向元素询问属性将返回具有相应标签名称的子元素序列:

>>> root = objectify.Element("root")
>>> b = objectify.SubElement(root, "b")
>>> print(root.b[0].tag)
b
>>> root.index(root.b[0])
0
>>> b = objectify.SubElement(root, "b")
>>> print(root.b[0].tag)
b
>>> print(root.b[1].tag)
b
>>> root.index(root.b[1])
1

为了方便起见,您可以省略索引0来访问第一个子项:

>>> print(root.b.tag)
b
>>> root.index(root.b)
0
>>> del root.b

迭代和切片也遵循请求的标签:

>>> x1 = objectify.SubElement(root, "x")
>>> x2 = objectify.SubElement(root, "x")
>>> x3 = objectify.SubElement(root, "x")

>>> [ el.tag for el in root.x ]
['x', 'x', 'x']

>>> [ el.tag for el in root.x[1:3] ]
['x', 'x']

>>> [ el.tag for el in root.x[-1:] ]
['x']

>>> del root.x[1:2]
>>> [ el.tag for el in root.x ]
['x', 'x']

如果您想迭代所有子项或需要为标记提供特定的命名空间,请使用 iterchildren() 方法。 与其他迭代方法一样,它支持可选的标签关键字参数:

>>> [ el.tag for el in root.iterchildren() ]
['b', 'x', 'x']

>>> [ el.tag for el in root.iterchildren(tag='b') ]
['b']

>>> [ el.tag for el in root.b ]
['b']

XML 属性的访问方式与普通 ElementTree API 中一样:

>>> c = objectify.SubElement(root, "c", myattr="someval")
>>> print(root.c.get("myattr"))
someval

>>> root.c.set("c", "oh-oh")
>>> print(root.c.get("c"))
oh-oh

除了用于将元素附加到树的普通 ElementTree API 之外,还可以通过将子树分配给对象属性来添加子树。 在这种情况下,子树会自动深度复制,并更新其根的标记名称以匹配属性名称:

>>> el = objectify.Element("yet_another_child")
>>> root.new_child = el
>>> print(root.new_child.tag)
new_child
>>> print(el.tag)
yet_another_child

>>> root.y = [ objectify.Element("y"), objectify.Element("y") ]
>>> [ el.tag for el in root.y ]
['y', 'y']

后者是对整个切片进行操作的缩写形式:

>>> root.y[:] = [ objectify.Element("y") ]
>>> [ el.tag for el in root.y ]
['y']

你也可以用这种方式替代子元素:

>>> child1 = objectify.SubElement(root, "child")
>>> child2 = objectify.SubElement(root, "child")
>>> child3 = objectify.SubElement(root, "child")

>>> el = objectify.Element("new_child")
>>> subel = objectify.SubElement(el, "sub")

>>> root.child = el
>>> print(root.child.sub.tag)
sub

>>> root.child[2] = el
>>> print(root.child[2].sub.tag)
sub

请{++ 注意 ++},更改元素的标签名称时必须特别小心:

>>> print(root.b.tag)
b
>>> root.b.tag = "notB"
>>> root.b
Traceback (most recent call last):
  ...
AttributeError: no such child: b
>>> print(root.notB.tag)
notB

创建对象树

lxml.etree 一样,您可以通过解析 XML 文档或从头开始构建一棵对象化树来创建对象化树。 要解析文档,只需使用模块的 parse()fromstring() 函数:

>>> fileobject = StringIO('<test/>')

>>> tree = objectify.parse(fileobject)
>>> print(isinstance(tree.getroot(), objectify.ObjectifiedElement))
True

>>> root = objectify.fromstring('<test/>')
>>> print(isinstance(root, objectify.ObjectifiedElement))
True

为了在内存中构建新树,objectify 复制了 lxml.etree 中的标准工厂函数 Element()

>>> obj_el = objectify.Element("new")
>>> print(isinstance(obj_el, objectify.ObjectifiedElement))
True

创建这样的元素后,您可以使用 lxml.etree 的常用 API 将子元素添加到树中:

>>> child = objectify.SubElement(obj_el, "newchild", attr="value")

新的子元素将自动从其树继承对象化行为。 但是,您通过 lxml.etreeElement() 工厂(而不是 objectify)创建的所有独立元素本身将不支持 objectify API:

>>> subel = objectify.SubElement(obj_el, "sub")
>>> print(isinstance(subel, objectify.ObjectifiedElement))
True

>>> independent_el = etree.Element("new")
>>> print(isinstance(independent_el, objectify.ObjectifiedElement))
False

使用 E-factory 生成节点树

为了进一步简化树的生成,您可以使用 E-factory:

>>> E = objectify.E
>>> root = E.root(
...   E.a(5),
...   E.b(6.21),
...   E.c(True),
...   E.d("how", tell="me")
... )

>>> print(etree.tostring(root, pretty_print=True))
<root xmlns:py="http://codespeak.net/lxml/objectify/pytype">
  <a py:pytype="int">5</a>
  <b py:pytype="float">6.21</b>
  <c py:pytype="bool">true</c>
  <d py:pytype="str" tell="me">how</d>
</root>

这允许您在标签中编写特定语言:

>>> ROOT = objectify.E.root
>>> TITLE = objectify.E.title
>>> HOWMANY = getattr(objectify.E, "how-many")

>>> root = ROOT(
...   TITLE("The title"),
...   HOWMANY(5)
... )

>>> print(etree.tostring(root, pretty_print=True))
<root xmlns:py="http://codespeak.net/lxml/objectify/pytype">
  <title py:pytype="str">The title</title>
  <how-many py:pytype="int">5</how-many>
</root>

objectify.E 是 objectify.ElementMaker 的实例。 默认情况下,它创建没有命名空间的 pytype 注释元素。 您可以通过将 False 传递给构造函数的 annotate 关键字参数来关闭 pytype 注释。 您还可以传递默认名称空间和 nsmap

>>> myE = objectify.ElementMaker(annotate=False,
...           namespace="http://my/ns", nsmap={None : "http://my/ns"})

>>> root = myE.root( myE.someint(2) )

>>> print(etree.tostring(root, pretty_print=True))
<root xmlns="http://my/ns">
  <someint>2</someint>
</root>

命名空间处理

在标签查找期间,名称空间主要在幕后处理。 如果访问 Element 的子元素而不指定命名空间,则查找将使用父元素的命名空间:

>>> root = objectify.Element("{http://ns/}root")
>>> b = objectify.SubElement(root, "{http://ns/}b")
>>> c = objectify.SubElement(root, "{http://other/}c")

>>> print(root.b.tag)
{http://ns/}b

请注意,lxml.etree 的 SubElement() 工厂在创建新子元素时不会继承任何名称空间。 元素创建必须明确命名空间,并通过如上所述的 E-factory 进行简化。

然而,查找元素时则隐式继承命名空间:

>>> print(root.b.tag)
{http://ns/}b

>>> print(root.c)
Traceback (most recent call last):
    ...
AttributeError: no such child: {http://ns/}c

要访问与其父级不同的命名空间中的元素,可以使用 getattr()

>>> c = getattr(root, "{http://other/}c")
>>> print(c.tag)
{http://other/}c

为了方便起见,还有一种快速访问项目的方法:

>>> c = root["{http://other/}c"]
>>> print(c.tag)
{http://other/}c

必须使用相同的方法来访问标签名称不是有效 Python 标识符的子项:

>>> el = objectify.SubElement(root, "{http://ns/}tag-name")
>>> print(root["tag-name"].tag)
{http://ns/}tag-name

>>> new_el = objectify.Element("{http://ns/}new-element")
>>> el = objectify.SubElement(new_el, "{http://ns/}child")
>>> el = objectify.SubElement(new_el, "{http://ns/}child")
>>> el = objectify.SubElement(new_el, "{http://ns/}child")

>>> root["tag-name"] = [ new_el, new_el ]
>>> print(len(root["tag-name"]))
2
>>> print(root["tag-name"].tag)
{http://ns/}tag-name

>>> print(len(root["tag-name"].child))
3
>>> print(root["tag-name"].child.tag)
{http://ns/}child
>>> print(root["tag-name"][1].child.tag)
{http://ns/}child

或者对于 lxml.objectify 中具有特殊含义的名称:

>>> root = objectify.XML("<root><text>TEXT</text></root>")

>>> print(root.text.text)
Traceback (most recent call last):
  ...
AttributeError: 'NoneType' object has no attribute 'text'

>>> print(root["text"].text)
TEXT

断言Schema

当处理来自不同源的 XML 文档时,您通常会要求它们遵循通用的模式。 在 lxml.objectify 中,这直接转化为强制执行特定的对象树,即确保预期的对象属性存在并具有预期的类型。 这可以通过解析时的 XML 模式验证轻松实现。 另请参阅有关此主题的验证文档{target="_blank"}。

首先,我们需要一个了解我们的schema的解析器,所以假设我们从类似文件的对象(或文件或文件名)解析schema:

>>> f = StringIO('''\
...   <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
...     <xsd:element name="a" type="AType"/>
...     <xsd:complexType name="AType">
...       <xsd:sequence>
...         <xsd:element name="b" type="xsd:string" />
...       </xsd:sequence>
...     </xsd:complexType>
...   </xsd:schema>
... ''')
>>> schema = etree.XMLSchema(file=f)

创建验证解析器时,我们必须确保它返回对象化树。 最好使用 makeparser() 函数来完成:

>>> parser = objectify.makeparser(schema = schema)

现在我们可以用它来解析有效的文档:

>>> xml = "<a><b>test</b></a>"
>>> a = objectify.fromstring(xml, parser)

>>> print(a.b)
test

或者无效的文件:

>>> xml = b"<a><b>test</b><c/></a>"
>>> a = objectify.fromstring(xml, parser)  # doctest: +ELLIPSIS
Traceback (most recent call last):
lxml.etree.XMLSyntaxError: Element 'c': This element is not expected...

请注意,这同样适用于解析时 DTD 验证,只是 DTD 在设计上不支持任何数据类型。

ObjectPath

为了方便和快速,objectify 支持自己的路径语言,由 ObjectPath 类表示:

>>> root = objectify.Element("{http://ns/}root")
>>> b1 = objectify.SubElement(root, "{http://ns/}b")
>>> c  = objectify.SubElement(b1,   "{http://ns/}c")
>>> b2 = objectify.SubElement(root, "{http://ns/}b")
>>> d  = objectify.SubElement(root, "{http://other/}d")

>>> path = objectify.ObjectPath("root.b.c")
>>> print(path)
root.b.c
>>> path.hasattr(root)
True
>>> print(path.find(root).tag)
{http://ns/}c

>>> find = objectify.ObjectPath("root.b.c")
>>> print(find(root).tag)
{http://ns/}c

>>> find = objectify.ObjectPath("root.{http://other/}d")
>>> print(find(root).tag)
{http://other/}d

>>> find = objectify.ObjectPath("root.{not}there")
>>> print(find(root).tag)
Traceback (most recent call last):
  ...
AttributeError: no such child: {not}there

>>> find = objectify.ObjectPath("{not}there")
>>> print(find(root).tag)
Traceback (most recent call last):
  ...
ValueError: root element does not match: need {not}there, got {http://ns/}root

>>> find = objectify.ObjectPath("root.b[1]")
>>> print(find(root).tag)
{http://ns/}b

>>> find = objectify.ObjectPath("root.{http://ns/}b[1]")
>>> print(find(root).tag)
{http://ns/}b

除了字符串之外,ObjectPath 还接受路径段列表:

>>> find = objectify.ObjectPath(['root', 'b', 'c'])
>>> print(find(root).tag)
{http://ns/}c

>>> find = objectify.ObjectPath(['root', '{http://ns/}b[1]'])
>>> print(find(root).tag)
{http://ns/}b

您还可以使用以**“.”**开头的相对路径忽略实际的根元素并仅继承其名称空间:

>>> find = objectify.ObjectPath(".b[1]")
>>> print(find(root).tag)
{http://ns/}b

>>> find = objectify.ObjectPath(['', 'b[1]'])
>>> print(find(root).tag)
{http://ns/}b

>>> find = objectify.ObjectPath(".unknown[1]")
>>> print(find(root).tag)
Traceback (most recent call last):
  ...
AttributeError: no such child: {http://ns/}unknown

>>> find = objectify.ObjectPath(".{http://other/}unknown[1]")
>>> print(find(root).tag)
Traceback (most recent call last):
  ...
AttributeError: no such child: {http://other/}unknown

为了方便起见,单个点代表空的 ObjectPath(标识):

>>> find = objectify.ObjectPath(".")
>>> print(find(root).tag)
{http://ns/}root

ObjectPath 对象可用于操作树:

>>> root = objectify.Element("{http://ns/}root")

>>> path = objectify.ObjectPath(".some.child.{http://other/}unknown")
>>> path.hasattr(root)
False
>>> path.find(root)
Traceback (most recent call last):
  ...
AttributeError: no such child: {http://ns/}some

>>> path.setattr(root, "my value") # creates children as necessary
>>> path.hasattr(root)
True
>>> print(path.find(root).text)
my value
>>> print(root.some.child["{http://other/}unknown"].text)
my value

>>> print(len( path.find(root) ))
1
>>> path.addattr(root, "my new value")
>>> print(len( path.find(root) ))
2
>>> [ el.text for el in path.find(root) ]
['my value', 'my new value']

与属性分配一样,setattr() 接受列表:

>>> path.setattr(root, ["v1", "v2", "v3"])
>>> [ el.text for el in path.find(root) ]
['v1', 'v2', 'v3']

但请注意,仅当子项存在时,此上下文中才支持索引。 对不存在的子项建立索引不会扩展或创建此类子项的列表,但会引发异常:

>>> path = objectify.ObjectPath(".{non}existing[1]")
>>> path.setattr(root, "my value")
Traceback (most recent call last):
  ...
TypeError: creating indexed path attributes is not supported

值得注意的是,ObjectPath 不依赖于 objectify 模块或 ObjectifiedElement 实现。 它还可以与普通 lxml.etree API 中的元素结合使用。

Python数据类型

objectify 模块了解 Python 数据类型,并尽力让元素内容表现得像它们。 例如,它们支持普通的数学运算符:

>>> root = objectify.fromstring(
...             "<root><a>5</a><b>11</b><c>true</c><d>hoi</d></root>")
>>> root.a + root.b
16
>>> root.a += root.b
>>> print(root.a)
16

>>> root.a = 2
>>> print(root.a + 2)
4
>>> print(1 + root.a)
3

>>> print(root.c)
True
>>> root.c = False
>>> if not root.c:
...     print("false!")
false!

>>> print(root.d + " test !")
hoi test !
>>> root.d = "%s - %s"
>>> print(root.d % (1234, 12345))
1234 - 12345

但是,数据元素继续提供 objectify API。 这意味着诸如 len()、切片和索引(例如字符串)之类的序列操作不能表现为 Python 类型。 与所有其他树元素一样,它们显示了 objectify 元素的正常切片行为:

>>> root = objectify.fromstring("<root><a>test</a><b>toast</b></root>")
>>> print(root.a + ' me') # behaves like a string, right?
test me
>>> len(root.a) # but there's only one 'a' element!
1
>>> [ a.tag for a in root.a ]
['a']
>>> print(root.a[0].tag)
a

>>> print(root.a)
test
>>> [ str(a) for a in root.a[:1] ]
['test']

如果需要对数据类型运行序列操作,则必须向 API 询问真实的 Python 值。 字符串值始终可以通过普通的 ElementTree.text 属性获得。 此外,所有数据类都提供 .pyval 属性,该属性以纯 Python 类型返回值:

>>> root = objectify.fromstring("<root><a>test</a><b>5</b></root>")
>>> root.a.text
'test'
>>> root.a.pyval
'test'

>>> root.b.text
'5'
>>> root.b.pyval
5

但请注意,这两个属性在 objectify 中都是只读的。 如果要更改值,只需将它们直接分配给属性即可:

>>> root.a.text  = "25"
Traceback (most recent call last):
  ...
TypeError: attribute 'text' of 'StringElement' objects is not writable

>>> root.a.pyval = 25
Traceback (most recent call last):
  ...
TypeError: attribute 'pyval' of 'StringElement' objects is not writable

>>> root.a = 25
>>> print(root.a)
25
>>> print(root.a.pyval)
25

换句话说,objectify 数据元素的行为就像不可变的 Python 类型。 您可以替换它们,但不能修改它们。

递归数并表示

要查看当前使用的数据类型,您可以调用模块级 dump() 函数,该函数返回元素的递归字符串表示形式:

>>> root = objectify.fromstring("""
... <root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
...   <a attr1="foo" attr2="bar">1</a>
...   <a>1.2</a>
...   <b>1</b>
...   <b>true</b>
...   <c>what?</c>
...   <d xsi:nil="true"/>
... </root>
... """)

>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    a = 1 [IntElement]
      * attr1 = 'foo'
      * attr2 = 'bar'
    a = 1.2 [FloatElement]
    b = 1 [IntElement]
    b = True [BoolElement]
    c = 'what?' [StringElement]
    d = None [NoneElement]
      * xsi:nil = 'true'

您可以在同一个子节点的不同类型之间自由切换:

>>> root = objectify.fromstring("<root><a>5</a></root>")
>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    a = 5 [IntElement]

>>> root.a = 'nice string!'
>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    a = 'nice string!' [StringElement]
      * py:pytype = 'str'

>>> root.a = True
>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    a = True [BoolElement]
      * py:pytype = 'bool'

>>> root.a = [1, 2, 3]
>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    a = 1 [IntElement]
      * py:pytype = 'int'
    a = 2 [IntElement]
      * py:pytype = 'int'
    a = 3 [IntElement]
      * py:pytype = 'int'

>>> root.a = (1, 2, 3)
>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    a = 1 [IntElement]
      * py:pytype = 'int'
    a = 2 [IntElement]
      * py:pytype = 'int'
    a = 3 [IntElement]
      * py:pytype = 'int'

元素的递归字符串表示

通常,元素使用 lxml.etree 提供的 str() 标准字符串表示形式。 您可以为对象化元素启用漂亮的打印表示,如下所示:

>>> objectify.enable_recursive_str()

>>> root = objectify.fromstring("""
... <root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
...   <a attr1="foo" attr2="bar">1</a>
...   <a>1.2</a>
...   <b>1</b>
...   <b>true</b>
...   <c>what?</c>
...   <d xsi:nil="true"/>
... </root>
... """)

>>> print(str(root))
root = None [ObjectifiedElement]
    a = 1 [IntElement]
      * attr1 = 'foo'
      * attr2 = 'bar'
    a = 1.2 [FloatElement]
    b = 1 [IntElement]
    b = True [BoolElement]
    c = 'what?' [StringElement]
    d = None [NoneElement]
      * xsi:nil = 'true'

可以用相同的方式关闭此行为:

>>> objectify.enable_recursive_str(False)

如何匹配数据类型

Objectify 使用两种不同类型的元素。 结构元素(或树元素)表示对象树结构。 数据元素表示叶节点处的数据容器。 您可以使用objectify.Element()工厂显式创建树元素,并使用 objectify.DataElement() 工厂显式创建数据元素。

创建 Element 对象时,lxml.objectify 必须确定要为它们使用哪个实现类。 这对于树元素来说相对容易,而对于数据元素则不太容易。 算法如下:

  1. 如果元素有子元素,则使用默认的树类。
  2. 如果元素定义为 xsi:nil,请使用 NoneElement 类。
  3. 如果给出了“Python 类型注解”属性,请使用它来确定元素类,请参见下文。
  4. 如果给出了 XML Schema xsi:type 提示,请使用它来确定元素类,请参见下文。
  5. 尝试通过反复试验从文本内容类型确定元素类。
  6. 如果元素是根节点,则使用默认树类。
  7. 否则,对空数据类使用默认类。

您可以在设置时更改树元素和空数据元素的默认类。 ObjectifyElementClassLookup() 调用接受两个关键字参数 tree_classempty_data_class,它们确定在这些情况下使用的 Element 类。 默认情况下,tree_class 是一个名为ObjectifiedElement 的类,empty_data_class 是一个 StringElement

类型注解

"类型注解" 机制部署定义为 lxml.objectify.PYTYPE_ATTRIBUTE 的 XML 属性。 它可能包含以下任何字符串值:intlongfloatstrunicodeNoneType

>>> print(objectify.PYTYPE_ATTRIBUTE)
{http://codespeak.net/lxml/objectify/pytype}pytype
>>> ns, name = objectify.PYTYPE_ATTRIBUTE[1:].split('}')

>>> root = objectify.fromstring("""\
... <root xmlns:py='%s'>
...   <a py:pytype='str'>5</a>
...   <b py:pytype='int'>5</b>
...   <c py:pytype='NoneType' />
... </root>
... """ % ns)

>>> print(root.a + 10)
510
>>> print(root.b + 10)
15
>>> print(root.c)
None

请注意,您可以通过 set_pytype_attribute_tag(tag) 模块函数更改用于此属性的名称和命名空间,以防您的应用程序需要。 还有一个实用函数 annotate() 可以为树的元素递归生成此属性:

>>> root = objectify.fromstring("<root><a>test</a><b>5</b></root>")
>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    a = 'test' [StringElement]
    b = 5 [IntElement]

>>> objectify.annotate(root)

>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    a = 'test' [StringElement]
      * py:pytype = 'str'
    b = 5 [IntElement]
      * py:pytype = 'int'

XML Schema的数据类型注释

指定数据类型信息的第二种方法使用 XML Schema类型作为元素注解。 Objectify 知道那些可以映射到普通 Python 类型的类型:

>>> root = objectify.fromstring('''\
...    <root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...          xmlns:xsd="http://www.w3.org/2001/XMLSchema">
...      <d xsi:type="xsd:double">5</d>
...      <i xsi:type="xsd:int"   >5</i>
...      <s xsi:type="xsd:string">5</s>
...    </root>
...    ''')
>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    d = 5.0 [FloatElement]
      * xsi:type = 'xsd:double'
    i = 5 [IntElement]
      * xsi:type = 'xsd:int'
    s = '5' [StringElement]
      * xsi:type = 'xsd:string'

同样,有一个实用函数 xsiannotate() 可以递归地为树的元素生成**“xsi:type”** 属性:

>>> root = objectify.fromstring('''\
...    <root><a>test</a><b>5</b><c>true</c></root>
...    ''')
>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    a = 'test' [StringElement]
    b = 5 [IntElement]
    c = True [BoolElement]

>>> objectify.xsiannotate(root)

>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    a = 'test' [StringElement]
      * xsi:type = 'xsd:string'
    b = 5 [IntElement]
      * xsi:type = 'xsd:integer'
    c = True [BoolElement]
      * xsi:type = 'xsd:boolean'

但请注意,xsiannotate() 将始终使用为任何给定 Python 类型定义的第一个 XML 架构数据类型,另请参阅定义附加数据类(Defining additional data classes)。

实用函数 deannotate(){target="_balnk"} 可用于删除 {== py:pytype ==} 和/或 {== xsi:type ==} 信息:

>>> root = objectify.fromstring('''\
... <root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...       xmlns:xsd="http://www.w3.org/2001/XMLSchema">
...   <d xsi:type="xsd:double">5</d>
...   <i xsi:type="xsd:int"   >5</i>
...   <s xsi:type="xsd:string">5</s>
... </root>''')
>>> objectify.annotate(root)
>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    d = 5.0 [FloatElement]
      * py:pytype = 'float'
      * xsi:type = 'xsd:double'
    i = 5 [IntElement]
      * py:pytype = 'int'
      * xsi:type = 'xsd:int'
    s = '5' [StringElement]
      * py:pytype = 'str'
      * xsi:type = 'xsd:string'
>>> objectify.deannotate(root)
>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    d = 5 [IntElement]
    i = 5 [IntElement]
    s = 5 [IntElement]

您可以使用关键字参数 pytype(默认值:True)和 xsi(默认值:True)来控制应取消注释哪些类型属性。 deannotate(){target="_balnk"} 还可以通过设置 xsi_nil=True (默认值:False)来删除 xsi:nil 属性:

>>> root = objectify.fromstring('''\
... <root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...       xmlns:xsd="http://www.w3.org/2001/XMLSchema">
...   <d xsi:type="xsd:double">5</d>
...   <i xsi:type="xsd:int"   >5</i>
...   <s xsi:type="xsd:string">5</s>
...   <n xsi:nil="true"/>
... </root>''')
>>> objectify.annotate(root)
>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    d = 5.0 [FloatElement]
      * py:pytype = 'float'
      * xsi:type = 'xsd:double'
    i = 5 [IntElement]
      * py:pytype = 'int'
      * xsi:type = 'xsd:int'
    s = '5' [StringElement]
      * py:pytype = 'str'
      * xsi:type = 'xsd:string'
    n = None [NoneElement]
      * py:pytype = 'NoneType'
      * xsi:nil = 'true'
>>> objectify.deannotate(root, xsi_nil=True)
>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    d = 5 [IntElement]
    i = 5 [IntElement]
    s = 5 [IntElement]
    n = u'' [StringElement]

请注意,默认情况下 deannotate(){target="_balnk"} 不会删除 pytype 命名空间的命名空间声明。 要删除它们,并通常清理文档中的命名空间声明(通常在完成整个处理时),请传递选项 cleanup_namespaces=True。 此选项是 lxml 2.3.2 中的新增选项。 在旧版本中,请改用函数 lxml.etree.cleanup_namespaces(){target="_blank"}。

DataElement 工厂

为了方便起见, DataElement(){target="_blank"} 工厂一步创建一个具有 Python 值的元素。 您可以传递所需的 Python 类型名称XSI 类型名称

>>> root = objectify.Element("root")
>>> root.x = objectify.DataElement(5, _pytype="int")
>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    x = 5 [IntElement]
      * py:pytype = 'int'

>>> root.x = objectify.DataElement(5, _pytype="str", myattr="someval")
>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    x = '5' [StringElement]
      * myattr = 'someval'
      * py:pytype = 'str'

>>> root.x = objectify.DataElement(5, _xsi="integer")
>>> print(objectify.dump(root))
root = None [ObjectifiedElement]
    x = 5 [IntElement]
      * py:pytype = 'int'
      * xsi:type = 'xsd:integer'

XML Schema 类型 驻留在 XML 模式命名空间中,因此 DataElement(){target="_blank"} 尝试为您正确添加 xsi:type 属性值前缀:

>>> root = objectify.Element("root")
>>> root.s = objectify.DataElement(5, _xsi="string")

>>> objectify.deannotate(root, xsi=False)
>>> print(etree.tostring(root, pretty_print=True))
<root xmlns:py="http://codespeak.net/lxml/objectify/pytype" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <s xsi:type="xsd:string">5</s>
</root>

DataElement(){target="_blank"} 使用默认的 nsmap 来设置这些前缀:

>>> el = objectify.DataElement('5', _xsi='string')
>>> namespaces = list(el.nsmap.items())
>>> namespaces.sort()
>>> for prefix, namespace in namespaces:
...     print("%s - %s" % (prefix, namespace))
py - http://codespeak.net/lxml/objectify/pytype
xsd - http://www.w3.org/2001/XMLSchema
xsi - http://www.w3.org/2001/XMLSchema-instance

>>> print(el.get("{http://www.w3.org/2001/XMLSchema-instance}type"))
xsd:string

虽然您可以设置自定义命名空间前缀,但如果您选择这样做,则必须提供有效的命名空间信息:

>>> el = objectify.DataElement('5', _xsi='foo:string',
...          nsmap={'foo': 'http://www.w3.org/2001/XMLSchema'})
>>> namespaces = list(el.nsmap.items())
>>> namespaces.sort()
>>> for prefix, namespace in namespaces:
...     print("%s - %s" % (prefix, namespace))
foo - http://www.w3.org/2001/XMLSchema
py - http://codespeak.net/lxml/objectify/pytype
xsi - http://www.w3.org/2001/XMLSchema-instance

>>> print(el.get("{http://www.w3.org/2001/XMLSchema-instance}type"))
foo:string

请注意 lxml 是如何为 XML Schema 实例命名空间选择默认前缀的。 我们可以重写它,如下例所示:

>>> el = objectify.DataElement('5', _xsi='foo:string',
...          nsmap={'foo': 'http://www.w3.org/2001/XMLSchema',
...                 'myxsi': 'http://www.w3.org/2001/XMLSchema-instance'})
>>> namespaces = list(el.nsmap.items())
>>> namespaces.sort()
>>> for prefix, namespace in namespaces:
...     print("%s - %s" % (prefix, namespace))
foo - http://www.w3.org/2001/XMLSchema
myxsi - http://www.w3.org/2001/XMLSchema-instance
py - http://codespeak.net/lxml/objectify/pytype

>>> print(el.get("{http://www.w3.org/2001/XMLSchema-instance}type"))
foo:string

如果同一命名空间使用了不同的命名空间前缀,则必须小心。 命名空间信息被合并以避免在向树添加新子元素时出现重复定义,但此机制不适用于属性值的前缀:

>>> root = objectify.fromstring("""<root xmlns:schema="http://www.w3.org/2001/XMLSchema"/>""")
>>> print(etree.tostring(root, pretty_print=True))
<root xmlns:schema="http://www.w3.org/2001/XMLSchema"/>

>>> s = objectify.DataElement("17", _xsi="string")
>>> print(etree.tostring(s, pretty_print=True))
<value xmlns:py="http://codespeak.net/lxml/objectify/pytype" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" py:pytype="str" xsi:type="xsd:string">17</value>

>>> root.s = s
>>> print(etree.tostring(root, pretty_print=True))
<root xmlns:schema="http://www.w3.org/2001/XMLSchema">
  <s xmlns:py="http://codespeak.net/lxml/objectify/pytype" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" py:pytype="str" xsi:type="xsd:string">17</s>
</root>

如果您选择偏离标准前缀,则您有责任修复属性值的前缀。 对 xsi:type 属性执行此操作的一种便捷方法是使用 xsiannotate(){target="_blank"} 工具:

>>> objectify.xsiannotate(root)
>>> print(etree.tostring(root, pretty_print=True))
<root xmlns:schema="http://www.w3.org/2001/XMLSchema">
  <s xmlns:py="http://codespeak.net/lxml/objectify/pytype" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" py:pytype="str" xsi:type="schema:string">17</s>
</root>

当然,在构建对象化树时,不鼓励对同一个命名空间使用不同的前缀。

定义附加数据类

您可以将其他数据类插入 objectify,其使用方式与预定义类型完全相同。 数据类可以直接从 ObjectifiedDataElement{target="_balnk"} 继承,也可以从 NumberElement{target="_blank"} 或 BoolElement{target="_blank"} 等专用类之一继承。 数字类型需要初始调用 NumberElementself._setValueParser(function) 方法来设置其类型转换函数(字符串 -> Python 数字类型)。 此调用应放入 element _init() 方法中。

数据类的注册使用 PyType{target="_blank"} 类:

>>> class ChristmasDate(objectify.ObjectifiedDataElement):
...     def call_santa(self):
...         print("Ho ho ho!")

>>> def checkChristmasDate(date_string):
...     if not date_string.startswith('24.12.'):
...         raise ValueError # or TypeError

>>> xmas_type = objectify.PyType('date', checkChristmasDate, ChristmasDate)

PyType 构造函数接受字符串类型名称、(可选的)可调用类型检查和自定义数据类。如果提供了类型检查,则必须接受字符串作为参数,如果不能处理字符串值,则引发 ValueError 或 TypeError。

如果一个元素带有一个 py: pytype 属性来表示它的数据类型,或者在没有这样一个属性的情况下,如果给定的类型检查调用在应用到元素文本时没有引发 ValueError/TypeError 异常,则使用 PyType。

如果需要,您还可以在 XML 架构类型名称下注册此类型:

>>> xmas_type.xmlSchemaTypes = ("date",)

如果元素具有指定其数据类型的 xsi:type 属性,则将考虑 XML 架构类型。 上面的行将 XSD 类型日期绑定到新定义的 Python 类型。 请注意,这必须在下一步注册类型之前完成。 然后你就可以使用它:

>>> xmas_type.register()

>>> root = objectify.fromstring(
...             "<root><a>24.12.2000</a><b>12.24.2000</b></root>")
>>> root.a.call_santa()
Ho ho ho!
>>> root.b.call_santa()
Traceback (most recent call last):
  ...
AttributeError: no such child: call_santa

如果需要指定类型检查函数之间的依赖关系,可以通过 register() 方法的 beforeafter 关键字参数传递一系列类型名称。 然后,PyType 将尝试在相应类型之前或之后注册自身,只要它们当前已注册即可。 请注意,这仅影响注册时当前注册的类型。 稍后注册的类型不会关心已注册类型的依赖关系。

如果您提供 XML Schema 类型信息,这将覆盖上面定义的类型检查函数:

>>> root = objectify.fromstring('''\
...    <root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
...      <a xsi:type="date">12.24.2000</a>
...    </root>
...    ''')
>>> print(root.a)
12.24.2000
>>> root.a.call_santa()
Ho ho ho!

要取消注册类型,请调用其 unregister() 方法:

>>> root.a.call_santa()
Ho ho ho!
>>> xmas_type.unregister()
>>> root.a.call_santa()
Traceback (most recent call last):
  ...
AttributeError: no such child: call_santa

但请注意,这并不立即适用于已经存在 Python 引用的元素。 只有在所有引用消失并且 Python 对象被垃圾收集后,它们的 Python 类才会被更改。

高级元素类查找

在某些情况下,正常的数据类设置是不够的。 然而,lxml.objectify 基于 lxml.etree,支持对树中使用的 Element 类进行非常细粒度的控制。 您所要做的就是配置一种不同的类查找机制(或自己编写一个)。

设置的第一步是创建一个新的解析器来构建对象化文档。 objectify API 适用于以数据为中心的 XML(而不是具有混合内容的文档 XML)。 因此,我们将解析器配置为让它从解析的文档中删除纯空白文本(如果该文本未包含在 XML 元素中)。 请注意,这会改变文档信息集,因此如果您将删除的空格视为特定用例中的数据,则应该使用普通的解析器并仅设置元素类查找。 然而,大多数应用程序都可以通过以下设置正常工作:

>>> parser = objectify.makeparser(remove_blank_text=True)

它的内部作用是:

>>> parser = etree.XMLParser(remove_blank_text=True)

>>> lookup = objectify.ObjectifyElementClassLookup()
>>> parser.set_element_class_lookup(lookup)

如果您想更改查找方案,例如,获得对命名空间特定类的额外支持,您可以将 objectify 查找注册为后备 命名空间查找。 然而,在这种情况下,您必须注意命名空间类继承自 objectify.ObjectifiedElement,而不仅仅是普通的 lxml.etree.ElementBase,以便它们支持 objectify API。 上面的设置代码就变成了:

>>> lookup = etree.ElementNamespaceClassLookup(
...                   objectify.ObjectifyElementClassLookup() )
>>> parser.set_element_class_lookup(lookup)

有关更多信息,请参阅有关类查找方案的文档。

与 lxml.etree 有什么不同?

这种不同的 Element API 显然意味着对 API 其余部分的正常行为有一些副作用。

  • len(<element>) 返回同级计数,而不是 <element> 的子级数量。 您可以使用countchildren()方法检索子级的数量。
  • 元素的迭代不会产生子元素,而是产生兄弟元素。 您可以使用元素上的iterchildren()方法访问所有子项,或通过调用getchildren()方法检索列表。
  • findfindallfindtext 方法需要基于 ETXPath 的不同实现。 在lxml.etree中,他们使用基于原始迭代方案的Python实现。 这样做的缺点是它们可能无法 100% 向后兼容,另外的优点是它们现在支持任何 XPath 表达式。