lxml xpath / findall并更新

时间:2017-10-11 11:31:33

标签: python xml lxml

我正在尝试更新xml文档,以将子元素添加到由xpath标识的一组父元素中。

为此,我通过xpath确定要更改的元素,然后对这些元素运行for循环。我将这段代码称为'xpath for block'

我可以:

  • 成功识别需要更新的元素
  • 在'xpath for block'
  • 中更新元素
  • 在'xpath for block'
  • 中更新根节点

我不能:

  • 保留更新一次离开'xpath for block'(编辑除了循环中的最后一个元素)

代码:

from lxml import etree as ET

def _modify_mods_content(mods_content):
    """Massage the MODS content before auto-populating page

    Args:
        mods_content (str): content of MODS datastream as a string

    Returns:
        updated_content (str): updated MODS content
    """
    nsmap = {'mods': NAMESPACES['mods']}

    mods_xml = ET.fromstring(mods_content)

    # Create empty given-name element
    given_name_tag = ET.QName(NAMESPACES['mods'], 'namePart')
    given_name = ET.Element(given_name_tag)
    given_name.attrib['type'] = 'given'


    # xpath to find every personal name that does not have a child 
    # given name element
    no_given_names_xpath = '//mods:name[@type="personal"][not(mods:namePart[@type="given"])]'


    for element in mods_xml.xpath(no_given_names_xpath, namespaces=nsmap):
        element.insert(0, given_name)

    new_string = ET.tostring(mods_xml)
    return new_string

如果我把一个pdb提示符放到xpath for block中,我可以看到该元素正在更新,如果我在树上一直做'getparent',我可以看到root mods_xml文件似乎是仅为该实例更新

一旦我离开for循环,没有任何更新仍然存在。

例如:

原始档案:

<root>
  <mods:name type="personal">
    <mods:namePart type="family">Smith</mods:namePart>
  </mods:name>
  <mods:name type="personal">
    <mods:namePart type="family">Jones</mods:namePart>
  </mods:name>
</root>

第一次在xpath for block

<root>
  <mods:name type="personal">
    <mods:namePart type="given"></mods:namePart>
    <mods:namePart type="family">Smith</mods:namePart>
  </mods:name>
  <mods:name type="personal">
    <mods:namePart type="family">Jones</mods:namePart>
  </mods:name>
</root>

第二次在xpath for block中:

<root>
  <mods:name type="personal">
    <mods:namePart type="family">Smith</mods:namePart>
  </mods:name>
  <mods:name type="personal">
    <mods:namePart type="given"></mods:namePart>
    <mods:namePart type="family">Jones</mods:namePart>
  </mods:name>
</root>

编辑:以下示例最初没有插入元素。这些示例从更长和更复杂的文档中简化。我一定错过了最后的更新在离开街区后仍然存在。

一旦我离开街区

<root>
  <mods:name type="personal">
    <mods:namePart type="family">Smith</mods:namePart>
  </mods:name>
  <mods:name type="personal">
    <mods:namePart type="given"></mods:namePart>
    <mods:namePart type="family">Jones</mods:namePart>
  </mods:name>
</root>

我意识到这可以通过XSLT完成。我只是想知道是否有一种更'pythonic'的方式。

我实际上(有点)使它成功,但它是如此可怕的黑客a)我不想这样做,b)它只有在所有元素处于相同深度时才有效(幸运的是,他们正好在这种情况下):

    while mods_xml.xpath(no_given_names_xpath, namespaces=nsmap):
        elements = mods_xml.xpath(no_given_names_xpath, namespaces=nsmap)
        replacement_element = elements[0]
        replacement_element.insert(0, given_name)
        parent = elements[0].getparent()
        parent.replace(elements[0], replacement_element)
        new_xml_string = ET.tostring(parent, pretty_print=True)
        mods_xml = ET.fromstring(new_xml_string)

非常欢迎任何想法或评论!

1 个答案:

答案 0 :(得分:0)

一位不在Stack Overflow上的同事帮助回答了答案。

该问题与this issue here有关。我原以为我创建的given_name元素是一个空白元素,我每次都可以在循环中添加到父文档中(好像我正在进行字符串连接)。

事实上,lxml将元素视为一个独特的对象,只是将它移动到文件周围(上面的例子都是简化的,所以我一定错过了插入的最后位置)。

解决方案是每次都使用一个小的私有函数来生成一个新元素。

更正的代码:

def _modify_mods_content(mods_content):
    """Massage the MODS content before auto-populating xml_ora

    Args:
        mods_content (str): content of MODS datastream as a string

    Returns:
        updated_content (str): updated MODS content
    """
    nsmap = {'mods': NAMESPACES['mods']}
    try:
        mods_xml = ET.fromstring(mods_content)

        def __create_empty_given_name_element():
            """Create empty given-name element

            Returns:
                given_name (ET.element): empty element for processing
            """
            given_name_tag = ET.QName(NAMESPACES['mods'], 'namePart')
            given_name = ET.Element(given_name_tag)
            given_name.attrib['type'] = 'given'
            given_name.text = ''
            given_name.tail = '\n      '
            return given_name

        # xpath to find every personal name that does not have a child given name element
        no_given_names_xpath = '//mods:name[@type="personal"][not(mods:namePart[@type="given"])]'

        for element in mods_xml.xpath(no_given_names_xpath, namespaces=nsmap):
            given_name_element = __create_empty_given_name_element()
            element.insert(0, given_name_element)

        return_text = ET.tostring(mods_xml)   
    except Exception as _:  # pylint: disable=W0703
        # Something went wrong, but try to let a human fix it
        # by returning original MODS
        return mods_content
    return return_text
相关问题