Search code examples
pythonxmlplist

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>

*Edit these are the for loops i have tried to get into the right location. It seems that it only goes for the last one though meaning it would put my new data into "Writing" instead of "Notes".

for plist_title in tree.xpath('//dict[key="Notes"][1]')
    for plist_tester in plist_title.xpath('//dict[key="13"][1]')
        plist_tester.insert(1,myData)

and if i wanted it to get into writing i would do something similar:

for plist_title in tree.xpath('//dict[key="Writing"][1]')
    for plist_tester in plist_title.xpath('//dict[key="13"][1]')
        plist_tester.insert(1,myData)

Or a different tester level:

for plist_title in tree.xpath('//dict[key="Notes"][1]')
    for plist_tester in plist_title.xpath('//dict[key="82"][1]')
        plist_tester.insert(1,myData)

sadly they dont work though :(


Solution

  • 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']