精通 Oracle+Python,第 6 部分:Python 支持 XML
无可辩驳的是,XML 现在是软件中信息交换的实际标准。
因此,Oracle 数据库附带了各种与 XML 相关的增强和工具,它们统称为 Oracle XML DB。XML DB 包含一系列嵌入到数据库中的技术,用于在 SQL 级别访问和处理 XML 数据,提供对 XML Schema、XPath 或 XQuery 之类技术的访问。
而且,Python 附带了成熟的结构化标记解析器库,支持以简洁、优雅的方式访问和处理 XML。除了标准库中内含的模块之外,还有针对 libxml2 的 Python 绑定,libxml2 是一个常用解析器,您可能早已在使用其他语言时遇到过它。
在本教程中,我们将了解各种 Python XML 解析器,并了解一种使用简单 XML 协议与 Oracle 数据库通信的方法。
在深入了解解析之前,我们利用 XML DB 直接从 HR 模式生成两个 XML 文件,如清单 1 所示。
清单 1. 通过 SQL*Plus 脚本 (hrxml.sql) 生成 XML 文件
conn hr/hr
set timing off
set termout off
set heading off
set long 99999
spool emp.xml replace
select dbms_xmlgen.getxml('select * from employees') from dual;
spool dept.xml replace
select dbms_xmlgen.getxml('select * from departments') from dual;
exit
通过 sqlplus -S /nolog @hrxml.sql 运行此脚本会在当前目录中创建两个文件:dept.xml 和 emp.xml,它们分别包含表 DEPARTMENTS 和 EMPLOYEES 的全部内容。通过 dbms_xmlgen.getxml() 函数为 DEPARTMENTS 表生成的 XML 结构被标准化为以下格式:
<?xml version="1.0"?>
<ROWSET>
<ROW>
<DEPARTMENT_ID>10</DEPARTMENT_ID>
<DEPARTMENT_NAME>Administration</DEPARTMENT_NAME>
<MANAGER_ID>200</MANAGER_ID>
<LOCATION_ID>1700</LOCATION_ID>
</ROW>
...
</ROWSET>
首先,我们来看一下在处理 XML 时我们可以选用的 Python 中的解析器。
Python XML 档案
在 Python 标准库所提供的 XML 解析器中,有一些比较重要,它们是:
- xml.dom — 实现文档对象模型 (DOM),DOM 是分层 XML 树的标准表示。利用此模块,您可以随机访问 W3C 规范(DOM 2 级建议)中定义的任意 XML 数据。此 API 能够通过一组针对 XML 节点的标准方法遍历 DOM 树。
- xml.dom.minidom — 对 DOM 标准(DOM 1 级建议以及 2 级中的一些扩展)的简化、轻型的实现。清单 2 中的 to_minidom 函数展示了一种用于在普通 SQL 查询之外创建 Python minidom 对象的方法。
清单 2. 将 SQL 查询转变为 minidom 对象
import cx_Oracle
from xml.dom.minidom import parseString
def to_minidom(sql):
with cx_Oracle.connect("hr/hr") as db:
cursor = db.cursor()
cursor.execute("select dbms_xmlgen.getxml('%s') from dual" % sql)
return parseString(cursor.fetchone()[0].read())
if __name__ == "__main__":
md = to_minidom("select * from departments")
rows = md.getElementsByTagName("ROW")
print type(rows), len(rows)
直接运行此文件将产生这样的结果:<class 'xml.dom.minicompat.NodeList'> 27,通过获取 getElementsByTagName 结果的长度,我们获得了相当于 COUNT(*) 可获得的内容,只不过这次我们是通过 XML 方式进行的。
- xml.sax — XML 简单 API (SAX) 的实现,SAX 与 DOM 相反,允许对 XML 节点进行串行访问。使用 SAX 并不能访问任意 XML 元素,但是 SAX 在处理大型文件时比 DOM 更高效,DOM 需要为整个 XML 树分配内存,而 SAX 仅读取 XML 流的一部分。此模块在同一命名空间中附带多个辅助模块:xml.sax.handler,包含 SAX 处理程序接口;xml.sax.saxutils,包含许多用于处理 SAX 模型的有用实用程序;以及 xml.sax.xmlreader,包含 XMLReader SAX 接口实现。
- xml.etree.ElementTree — 随 Python 版本 2.5 提供,在此之前已经流行很久,ElementTree 模块代表另一类 XML 解析器。Fredrik Lundh 编写的模块效率很高且本质上是“Python 的”,因而很快融入到了 Python 的标准库中。ElementTree(及其 C 实现的对应 cElementTree)是一个将 XML 树映射到灵活的数据类型的对象,按照作者的说法,它是“跨列表和字典”的某种事物。ElementTree 由于其灵活性,目前被认为是在 Python 中用于解析 XML 的实际标准模块。本文后面的一些示例会清楚地说明这一点。
处理
下面使用在本教程开头生成的两个文件来演示 Python 的原生 XML 处理功能。通过使用 os.chdir() 函数更改当前路径(可以使用 os.getcwd() 获取当前路径),确保解释器能够访问这些文件。因为 xml.dom 和 xml.dom.minidom 基本上包含相同的功能,所以在此仅介绍后者。
清单 3. 使用 xml.dom.minidom 将部门解析为字典的示例
from xml.dom.minidom import parse
class ParseDept(dict):
def __init__(self, xmlfile):
dict.__init__(self)
self.dom = parse(xmlfile)
dept = {}
for i in self.dom.getElementsByTagName("ROW"):
dept_id = i.getElementsByTagName("DEPARTMENT_ID").pop().firstChild.data
self[dept_id] = {}
for j in i.childNodes:
if j.nodeType==self.dom.ELEMENT_NODE:
self[dept_id][j.tagName.lower()] = j.firstChild.data
if __name__ == "__main__":
dept = ParseDept("dept.xml")
清单 4. 使用 xml.sax 处理部门数据
from xml.sax import make_parser
from xml.sax.handler import ContentHandler
class DeptHandler(ContentHandler):
dept = {}
def startElement(self, name, attrs):
self.text = ""
return
def characters(self, ch):
self.text += ch
return
def endElement(self, name):
if name=="DEPARTMENT_ID":
self.curr = self.text
self.dept[self.text] = {}
elif name!="ROW":
self.dept[self.curr][name.lower()] = self.text
def __del__(self):
print self.dept
if __name__ == "__main__":
parser = make_parser()
parser.setContentHandler(DeptHandler())
parser.parse(open("dept.xml"))
清单 5. 使用 ElementTree 模块解析 XML
from xml.etree.ElementTree import ElementTree
class DeptTree:
def __init__(self, xmlfile):
self.tree = ElementTree()
self.tree.parse(xmlfile)
def traverse(self):
dept = {}
rows = self.tree.findall("ROW")
for row in rows:
for elem in row.findall("*"):
if elem.tag=="DEPARTMENT_ID":
dept_id = elem.text
dept[dept_id] = {}
else:
dept[dept_id][elem.tag.lower()] = elem.text
return dept
if __name__ == "__main__":
dt = DeptTree("dept.xml").traverse()
print dt
从这些实现来看,ElementTree 显然在可用性和开发速度方面更胜一筹。它通过易于使用、感觉非常自然的强大处理功能完善了 Python。而且,它具有名为 cElementTree 的 C 实现(记得 cPickle 吗?),这让它执行起来非常高效。
根据项目需求,有时 DOM 解析器可能会更有用。有了这一丰富的工具集,您便能够相应地选择合适的解析器。
XML API
说到信息交换,XML 是传输结构化数据的理想选择。诸如 WSDL、SOAP、RSS、XMPP、BPEL 之类的所有主要标准都基于 XML 构建,并且 XSLT、XQuery 或 XML Schema 等多种辅助技术使它们具有灵活性、兼容性。
数据库 API 通常限于 ADD_EMPLOYEE、DROP_DEPARTMENT 等原子级函数,当需要执行批处理操作时,通过中间件循环调用这些函数。执行这样的操作常常是因为客户端无法在数据库调用中使用复杂类型。cx_Oracle 也是这种情况。除了在本系列的第 5 部分中演示的 PL/SQL 数组的基本功能之外,您不能在 Python 中定义复杂对象然后将其用于 PL/SQL 调用。
随着 XML 技术的出现,Oracle 数据库通过各种此类技术得以丰富,包括 XQuery、XPath、XSLT、XML Schema、SQL/XML,这里仅举几例。SQL/XML 和 XML/SQL 扩展使得查询和构建 XML 数据如同使用关系数据那样简单,这确实是因为对关系模型和 XML 模型之间映射的支持非常广泛。
通过 XML、Python 和 Oracle XML DB,可以使用复杂结构调用 API。
清单 6. 通过 PKG_HR_XML 包进行基本 HR 管理
CREATE OR REPLACE PACKAGE pkg_hr_xml AS
FUNCTION department_get(
p_deptid IN NUMBER
) RETURN CLOB;
PROCEDURE department_merge(
p_deptxml IN CLOB
);
END pkg_hr_xml;
/
CREATE OR REPLACE PACKAGE BODY pkg_hr_xml AS
FUNCTION department_get(
p_deptid IN NUMBER
) RETURN CLOB AS
l_deptxml CLOB;
BEGIN
l_deptxml := dbms_xmlgen.getxml('
SELECT d.*, CURSOR(
SELECT *
FROM employees e
WHERE e.department_id=d.department_id
) emp
FROM departments d WHERE department_id='||p_deptid||'
');
RETURN l_deptxml;
END department_get;
PROCEDURE department_merge(
p_deptxml IN CLOB
) AS
l_deptid INT;
l_deptxml XMLType;
BEGIN
BEGIN
l_deptxml := XMLType(p_deptxml);
SELECT extractValue(l_deptxml, 'ROWSET/ROW/DEPARTMENT_ID[1]/text()')
INTO l_deptid FROM dual;
END;
MERGE INTO departments d
USING (
WITH t1 AS (
SELECT extract(l_deptxml, 'ROWSET/ROW/*[name(descendant::*)!="EMP_ROW"]') e
FROM dual)
SELECT
extractValue(e, '/DEPARTMENT_ID') department_id,
extractValue(e, '/DEPARTMENT_NAME') department_name,
extractValue(e, '/MANAGER_ID') manager_id,
extractValue(e, '/LOCATION_ID') location_id
FROM t1
) t ON (t.department_id=d.department_id)
WHEN MATCHED THEN
UPDATE SET d.department_name=t.department_name, d.manager_id=t.manager_id,
d.location_id=t.location_id
WHEN NOT MATCHED THEN
INSERT (department_id, department_name, manager_id, location_id)
VALUES (departments_seq.nextval, t.department_name, t.manager_id, t.location_id);
MERGE INTO employees e
USING (
SELECT
extractValue(value(f), '/EMP_ROW/EMPLOYEE_ID') employee_id,
extractValue(value(f), '/EMP_ROW/FIRST_NAME') first_name,
extractValue(value(f), '/EMP_ROW/LAST_NAME') last_name,
extractValue(value(f), '/EMP_ROW/EMAIL') email,
extractValue(value(f), '/EMP_ROW/PHONE_NUMBER') phone_number,
extractValue(value(f), '/EMP_ROW/HIRE_DATE') hire_date,
extractValue(value(f), '/EMP_ROW/JOB_ID') job_id,
extractValue(value(f), '/EMP_ROW/SALARY') salary,
extractValue(value(f), '/EMP_ROW/COMMISSION_PCT') commission_pct,
extractValue(value(f), '/EMP_ROW/MANAGER_ID') manager_id
FROM TABLE(XMLSequence(extract(l_deptxml , 'ROWSET/ROW/EMP/EMP_ROW'))) f
) t ON (t.employee_id=e.employee_id)
WHEN MATCHED THEN
UPDATE SET e.first_name=t.first_name, e.last_name=t.last_name,
e.email=t.email, e.phone_number=t.phone_number, e.hire_date=t.hire_date,
e.job_id=t.job_id, e.salary=t.salary, e.commission_pct=t.commission_pct,
e.manager_id=t.manager_id
WHEN NOT MATCHED THEN
INSERT (employee_id, first_name, last_name, email, phone_number, hire_date,
job_id, salary, commission_pct, manager_id, department_id)
VALUES (employees_seq.nextval, t.first_name, t.last_name, t.email,
t.phone_number, t.hire_date, t.job_id, t.salary, t.commission_pct,
t.manager_id, l_deptid);
END department_merge;
END pkg_hr_xml;
/
ALTER TRIGGER update_job_history DISABLE;
/
通过使用 PKG_HR_XML 包,我们可以利用两个重要的 Oracle XML DB 特性:通过 DBMS_XMLGEN 包将关系数据映射到 XML,通过 XMLSequence() 和 extract() 将 XML 节点提取到关系结构中。(注意我们需要禁用 update_job_history 触发器,因为 JOB_HISTORY 表中的 employee_id、start_date 列具有唯一约束。)
department_get() 和 department_merge() 均使用复杂的 XML 结构,其中包括单个部门信息以及此部门中的所有员工列表。这样,我们可以通过一个调用成批更新所有部门的员工信息。
在客户端,我们设置了一个基于 ElementTree 的 Python API,如清单 7 所示。
清单 7. 用 cx_Oracle 和 ElementTree 包装 PKG_HR_XML PL/SQL 包。
import cx_Oracle
from cStringIO import StringIO
from xml.etree.ElementTree import ElementTree
class Department:
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.cursor.close()
self.db.close()
def __init__(self, deptid):
self.db = cx_Oracle.Connection("hr/hr@xe")
self.cursor = self.db.cursor()
clob = self.cursor.var(cx_Oracle.CLOB)
return_value = self.cursor.callfunc("pkg_hr_xml.department_get", clob, [deptid])
try:
self.tree = ElementTree(file=StringIO(return_value.read()))
except AttributeError:
raise Warning, "Department %s not found." % deptid
def set(self):
clob = self.cursor.var(cx_Oracle.CLOB)
deptxml = StringIO()
self.tree.write(deptxml)
clob.setvalue(0, deptxml.getvalue())
self.cursor.callproc("pkg_hr_xml.department_merge", [clob])
def __str__(self):
deptxml = StringIO()
self.tree.write(deptxml)
return deptxml.getvalue()
if __name__ == "__main__":
with Department(60) as dept:
print dept
dept.set()
在清单 7 中的示例中,我们通过 Python XML API 使用 ElementTree 包装一个 PL/SQL 包。使 Department(60) 实例化并得到内存分配的 dept 变量代表一个 XML 树,它带有 set() 方法,通过该方法可以将更改保存回数据库。实际上,更改客户端 XML 将转换为 MERGE SQL 语句,该语句会执行必需的 INSERT 或 UPDATE。
要获得有关 ElementTree 的详细帮助以及许多示例,请访问 Fredrik Lundh 的 effbot.org。
总结
本教程介绍和演示了几种重要的 Python XML 方法。和 Oracle XML DB 一起使用时,有一些极佳的功能可以将这些技术结合在一起。Python 对 DOM、SAX 和树状 XML 结构提供即需即用的支持,为您处理 XML 数据提供了多种选择。
另一方面,因为 Python 可以作为理想的数据集成工具,所以其他序列化格式可以轻松地转换为 XML 和其他方式。由于 Python 数据类型的灵活性以及 Python 的动态本质,Python 成为了转换各种文件和数据格式的理想之选。