[Python] 使用Python抓取XML內容,並儲存到Oracle

  • 1900
  • 0

我們時常會傳遞XML以為訊息交換方式,所以在這邊紀錄一下使用parsing XML in Python所需注意事項,以及寫入Oracle時可以使用的技巧。

目前在工作中需要將機台資料(XML格式)parse進資料庫,但因為環境限制,所以這邊使用Python的方式來做。首先假設我們有一支XML(test.xml)放在桌面上,它的內容長得像這樣:

<?xml version="1.0" encoding="utf-8"?>
<XA:XXXData xmlns:XA="XXX:XXXDataV9-9" ExtName="ExtensionXXXDataV9-9">
  <Data>
    <Lot>
      <LotBase LotID="XXXX.03" ARPFlag="1" StepperID="XXX-XXXX" ElevatorID="1" StartTime="2018-03-24T05:31:26"/>
      <LotExt PrevStepperID="Default_Stepper" />
    </Lot>
    <Field>
      <FieldBase IndX="-6" IndY="4" LocX="-124.3525408" LocY="114.2555333" Shift="0" Status="0"/>
    </Field>
    <Field>
      <FieldBase IndX="-5" IndY="4" LocX="-103.7143409" LocY="114.2555333" Shift="0" Status="0"/>
    </Field>
    <Field>
      <FieldBase IndX="-4" IndY="4" LocX="-83.076141" LocY="114.2555333" Shift="0" Status="0"/>
    </Field>
  </Data>
  <ci:ContextInfo xmlns:ci="XXX:XXX-v01">
    <List>
      <SlotNumber>17</SlotNumber>
      <StageID>2</StageID>
    </List>
  </ci:ContextInfo>
</XA:XXXData>

目的是要將這個XML裡面的大部份資料都寫進Oracle DB,而且資料要分別寫進兩個資料表,這兩個table是有關連性的,所以還要建立PK以及FK,主table寫完之後再將自動產生的PK連同其他資訊一併寫入到副table。以下先貼上程式碼(test.py),再一段一段解釋:

# -*- coding: utf-8 -*-
"""
Created on Fri Sep  7 13:52:17 2018

@author: Albert
"""

import cx_Oracle
import xml.etree.ElementTree as ET
import pandas as pd
import numpy as np

xmlFullPath = 'C:/Users/Albert/Desktop/test.xml'

#用來將DataFrame的資料一筆一筆塞進資料庫,並將對應的自動增量ID加進去DataFrame,以方便做關聯式資料的新增
#各個事件觸發順序是:__new__ => __init__ => __enter__ => __exit__ => __del__
class OracleHelp:
    #初始化時(觸發於建立物件之後)
    def __init__(self):
        self._db = cx_Oracle.Connection("{UserName}/{Password}@{IP}:{Port}/{ServiceName}")
        self._cursor = self._db.cursor()

    #資源回收時(觸發於物件消滅時)
    def __del__(self):
        self._cursor.close()
        self._db.close()

    #用於進入with語句時
    def __enter__(self):
        self._db = cx_Oracle.Connection("{UserName}/{Password}@{IP}:{Port}/{ServiceName}")
        self._cursor = self._db.cursor()
        return self 

    #用於離開with語句時
    def __exit__(self, type, value, tb):
        self._cursor.close()
        self._db.close()
 
    #提供呼叫的方法
    def AddEntityAndGetId(self, procedurename, df):
        #https://cx-oracle.readthedocs.io/en/latest/cursor.html
        #宣告一個變數,用來承接cursor回傳的ID(這邊是使用Oracle的procedure來寫入資料)
        entityID = self._cursor.var(cx_Oracle.NUMBER)
        
        for i in range(0, len(df)):
            arglist = df.values[i].tolist()
            
            if ("ID" not in df.columns.values.tolist() and "id" not in df.columns.values.tolist()):
                arglist.insert(len(arglist), entityID)
            else:
                arglist[len(arglist) - 1] = entityID
                
            #下面這一行執行完之後,entityID就會被塞值進去,就可以再getvalue(),並且寫進DataFrame裡面
            self._cursor.callproc(procedurename, arglist)
            df.set_value(i, 'ID', entityID.getvalue())

        return df

def Parse(xmlPath):
    lotBuf = []
    fieldBuf = []
    
    #將整個XML吃進來
    tree = ET.parse(xmlPath)
    
    #取得root節點
    root = tree.getroot()
    
    #https://docs.python.org/3.6/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.find
    #https://docs.python.org/3.6/library/xml.etree.elementtree.html#elementtree-xpath
    #使用find搭配XPath搜尋節點,在這邊可以找到LotBase節點
    lNode = root.find('./Data/Lot/LotBase')
    
    #https://docs.python.org/3.6/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.get
    #使用get取得特定Attribute,在這邊可以取得LotID屬性
    lotID = lNode.get('LotID')
    
    #繼續抓出其他Attribute的值
    arpFlag = lNode.get('ARPFlag')
    stepperID = lNode.get('StepperID')
    elevatorID = lNode.get('ElevatorID')
    startTime = lNode.get('StartTime')

    #重複利用lNode,在這邊改找出LotExt節點
    lNode = root.find('./Data/Lot/LotExt')
    prevStepperID = lNode.get('PrevStepperID')
    
    #使用XPath抓出特定namespace的節點
    ciNode = root.find("{XXX:XXX-v01}ContextInfo")
    slotNumber = ciNode.find('List').find('SlotNumber')
    stageID = ciNode.find('List').find('StageID')
    
    #塞資料進Array
    lotBuf = np.array([[lotID, arpFlag, stepperID, elevatorID, startTime, prevStepperID, slotNumber, stageID]])
    #建立DataFrame,以備後面批次塞Oracle使用
    lotDF = pd.DataFrame(lotBuf, columns = ['LOTID','ARPFLAG','STEPPERID','ELEVATORID','STARTTIME','PREVSTEPPERID','SLOTNUMBER','STAGEID'])
    
    #將DataFrame裡面的特定某些欄位的資料轉型為Numeric,否則可能會寫不進Oracle的Number欄位
    lotDF[['ARPFLAG','SLOTNUMBER']] = lotDF[['ARPFLAG','SLOTNUMBER']].apply(pd.to_numeric, errors='coerce')
    
    #建立一個OracleHelp物件
    oh = OracleHelp()
    
    #透過OracleHelp的AddEntityAndGetId方法,轉呼叫Oracle上面的procedure: INSERT_LOT,並且將新增時利用sequence自動遞增產生的ID回傳,並塞進lotDF裡面
    lotDF = oh.AddEntityAndGetId("INSERT_LOT", lotDF)
    
    #https://docs.python.org/3.6/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element.iter
    #找出root底下所有叫做Field的節點
    for fNode in root.iter('Field'):
        fBase = fNode.find('FieldBase')
        indX = fBase.get('IndX')
        indY = fBase.get('IndY')
        locX = fBase.get('LocX')
        locY = fBase.get('LocY')
        shift = fBase.get('Shift')
        status = fBase.get('Status')
        dbLotID = lotDF.iloc[0]['ID'] #從lotDF拿剛剛取回來的新Lot對應的自動產生的ID,用這樣來做出關聯性
        fieldBuf.append([indX,indY,locX,locY,shift,status,dbLotID])
    
    fieldDF = pd.DataFrame(fieldBuf, columns = ['IndX','IndY','LocX','LocY','Shift','Status','LotID'])
    fieldDF[['IndexX','IndexY','LocationX','LocationY','Status','LotID']] = fieldDF[['IndexX','IndexY','LocationX','LocationY','Status','LotID']].apply(pd.to_numeric, errors='coerce')
    fieldDF = oh.AddEntityAndGetId("INSERT_FIELD", fieldDF)
    
    #到這邊就完成把XML內容parse出來,並且寫進DB裡面的工作了,最後記得要commit
    oh._db.commit()

if __name__ == '__main__':
    Parse(xmlFullPath)

首先是OracleHelp,我們會利用這個類別來做完一整個跟DB相關的工作,包括「利用procedure寫入關聯性資料」以及「取回新建資料的ID」。其中的__init__、__del__、__enter__、__exit__等等,是指這個類別的物件相關事件的處理器,目的是要在物件建立完成之後自動取得一條Oracle連線,而且在物件消滅時自動關閉連線。而AddEntityAndGetId這個方法是主要方法,其中的self參數,是為了讓方法內部可以取得物件內部建立的_cursor物件,這樣才在_cursor指定一個回傳的變數,並在執行完SQL語法之後取得_cursor夾帶回來的值。

接著是entityID這個變數,因為_cursor.var()方法就是用來設定PL/SQL的In/Out變數用的,所以可以利用這個方式來取得_cursor.callproc之後回傳的ID值,用entityID.getvalue()就能取回,然後再放進DataFrame對應的列數的ID欄位中。跑完for迴圈之後再把整個DataFrame回傳,用來做下一個關聯資料表的PK的取得。

再來是Parse()這個方法,首先看到ET.parse()方法,只要餵XML的路徑給它,就可以將整個XML內容吃進程式,成為一個tree,然後呼叫getroot()來取得根節點。取得根節點之後,再利用find('XPath')方法取得特定節點,就可以再利用get()方法取得特定節點底下的Attribute的值。利用這樣的方式取得一筆資料所需的完整資訊之後,就可以lotBuf這個array中,再利用pd.DataFrame()將array轉成DataFrame:lotDF,最後呼叫oh.AddEntityAndGetId()將lotDF的資料一筆一筆存進DB並取回ID塞進lotDF。搞定Lot的資料之後,就可以再來處理Field的資料,這邊會用到iter()方法,這方法會取得該節點底下所有符合條件的節點,所以它回傳的是一個集合,我們就可以再直接套用for迴圈來取得每一個Field的資料並放進fieldBuf array中,後面就跟Lot一樣,轉成DataFrame並寫進資料庫,兩個資料都寫完之後再呼叫commit()就完工了。

Python Version:
3.6.6

Python Packages:
cx_Oracle (6.4.1)
pandas (0.23.0)
numpy (1.14.3)

Reference:
都寫在.py程式碼裡面了