使用 Ruby 操作 OOo 文档

作者:James Britt

OpenOffice.org (OOo) 是一套功能丰富的办公工具,包括文字处理、电子表格创建和演示文稿制作应用程序,其增强功能和整体质量都有所提高。OOo 名副其实,使其源代码和文件格式完全开放。对于任何希望在无需安装创建器应用程序的情况下操作文档的人来说,这是一个巨大的优势。

一般来说,有两种方法可以访问或操作文档内容。一种是自动化源应用程序,让程序代替人员输入命令。另一种是直接访问文档。第一种方法的优势在于,您可以利用现有应用程序的强大功能,从而节省大量时间来研究文件格式和处理命令。OOo 可以执行内部宏,并通过 UNO 公开脚本接口。缺点是您需要手头有实际的应用程序,即使这样,它也可能无法完成您想要的操作。本文描述了第二种方法:通过直接访问源文件来访问和操作文档。

OOo Extract

当我得知 Daniel Carrera 发布了他的 OOoExtract 程序时,我第一次意识到 OpenOffice.org 文档可以做些什么。这是一个 Ruby 应用程序,允许您对 OOo Writer 文档内容运行命令行搜索。正如主页所述,OOoExtract 对文本内容和样式执行匹配,使用完整的正则表达式执行搜索模式,并运行使用布尔运算符构建的搜索。该程序可以在任何安装了 Ruby 解释器的平台上运行,并且几乎适用于所有操作系统。

Linux Journal 之前曾讨论过 Ruby,但如果您不熟悉它,一个好的但简短的描述可能是说它是 Perl 和 Smalltalk 的混合体,并带有一些来自 Lisp 和 Python 的功能。它具有深度面向对象的特性,并且语法简洁直观。其创建者 Yukihiro “Matz” Matsumoto 于 1994 年发布了第一个 alpha 版本。它的受欢迎程度稳步增长,第三届国际 Ruby 会议于 2003 年 11 月在德克萨斯州奥斯汀举行。

要了解 OOoExtract 的使用感受,请下载该程序;目前,您可以将该应用程序作为单个可执行文件或包含独立文件中组成库的 tarball 获取。安装完成后,我们可以创建一个简单的 Writer 文档并运行一些搜索。如果您手头有 OOo,请启动它并输入一些简短的文本,例如

My sample document
It has two lines

将文件另存为 sample1.sxw 到您安装 OOoExtract 的同一目录,并从命令行运行 OOoExtract,如下所示

./ooo_extract.rb --text sample sample1.sxw
My sample document

该程序在 sample1.sxw 中搜索与单词 sample 匹配的任何行。实际上,这是一个正则表达式,尽管很简单。我们也可以使用更复杂的表达式,例如这个匹配任何三个字母单词的表达式

./ooo_extract.rb --text "\s\w\w\w\s"  sample1.sxw
It has two lines

这一切都很好,但 OOoExtract 真正的亮点在于它允许我们搜索内容元数据,即有关文档中文本的额外信息。假设我们在示例 Writer 文档中添加一行额外的行

This one has some extra formatting

输入文本后,选择单词 extra 并应用页脚段落样式。保存文件并运行此搜索

./ooo_extract.rb --style="Footer"  sample1.sxw
This one has some extra formatting

除了根据内容查找文本外,OOoExtract 还可以为您提供带有特定标记的文本。如果您创建自己的语义丰富的样式,这将非常方便。然后,您可以使用 OOoExtract 根据内容和含义检索信息,有效地将 OpenOffice.org Writer 文档转换为轻量级数据库。您可以通过在文件名中使用通配符来针对多个文件运行该程序。例如,假设您将食谱存储在 Writer 文件中。如果您定义并使用了自定义样式,则可以找到特定信息,例如哪些食谱包含苹果作为配料

./ooo_extract.rb --text="apple" --style="Ingredient" recipes/*.sxw
AppleSalsa.sxw: 2 medium red apples
AppleStrudel.sxw: 4 cups peeled and sliced apples
SXW 文件格式

那么,OOoExtract 是如何施展魔法的呢?秘密在于文件格式。虽然任何给定的 Writer 文件都具有 sxw 文件扩展名,但运行 UNIX 文件命令会告诉我们它是一个 zip 文件

$ file sample1.sxw
sample1.sxw: Zip archive data, at least v2.0 to extract

压缩了什么?让我们看看

$ unzip -l sample1.sxw
Archive:  sample1.sxw
  Length     Date   Time    Name
 --------    ----   ----    ----
       30  11-26-03 01:40   mimetype
     2328  11-26-03 01:40   content.xml
     8358  11-26-03 01:40   styles.xml
     1159  11-26-03 01:40   meta.xml
     7021  11-26-03 01:40   settings.xml
      752  11-26-03 01:40   META-INF/manifest.xml
 --------                   -------
    19648                   6 files

OOo XML 格式以纯文本形式公开所有内容和元数据;无需担心神秘的二进制编码或复杂的布局。由于数据以 XML 形式公开,因此可以使用许多现有的 XML 工具进行额外的 OOo 解析。以纯文本形式提供文件意味着,如果您只是查看,就可以获得您可能想了解的有关文件的任何信息。但是,我们得到了很大的帮助,因为 OpenOffice.org 团队还提供了各种文档,详细说明了该格式。《OpenOffice.org XML 文件格式 1.0 技术参考手册》是一份 571 页的 PDF 文档。我承认没有读过整本巨著,但我怀疑它不会缺少任何您可能想找到的细节。

为了我们的目的,我们只需要查看一些基本的标记,就可以了解 OOoExtract 的工作原理,并对标记有所了解。

如果您解压缩我们的示例文档并将 content.xml 加载到文本编辑器中,您应该会注意到一些事项。首先,该文件未针对您的观看乐趣进行格式化。您可能需要通过 XML 格式化工具(例如 tidy)运行该文件,以添加一些新行和缩进,使其更易于阅读。

该文件以 XML 声明开头,后跟 DOCTYPE 引用。紧随其后的是根元素 office:document-content。开始标记具有许多 XML 命名空间属性。我们不必担心这些,但它们可以让我们了解在 OOo 文档中可能找到的内容范围。

在根元素内部,我们立即找到脚本、字体声明和样式的子元素。由于我们的文档相当简单,因此此处的数据很少。对于我们当前的兴趣,有用的内容位于 office:body 元素内部。然而,即使在这里,也有一些元素只是声明了各种项目(例如表格和插图)的存在(或者,在我们的例子中,不存在)。完整文档可从 Linux Journal FTP 站点获取 [ftp://ftp.linuxjournal.com/pub/lj/listings/issue119/7236.tgz]。

我们文档中的实际内容出现在 text:p 元素内部

<text:p text:style-name="Standard">My sample
document</text:p>
<text:p text:style-name="Standard">It has two
lines</text:p>
<text:p text:style-name="Footer">This one has
some extra formatting</text:p>

顺便说一句,如果您不熟悉 XML 语法的某些细节,此表示法只是表示它是一个 p 元素,在文本命名空间中定义。前缀和冒号的使用是引用文档顶部给出的命名空间 URI 的简写方式。它用于避免与可能为某些其他 XML 词汇表定义的其他 p 元素发生名称冲突。为了我们的目的,我们可以简单地将其视为一个完整的元素名称。

我们的示例文档只有三个段落,因此正如我们可能期望的那样,有三个 text:p 元素。每个元素都有一个 text:style-name 属性,指示要应用于文本的样式。正是此属性使 OOoExtract 能够根据样式查找文本。

您可能想知道页脚样式。我们的 content.xml 文件没有定义它,实际上,这种将样式名称与实现细节分离的做法是很好的。如果文档不是一个简单的名称,而是包含字体大小和系列、颜色等各种属性,那就太可惜了。根据语义或结构数据查找内容的能力将丢失,我们将被限制为严格按照呈现方式处理数据。如果您真的想查看 OOo 如何定义页脚样式,您可以查看 styles.xml。在那里您会发现页脚样式基于标准样式,并进行了一些更改。

从 Zip 到 REXML

OpenOffice.org 使用压缩 XML 固然很好,但是一旦我们提取了这些文件,接下来会发生什么?幸运的是,Ruby 1.8 包括一个出色的 XML 解析器 REXML。REXML 是一个符合 XML 1.0 标准的解析器,除了它自己的 Ruby 风格的 API 之外,它还提供了 XPath 和 SAX2 的完整实现。它由 Sean Chittenden 开发和维护。Sean 说他编写 REXML 是因为,当时,Ruby 的 XML 解析只有两种选择。一种是绑定到本机 C 解析器,这可能会限制可移植性。另一种是纯 Ruby,但在 Sean 看来,它缺乏合适的 API。Sean 熟悉各种 Java XML 解析器,但不喜欢它们坚持 W3C 的 DOM 或社区驱动的 SAX。Electric XML 的设计者提供了一个基于已知 Java 习惯用语的 API,Java 程序员可以很容易地直观地理解它。

这就是 REXML API 背后的理念;名称代表 Ruby Electric XML。不过,毫不奇怪,REXML API 从最初的 Java 风格转向了 Ruby 方式的设计,允许开发人员使用 Ruby 通用的语法和功能(例如块和内置迭代器)来访问和操作 XML。

REXML API

REXML 树解析器可以轻松加载 XML 文档

require "rexml/document"
file = File.new( "som_xml_file.xml" )
doc = REXML::Document.new file

require "rexml/document"
my_xml_string = "<sample>
   <text>This is my REXML doc</text>
   </sample>"
doc = REXML::Document.new my_xml_string

Document 构造函数接受字符串或 I/O 对象;REXML 会找出它是哪一个并执行正确的操作。获得文档后,您可以结合 Ruby 的 Array 和 each 语法以及 XPath 选择器来查找元素

my_xpath = "sample/text"
doc.elements.each( my_xpath ){
    |el| puts el.text }

在上面的示例中,each 方法迭代 XPath 选择器匹配的每个元素。代码块({ ... } 内的部分)针对每次迭代调用。变量 el 是迭代中的当前元素,因此此示例只是打印 XPath 匹配的每个元素的文本。

XPath

我们的示例 Writer 文档及其对应的 XML 非常简单,因此找到我们想要的内容几乎是微不足道的。找出特定内容的正确元素并不需要太多时间。对于本文之类的文章来说,简单的示例可能是最好的,但在现实生活中,我们不太可能看到如此基本的内容。我们可能只知道标记的有限细节,例如样式属性或父元素。找到此类内容变得更具挑战性,但 XPath 有助于化险为夷。

XPath 是 W3C 关于寻址 XML 文档部分的建议。它允许人们构造路径说明符,该说明符根据元素和属性名称以及内容,加上相对或绝对定位来定义位置。给定一个复杂的 XML 文档,您可以定义一个 XPath 表达式,该表达式查找所有 text:p 元素,这些元素是 office:body 元素的直接子元素,表达式如下

*/office:body/text:p

前导星号(在 XPath 术语中)表示遵循 XML 文档树中通向 text:p 元素的任何路径,该元素是 office:body 元素的子元素。使用 REXML,我们可以使用此 XPath 来检索和迭代匹配元素的集合

xml.each_element( */office:body/text:p" ) do |el|
   # do something with el, such as
   # look for content or a style attribute
end

在此示例中,do 和 end 之间的代码是一个块。它就像一个匿名函数,针对集合中的每个项目(在本例中为与 XPath 匹配的每个元素)调用,其中项目作为参数传入,由“do”之后的两个竖线指示。这基本上是 OOoExtract 的工作原理,但您应该访问 OOoExtract 主页以了解有关众多命令行参数的详细信息。

迈向更通用的 OOo API

在了解了 OOoExtract 之后,我希望拥有一个更通用的 Ruby OOo 对象。驱动 OOoExtract 的相同基本思想不仅可以允许读取数据,还可以允许创建、更新和删除数据,例如,我们从数据库工具中了解和喜爱的 CRUD 操作。为此,在 RubyForge(Ruby 软件 CVS 存储库)上创建了一个名为 OOo4R 的项目。设计目标是简单地访问数据和元数据,透明地使用 XPath,以及直观的 API 来执行常见操作,例如添加段落、标题和样式。空间不允许完整地介绍所有此类功能,但我们可以查看访问文档元数据,以了解使用 Ruby 的动态消息处理来提取元素内容的一种方式。

前面我们看到,OOo 文档有多个 XML 文件打包在一个 zip 文件中。我们查看了 content.xml 文件;另一个是 meta.xml。它保存有关文档本身的信息,例如文档标题、创建日期和字数统计。根元素是 office:document-meta。反过来,它包含一个 office:meta 元素,其中包含许多带有感兴趣数据的子元素。例如

<meta:initial-creator>James Britt
</meta:initial-creator>
<meta:creation-date>2003-11-25T17:36:31
</meta:creation-date>
<dc:creator>James Britt</dc:creator>
<dc:date>2003-11-25T18:40:59</dc:date>
<dc:language>en-US</dc:language>
<meta:editing-cycles>13</meta:editing-cycles>

完整的元数据文件可从 Linux Journal FTP 站点获取 [ftp://ftp.linuxjournal.com/pub/lj/listings/issue119/7236.tgz]。

除了主 Document 类之外,OOo4R 还定义了一个元类来封装元数据。元类使用 REXML 文档来保存 meta.xml 的内容。元对象在很大程度上是属性的集合。典型的用法是向对象询问特定值(例如作者姓名),或分配值(例如新标题)。一种编写此代码的方法是编写一系列显式属性访问器方法。每个属性我们需要两个方法。或者,我们可以使用动态方法调用,通过抓取访问器消息、查找匹配的元属性,并对相应的属性执行请求的操作或引发异常。

以下代码示例重点介绍 OOo 中使用的都柏林核心元数据元素。都柏林核心元数据倡议是一个用于定义元数据标准的开放论坛。都柏林核心元素通常可以在 RSS 源和某些 XHTML 文档中找到。与 OpenOffice.org XML 文件中的所有元素一样,这些元素都有命名空间前缀。与其让用户了解和使用这些前缀,不如我们将完整的元素名称映射到友好的名称。

Meta 类的定义首先创建哈希,该哈希将友好的名称映射到实际的元素名称,以及一个类常量来保存元数据的基本 XPath。类构造函数只是从 XML 源创建一个 REXML 文档

module OOo
  class Meta

  NAME_MAP = {
   'description' => 'dc:description',
   'subject'     => 'dc:subject',
   'creator'     => 'dc:creator',
   'author '     => 'dc:creator',
   'date'        => 'dc:date',
   'language'    => 'dc:language',
   'title'       => 'dc:title'
  }
    XPATH_BASE  = "*/office:meta"

    def initialize( src )
      @doc = REXML::Document.new(  src.to_s )
    end

我们可以重新定义所有 Ruby 类都可用的 method_missing 方法,以便它不会引发异常(默认情况下会这样做),而是查看发送给对象的消息是否映射到我们元数据中的某个项目

def method_missing( name, *args )
  n = name.to_s
  if is_assignment? n
    el = map_for_assignment n
    xpath = "#{XPATH_BASE}/#{el}"
    assign( xpath, *args)
  else
    el = Meta.map_name n
    xpath = "#{XPATH_BASE}/#{el}"
    find( xpath  )
  end
end

method_missing 的第一个参数是一个符号对象,因此我们的代码会抓取字符串表示形式。is_assignment 方法只是检查名称是否以 = 字符结尾。如果这是一个赋值请求,则 map_for_assignment 会删除元数据名称后面的任何尾随字符,并将友好的名称映射到实际的都柏林核心元素名称;assign 更新 REXML 文档中的相应元素

def assign( xpath, val )
  node = @doc.elements.to_a( xpath )[0]
  node.text = val
end

如果这似乎不是赋值,则代码会尝试读取一些元数据。与之前一样,名称被映射,但现在代码调用 find

def find( xpath )
 begin
  return @doc.elements.to_a( xpath.to_s )[0].text
 rescue Exception
  raise OOo::OOoException.new(
     "Error with xpath '#{xpath}': #{$!}", $@ )
 end
end

# Helper methods omitted ...

 end
end

该技术适用于访问其他元数据元素,尽管在某些特殊情况下,元数据包含在一系列子元素中。使用 Ruby 内置的 Zip 类更新 zip 文件内容并将 zip 文件写回磁盘,使我们可以保存修改后的 OOo 文档。

总结

由于 OpenOffice.org 文件格式使用完全文档化的 XML 格式,因此可以在不需要 OOo 本身的情况下创建或操作 OOo 文件。Ruby 内置的 XML 处理和动态特性使其自然适合 OOo 任务。

James Britt 运营着位于亚利桑那州斯科茨代尔的软件和设计公司 Neurogami, LCC。他与人合著了一本关于 Wrox Press 的 XML 书籍,撰写了多篇关于软件开发的文章,并在德克萨斯州奥斯汀举行的第三届国际 Ruby 会议上就 Ruby 和 XML 进行了演讲。可以通过 jamesgb@neurogami.com 与他联系。

加载 Disqus 评论