Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
234 views
in Technique[技术] by (71.8m points)

lxml - Python XML Remove Some Elements and Their Children but Keep Specific Elements and Their Children

I have a very large .xml file and I am trying to make a new .xml file that just has a small part of this larger file's contents. I want to specify an attribute (in my case, an itemID) and give it a few specific values and then it would strip away all the elements except for the ones that have those itemIDs and their children.

My large .xml file looks something like this:

<?xml version='1.0' encoding='UTF-8'?>
<api version="2">
  <currentTime>2013-02-27 17:00:18</currentTime>
  <result>
    <rowset name="assets" key="itemID" columns="itemID,locationID,typeID,quantity,flag,singleton">
      <row itemID="1008551770576" locationID="31000559" typeID="17187" quantity="1" flag="0" singleton="1" rawQuantity="-1" />
      <row itemID="1008700753886" locationID="31000559" typeID="17187" quantity="1" flag="0" singleton="1" rawQuantity="-1" />
      <row itemID="1008700756994" locationID="31000559" typeID="17184" quantity="1" flag="0" singleton="1" rawQuantity="-1" />
      <row itemID="1008701224901" locationID="31000559" typeID="17186" quantity="1" flag="0" singleton="1" rawQuantity="-1" />
      <row itemID="1004072840841" locationID="31002238" typeID="17621" quantity="1" flag="0" singleton="1" rawQuantity="-1">
        <rowset name="contents" key="itemID" columns="itemID,typeID,quantity,flag,singleton">
          <row itemID="150571923" typeID="25863" quantity="2" flag="119" singleton="0" />
          <row itemID="188435728" typeID="3388" quantity="1" flag="119" singleton="0" />
          <row itemID="210122947" typeID="3419" quantity="4" flag="119" singleton="0" />
        </rowset>
      </row>
      <row itemID="1005279202146" locationID="31002238" typeID="17621" quantity="1" flag="0" singleton="1" rawQuantity="-1">
        <rowset name="contents" key="itemID" columns="itemID,typeID,quantity,flag,singleton">
          <row itemID="1004239962001" typeID="16275" quantity="49596" flag="4" singleton="0" />
          <row itemID="1005364142068" typeID="4246" quantity="156929" flag="4" singleton="0" />
          <row itemID="1005624252854" typeID="4247" quantity="93313" flag="4" singleton="0" />
        </rowset>
      </row>
      <row itemID="1004388226389" typeID="648" quantity="1" flag="0" singleton="1" rawQuantity="-1">
        <rowset name="contents" key="itemID" columns="itemID,typeID,quantity,flag,singleton">
          <row itemID="1004388228218" typeID="31119" quantity="1" flag="92" singleton="1" rawQuantity="-1" />
          <row itemID="1004388701243" typeID="31119" quantity="1" flag="94" singleton="1" rawQuantity="-1" />
          <row itemID="1004388701485" typeID="31119" quantity="1" flag="93" singleton="1" rawQuantity="-1" />
          <row itemID="1009147502645" typeID="51" quantity="1" flag="5" singleton="1" rawQuantity="-1" />
        </rowset>
      </row>
    </rowset>
  </result>
  <cachedUntil>2013-02-27 23:00:18</cachedUntil>
</api>

This file has around ninety thousand rows and is about 9 megabytes.

Note how there are itemIDs and some item types can (but doesn't always) have more items inside them and these children also have their own itemIDs. I am trying to get a few specific itemIDs and their children and leave out all the others.

I used the code from this answer and it gets me quite close. It is perfect except that it leaves out the children of the itemID I used.

My code looks like this:

import lxml.etree as le

##Take this big .xml file and pull out just the parts we want then write those to a new .xml file##
with open(filename,'r') as f:
    doc=le.parse(f)
    for elem in doc.xpath('//*[attribute::itemID]'):
        if elem.attrib['itemID']=='1004072840841':
            elem.attrib.pop('itemID')
        else:
            parent=elem.getparent()
            parent.remove(elem)
    print(le.tostring(doc))

This is what the resulting print out looks like:

<api version="2">
  <currentTime>2013-03-01 21:46:52</currentTime>
  <result>
    <rowset name="assets" key="itemID" columns="itemID,locationID,typeID,quantity,flag,singleton">
      <row locationID="31002238" typeID="17621" quantity="1" flag="0" singleton="1" rawQuantity="-1">
        <rowset name="contents" key="itemID" columns="itemID,typeID,quantity,flag,singleton">
          </rowset>
      </row>
      </rowset>
  </result>
  <cachedUntil>2013-03-02 03:46:53</cachedUntil>
</api>

I want it to look like this:

<api version="2">
  <currentTime>2013-03-01 21:46:52</currentTime>
  <result>
    <rowset name="assets" key="itemID" columns="itemID,locationID,typeID,quantity,flag,singleton">
      <row locationID="31002238" typeID="17621" quantity="1" flag="0" singleton="1" rawQuantity="-1">
        <rowset name="contents" key="itemID" columns="itemID,typeID,quantity,flag,singleton">
          <row itemID="150571923" typeID="25863" quantity="2" flag="119" singleton="0" />
          <row itemID="188435728" typeID="3388" quantity="1" flag="119" singleton="0" />
          <row itemID="210122947" typeID="3419" quantity="4" flag="119" singleton="0" />
        </rowset>
      </row>
      </rowset>
  </result>
  <cachedUntil>2013-03-02 03:46:53</cachedUntil>
</api>

I don't understand the code well enough to see what I'd need to change in order to also include the children of the itemID I have it search for. Also, ideally I'd be able to put in multiple itemIDs and it would strip away all but those itemIDs and their children. This means it would need to keep the itemID=[number] row attribute (so that I could use xPath to refer to a particular itemID and its children when I use this xml file.)

So my main question is about how to include the children of the itemID I search for in my resulting .xml. My secondary question is about how to do this for more than one itemID at the same time (so that the resulting .xml file would strip away all but those itemIDs and their children.)

UPDATE: UGLY SOLUTION

I figured out that the elem.attrib.pop('itemID') part was the part that took out the itemID and since I'd like to have mutliple itemIDs and their children remain I needed to keep this, so I took that part out. I was trying to find a way to skip over the children of the line with the itemID I was searching for and what I came up with was to flag each one with an attribute that I could then search back over and delete all that don't have that attribute. I don't need the flag attribute for what I'm doing so I went ahead and used it for this purpose (as attempts to introduce a new attribute were meeting with a key error when I tried to iterate back over them.) Just flagging the children wasn't enough, I had to also tag the children's children.

Here is my ugly solution:

with open(filename,'r') as f:
    doc=le.parse(f)
    for elem in doc.xpath('//*[attribute::itemID]'):
        if elem.attrib['itemID']=='1004072840841' or elem.attrib['itemID']=='1005279202146': # this or statement lets me get a resulting .xml file that has two itemIDs and their children
            elem.attrib['flag']='Keep'
            for child in elem.iterchildren():
                child.attrib['flag']='Keep'
                for c in child.iterchildren():
                    c.attrib['flag']='Keep'
        else:
            pass
    for e in doc.xpath('//*[attribute::flag]'):
        if e.attrib['flag']!='Keep':
            parent=e.getparent()
            parent.remove(e)
        else:
            pass
    print(le.tostring(doc))
    ##This part writes the pruned down .xml to a file##
    with open('test.xml', 'w') as t:
        for line in le.tostring(doc):
            t.write(line)
    t.close

This ugly solution involves a lot of iterating over the data and is, I suspect, far from the most efficient way of getting this done, but it does work.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

It's not very clear exactly what you're after, but this code produces the output you say you'd like:

from lxml import etree as ET

def filter_by_itemid(doc, idlist):
    rowset = doc.xpath("/api/result/rowset[@name='assets']")[0]
    for elem in rowset.getchildren():
        if int(elem.get("itemID")) not in idlist:
            rowset.remove(elem)
    return doc

doc = ET.parse("test.xml")
filter_by_itemid(doc, [1004072840841])

print(ET.tostring(doc))

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...