我們時常會傳遞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程式碼裡面了