Dippy Dippy - 3 years ago 142
Python Question

Adding data to deep node in Plist with Python

I have a plist that I initially pull information from then need to write information to it. With xml.etree.ElementTree, i can access "WUT" but if i try to overwrite that with the number '1' or add another line with '2' after it using this:

ET.SubElement(plist[0][15][1][1][1][1][1][1][1], '1')


or with

plist[0][15][1][1][1][1][1][1][1].append('1')


I will get an index out of bounds error because obviously the node 1 doesnt exist yet.

I need to add to a specific node (and in most cases create that node). Below under the WUT I would like to add roughly 200 string children with random information (lets say 1-200 for now). How would I do this with either plistlib or xml.etree.ElementTree.

<key>Title</key>
<dict>
<key>Set</key>
<dict>
<key>Notes</key>
<dict>
<key>Tester</key>
<array>
<dict>
<key>13</key>
<dict>
<key>Param</key>
<array>
<string>WUT</string>
</array>
</dict>
</dict>
<dict>
<key>82</key>
<dict>
<key>Param</key>
<array>
<string>WUT</string>
</array>
</dict>
</dict>
<dict>
<key>64</key>
<dict>
<key>Param</key>
<array>
<string>WUT</string>
</array>
</dict>
</dict>


and the result i would like to achieve is:

<key>Title</key>
<dict>
<key>Set</key>
<dict>
<key>Notes</key>
<dict>
<key>Tester</key>
<array>
<dict>
<key>13</key>
<dict>
<key>Param</key>
<array>
<string>1</string>
<string>2</string>
<string>3</string>
<string>4</string>
<string>5</string>
<string>6</string>
<string>7</string>
</array>
</dict>
</dict>
<dict>
<key>82</key>
<dict>
<key>Param</key>
<array>
<string>WUT</string>
</array>
</dict>
</dict>
<dict>
<key>64</key>
<dict>
<key>Param</key>
<array>
<string>WUT</string>
</array>
</dict>
</dict>

Answer Source

Use lxml with a bit of xpath magic:

from lxml import etree

from lxml.builder import E

plist = """
<root>
  <key>Title</key>
  <dict>
    <key>Set</key>
    <dict>
      <key>Notes</key>
      <dict>
        <key>Tester</key>
        <array>
          <dict>
            <key>13</key>
            <dict>
              <key>Param</key>
              <array>
                <string>WUT</string>
              </array>
            </dict>
          </dict>
        </array>
      </dict>
    </dict>
  </dict>
</root>"""

This creates an lxml object (you can load it from a file too):

xml_data = etree.fromstring(plist)

Create candidate element array:

array = E('array')

for num in range(10):
    array.append(E.string(str(num)))

Here is what it looks like:

print(etree.tostring(array, pretty_print=True))
<array>
  <string>0</string>
  <string>1</string>
  <string>2</string>
  <string>3</string>
  <string>4</string>
  <string>5</string>
  <string>6</string>
  <string>7</string>
  <string>8</string>
  <string>9</string>
</array>

From the main plist, grab the interesting entry, in this case is the first array that contains WUT, remove it and append the tag you want:

for first_array_contains_wut in xml_data.xpath('//array[string="WUT"][1]'):
    parent_tag = first_array_contains_wut.getparent()
    parent_tag.remove(first_array_contains_wut)
    parent_tag.append(array)

And here is what the final version looks like after mangling:

print(etree.tostring(xml_data, pretty_print=True))
<root>
  <key>Title</key>
  <dict>
    <key>Set</key>
    <dict>
      <key>Notes</key>
      <dict>
        <key>Tester</key>
        <array>
          <dict>
            <key>13</key>
            <dict>
              <key>Param</key>
              <array><string>0</string><string>1</string><string>2</string><string>3</string><string>4</string><string>5</string><string>6</string><string>7</string><string>8</string><string>9</string></array></dict>
          </dict>
        </array>
      </dict>
    </dict>
  </dict>
</root>

From the comments:

If the key "Notes" could change around to "Writing" in the same plist, is there a way to use xpath to search for both "Writing" and the "13" key? If both Notes and writing had 13, there is no way i could be sure i have found it. I tried with 2x for loops, but i always end up with the second "13".

You can try a more explicit match, if this is what I understand your node to look like:

<root>
  <key>Title</key>
  <dict>
    <key>Set</key>
    <dict>
      <key>Notes</key>
      <dict>
        <key>Tester</key>
        <array>
          <dict>
            <key>13</key>
            <dict>
              <key>Param</key>
              <array>
                <string>WUT</string>
              </array>
            </dict>
          </dict>
        </array>
      </dict>
      <dict>
        <key>Writing</key>
        <dict>
          <key>Tester</key>
          <array>
            <dict>
              <key>13</key>
              <dict>
                <key>Param</key>
                <array>
                  <string>WUT2</string>
                </array>
              </dict>
            </dict>
          </array>
        </dict>
      </dict>
    </dict>
  </dict>
</root>

If I want to get 'WUT2' which is under the dict that has 'Writing', you can do:

In [26]: [x.text for x in xml_data.xpath('//dict[key/text()="Writing"]//string')]
Out[26]: ['WUT2']

However if you are after the second '13' key, then you can do something like:

In [35]: [x.text for x in xml_data.xpath('//dict[//key[text()="13"]][2]//string')]
Out[35]: ['WUT2']
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download