使用 Python 提取和解析 ODF 文件
开放文档格式 (ODF) 联盟旨在在不同的文字处理应用程序之间共享信息。本文重点介绍了 ODF 文件的基本结构、底层 XML 文件的一些内部结构,并展示了如何使用 Python 读取内容以执行简单的关键字搜索。该代码也可以作为更高级操作的基础。本着开放的精神,我们使用开源软件来读取 ODF 文件,在本例中是 Python 和 OpenOffice.org 软件包。
如果您运行的是最新版本的 Linux 或 OS X,那么您的机器上应该已经安装了 Python 和 OpenOffice.org。如果您需要最新版本,可以从 www.python.org 免费获取适用于 Windows 和 Linux 平台的 Python。OpenOffice.org 软件包也可以从 www.openoffice.org 免费获取。在 XP 桌面安装 OpenOffice.org 相对容易。从各自的站点下载软件包并运行安装程序。安装完成后,只需单击桌面上的已安装图标即可运行应用程序。
提示
大多数人确实安装了 Microsoft Office。如果是这种情况,解决方案是使用 Microsoft Word 的插件 (sourceforge.net/projects/odf-converter)。您可以在同一台机器上同时安装 OpenOffice.org 和 Microsoft 软件包,而不会引起任何冲突。
在安装插件之前,请阅读 SourceForge 站点上的“错误”部分,了解任何不兼容性。我使用了 OpenOffice.org 套件来保存本文的文件,因为它更容易。
一旦您确认您已具备先决条件,您就可以创建一个 ODF 文件。打开 Writer,在文档中键入一些文本并保存。您可以读入一个文件并将其另存为 .odt 文件。
快速查看“保存”对话框中的扩展名会发现很多信息。ODF 文件可以有许多扩展名,这些扩展名提供了有关其中存储的信息类型以及存储它的应用程序的线索。请参见表 1。
表 1. ODF 文件类型及其扩展名
文档格式 | 文件扩展名 |
---|---|
OpenDocument 文本 | *.odt |
OpenDocument 文本模板 | *.ott |
OpenDocument 主文档 | *.odm |
HTML 文档 | *.html |
HTML 文档模板 | *.oth |
OpenDocument 电子表格 | *.ods |
OpenDocument 电子表格模板 | *.ots |
OpenDocument 绘图 | *.odg |
OpenDocument 绘图模板 | *.otg |
OpenDocument 演示文稿 | *.odp |
OpenDocument 演示文稿模板 | *.otp |
OpenDocument 公式 | *.odf |
OpenDocument 数据库 | *.odb |
那么,ODF 文件中有什么?ODF 文件基本上是一个压缩的存档文件,其中包含多个 XML 文件。文件中的实际文件和目录将根据信息的类型和创建文档的系统而有所不同。
挑出 ODF 文件中文件名的第一步需要解压缩文件本身。幸运的是,Python 内置了对使用 zipfile 模块处理此操作的支持。键入python在命令行上运行交互式 shell。运行 shell 允许您检查从模块返回的对象的内容。因为您可能每种数据类型只执行一次此操作,所以此时没有必要编写和执行脚本。如果您想保留以供将来使用,最好在文本编辑器中编写脚本或使用 Python 自带的 IDLE 编辑器并保存脚本。请参见清单 1,了解如何在类或模块中显示成员函数。
清单 1. 在类或模块中显示成员函数
Python 2.4.1 (#65, Mar 30 2005, 09:13:57) [MSC v.1310 32 bit (Intel)] on win32 Type "copyright", "credits" or "license()" for more information. >>> import zipfile >>> myfile = zipfile.ZipFile ↪('E:/articles/odf/theArticle.odt') >>> dir(myfile) ['NameToInfo', '_GetContents', '_RealGetContents', '__del__', '__doc__', '__init__', '__module__', '_filePassed', '_writecheck', 'close', 'comment', 'compression', 'debug', 'filelist', 'filename', 'fp', 'getinfo', 'infolist', 'mode', 'namelist', 'printdir', 'read', 'start_dir', 'testzip', 'write', 'writestr'] >>> >>> >>> listoffiles = myfile.infolist() >>> dir(listoffiles[0]) ['CRC', 'FileHeader', '__doc__', '__init__', '__module__', 'comment', 'compress_size', 'compress_type', 'create_system', 'create_version', 'date_time', 'external_attr', 'extra', 'extract_version', 'file_offset', 'file_size', 'filename', 'flag_bits', 'header_offset', 'internal_attr', 'orig_filename', 'reserved', 'volume'] >>>
来自 Python 文档的 infolist() 命令返回压缩存档文件的对象列表。在此列表中的第一个对象上运行 dir() 命令,以获取为每个压缩文件存储的更多信息(清单 2)。查看对象成员的这个出色功能称为内省。
在对象列表上的迭代显示有关每个文件的相关信息,例如创建时间、提取大小、压缩大小等等。此时最好编写 Python 脚本并运行它,而不是在交互式 Python shell 的命令行中工作。这样,您可以保留脚本以供以后使用。因此,打开文本编辑器并键入脚本。
清单 2. 列出 ODT 存档文件中的文件
import sys, zipfile myfile = zipfile.ZipFile(sys.argv[1]) listoffiles = myfile.infolist() for s in listoffiles: print s.orig_filename
import 语句允许您使用 sys 模块获取文件的命令行参数,而 zipfile 模块加载用于读取和解压缩文件的功能。正如您从 Python shell 中看到的那样,zipfile 存档上的 infolist() 方法列出了其中的文件。因此,迭代 infolist() 中的项目,然后调用 orig_filename 成员函数,可以为您提供存档文件中所有文件的列表。
要获得更详细的信息,请尝试类似这样的操作
print s.orig_filename, s.date_time, s.filename, ↪s.file_size, s.compress_size
您将收到更多关于文件的信息,非常类似于此
mimetype (2006, 9, 9, 7, 50, 10) mimetype 39 39 Configurations2/statusbar/ (2006, 9, 9, 7, 50, 10) Configurations2/statusbar/ 0 0 Configurations2/accelerator/current.xml ↪(2006, 9, 9, 7, 50, 10) Configurations2/accelerator/current.xml 0 2 Configurations2/floater/ (2006, 9, 9, 7, 50, 10) Configurations2/floater/ 0 0 ...SNIPPED FOR BREVITY...
典型的 ODF 文本文件(带有 .odt 扩展名)在解压缩时将具有以下一些文件。这是输出
mimetype Configurations2/statusbar/ Configurations2/accelerator/current.xml Configurations2/floater/ Configurations2/popupmenu/ Configurations2/progressbar/ Configurations2/menubar/ Configurations2/toolbar/ Configurations2/images/Bitmaps/ layout-cache content.xml styles.xml meta.xml Thumbnails/thumbnail.png settings.xml META-INF/manifest.xml
存档文件中最重要的文件是 content.xml 文件,因为它包含文档本身的数据。我在这里讨论这个文件;但是,有关每个标签等的详细信息,请查看 OASIS 网站上 2,000 多页的 PDF 文件(请参阅“资源”)。
基本上,content.xml 文件看起来像一个 DHTML 文件,其中包含所有内容的标签。对于我的搜索操作,我最关心的标签是 <text:p> 标签及其结束标签 </text:p>,它包装文档中的段落。稍后我将在本文中向您展示如何从内容文件中获取此标签。
存档文件中其他感兴趣的文件是 META-INF/manifest.xml、mimetype、meta.xml 和 styles.xml。其他文件只是包含用于读取和显示内容文件的文字处理器的配置数据。
清单文件只是一个 XML 文件,其中列出了压缩存档文件中的所有文件。mimetype 文件是单行文件,其中包含内容文件的 mimetype。meta.xml 包含有关作者、创建日期等的信息。styles 文件包含用于显示文件的所有格式样式。
您可以使用 zip 对象上的 read() 方法从 ODF 文件中提取任何这些文件,以将其作为很长的字符串获取。读取后,您可以修改、查看并将整个字符串作为独立文件写入磁盘。清单 3 显示了如何提取 manifest.xml 文件。
清单 3. 提取 ODT 存档文件的文件
import sys, zipfile if len(sys.argv) < 2: print "Usage: extract odf-filename outputfilename sys.exit(0) myfile = zipfile.ZipFile(sys.argv[1]) listoffiles = myfile.infolist() for s in listoffiles: if s.orig_filename == 'META-INF/manifest.xml': fd = open(sys.argv[2],'w') bh = myfile.read(s.orig_filename) fd.write(bh) fd.close()
对于多个文件,您可以使用列表而不是 if 子句
if s.orig_filename in ['content.xml', 'styles.xml']:
这样,您可以通过简单地读取文件内容并操作它们或将它们写入磁盘来提取您需要查看的任何文件。
XML 文件的内容最适合作为树结构进行操作。使用 Python 中的 XML 解析功能来获取 XML 文件中所有节点的树。一旦您在内容文件中获得了树,您就可以轻松访问 <text:p> 节点。您实际上不必将文件提取到磁盘,因为您也可以在字符串上运行 XML 解析器,就像从文件中读取一样。
XML 解析器有两种类型:SAX 和 DOM。SAX 解析器更快,但内存密集程度较低,因为它一次读取和解析一个标签的输入文件。您一次只能处理一个标签,并且必须自己跟踪数据。相比之下,DOM 解析器将整个文件读入内存,因此为导航和操作 XML 节点提供了更好的选择。
让我们看看使用 SAX 和 DOM 的示例,以便您可以了解哪一种适合您的目的。SAX 示例展示了如何从 XML 文件中提取唯一节点名称。DOM 示例说明了在将文件完全读取到内存后,如何从特定节点中读取值。
首先,让我们使用 SAX 解析器查看 content.xml 文件中有哪些节点可用。该代码只是打印文件中找到的每种类型节点的名称。请注意,对于不同类型的文件,您可能会获得不同的节点名称(清单 4)。
清单 4. 列出唯一标签编号
# # This program will list out the uniq tag # names in a XML document. # Author: Kamran Husain # import sys from xml.sax import parse, ContentHandler class tagHandler(ContentHandler): def __init__(self, tagName = None): self.tag = tagName self.uniq = {} def startElement(self,name,attr): if self.tag == None: self.uniq[name] = 1; elif self.tag == name: self.uniq[name] = name # ignore attributes for now def getNames(self): return self.uniq.keys() if __name__ == '__main__': myTagHandler = tagHandler() parse(sys.argv[1], myTagHandler) myNames = [str(x) for x in myTagHandler.getNames()] myNames.sort() for x in myNames: print x
SAX 程序需要一个从 ContentHandler 派生的类,并重写函数来处理每个节点的开始、中间和结束。清单 4 中显示的 tagHandler 类主要用于跟踪每个节点的开始并忽略内容。使用清单中找到的名称作为字典中的键。然后,使用 keys() 方法将名称列出到列表中,您也可以对其进行 sort()。我经常使用此技术来快速获得唯一成员的排序,因为 Python 字典中的哈希函数非常快。这是程序的输出
office:automatic-styles office:body office:document-content office:font-face-decls office:forms office:scripts office:text style:font-face style:list-level-properties style:paragraph-properties style:style style:text-properties text:a text:line-break text:list text:list-item text:list-level-style-bullet text:list-style text:p text:s text:sequence-decl text:sequence-decls text:span
我排序了键列表,并且只打印了找到的标签类型。您将有很多标签,并且列出的标签顺序不是它们在 XML 文件中找到的顺序。您最有可能关心的标签是 <text:p>,它包含每个段落中的文本。在本例中,我想在一个或多个段落的文本中搜索关键字。
虽然我可以使用程序的 SAX 版本来搜索文本,但我使用了 DOM 库,因为代码编写起来会更容易一些(随后也更容易维护),而且我之前也承诺过一个示例。
Python 中的 xml.dom 和 xml.dom.minidom 软件包允许将 XML 文件作为 DOM 树读入。这两个软件包都随标准 Python 安装一起提供。使用 minidom 软件包,因为它占用空间更小,并且比 dom 软件包更易于使用。minidom 软件包足以满足我几乎所有的 XML 工作,我从未真正需要过重量级的 xml.dom 软件包。有关更多信息,请参阅“资源”。
使用 minidom 软件包将 XML 文件的元素读入树状结构。树的节点是基于 Python 中的 Node 类的对象。清单 4 的输出提供了节点名称。
到目前为止,您一直在使用简单的程序来列出和提取存档文件中的数据。现在,下一个示例对您刚刚读入的文件运行搜索操作。请查看清单 5 中的程序。
清单 5. 在 Python 中读取和解析 ODF
import os, sys import zipfile import xml.dom.minidom class OdfReader: def __init__(self,filename): """ Open an ODF file. """ self.filename = filename self.m_odf = zipfile.ZipFile(filename) self.filelist = self.m_odf.infolist() def showManifest(self): """ Just tell me what files exist in the ODF file. """ for s in self.filelist: #print s.orig_filename, s.date_time, s.filename, s.file_size, s.compress_size print s.orig_filename def getContents(self): """ Just read the paragraphs from an XML file. """ ostr = self.m_odf.read('content.xml') doc = xml.dom.minidom.parseString(ostr) paras = doc.getElementsByTagName('text:p') print "I have ", len(paras), " paragraphs " self.text_in_paras = [] for p in paras: for ch in p.childNodes: if ch.nodeType == ch.TEXT_NODE: self.text_in_paras.append(ch.data) def findIt(self,name): for s in self.text_in_paras: if name in s: print s.encode('utf-8') if __name__ == '__main__': """ Pass in the name of the incoming file and the phrase as command line arguments. Use sys.argv[] """ filename = sys.argv(0} phrase = sys.argv(1) if zipfile.is_zipfile(filename): myodf = OdfReader(filename) # Create object. myodf.showManifest() # Tell me what files # we have here myodf.getContents() # Get the raw # paragraph text. myodf.findIt(phrase) # find the phrase ...
该程序旨在作为一个类工作,该类读取和搜索 ODF 文件中的文本。声明 ODF 读取器的类有助于组织在节点内搜索文本的代码。showManifest() 成员函数只是告诉我 ODF 文件中存在哪些文件。在这个特定的程序中,我将所有文本收集为段落列表,然后搜索从命令行传入的关键字。如果搜索到的单词匹配,则打印段落。
在每个 <text:p> 中找到的文本是 Unicode 文本。您必须将其转换为普通文本才能正确打印和/或在小部件中使用。encode() 命令将 Unicode 转换为可打印的字符串。
Unicode 为每个字符提供了一个唯一的数字,而与所使用的平台、程序和语言无关。在多个平台上无缝处理相同文本的能力是启用 Unicode 的应用程序的一大特色。对于某些遗留操作,此类功能确实需要付出代价。每个 Unicode 字符都可以将标志设置为特殊打印等的位,这会导致普通 print 语句将每个字符解释为数字而不是文本。在 Python 中,Unicode 字符串的 encode() 成员函数返回字符串的可打印版本。这是一个示例代码片段
def findIt(self,name): for s in self.text_in_paras: if name in s: print s.encode('utf-8')
清单 5 中的代码不限于 ODT 文件。您可以修改此处提供的代码以处理带有 .ods 扩展名的电子表格文件。运行清单 3 中的程序以取出 content.xml 文件,然后运行第二个程序(如清单 4 所示)以列出节点类型。下面是一个 .ods 文件的示例;请注意,除了表格标签之外,此文件还包含段落
office:automatic-styles office:body office:document-content office:font-face-decls office:scripts office:spreadsheet style:font-face style:style style:table-column-properties style:table-properties style:table-row-properties table:table table:table-cell table:table-column table:table-row text:p
像以前一样使用清单 5 中的程序从段落中提取和搜索文本。简单地将 text:p 更改为 table:table-cell 即可在单元格而不是段落中搜索文本。
总而言之,ODF 文件是多个 XML 文件的压缩存档文件。其中一个文件包含已知标签中的内容。每种类型的 ODF 文件都可以根据存储的信息具有不同的标签。通过使用内省和 Python 中的 XML 解析功能,您可以列出文件中的节点类型,并将它们读入树结构。读取后,您可以仅从树中您感兴趣的那些节点中提取数据。
资源
OASIS 开放文档格式规范和相关信息可从 www.oasis-open.org/committees/tc_home.php?wg_abbrev=office 下载。
content.xml 文件中标签的文档可以在 www.oasis-open.org/committees/documents.php?wg_abbrev=office 找到。
从 www.python.org 下载 Python。
Python 快速参考,Alex Martelli:O'Reilly,2003 年。
Python 和 XML,Christopher A. Jones 和 Fred Drake, Jr.:O'Reilly,2001 年。
XML 口袋参考,第 3 版,Simon St. Laurent 和 Michael Fitzgerald:O'Reilly,2005 年。
Kamran Husain 从事软件工作已有 20 年。可以通过 khusain62@yahoo.com 与他联系。