Office 文档中的令牌替换






4.89/5 (11投票s)
一种使用动态内容替换生成 Office 文档的新高效方法。
引言
一种常见的情况是从数据库中的信息生成发票等。客户通常有极其定制的发票格式,但要填写的数据基本相同。本文及其相关代码提出了一种新的(我搜索过,但找不到与此主题相关的任何内容,因此我认为它是新的)、创新的、高效的生成动态 Office 文档的方法。我在整篇文档中使用“Office”一词,因为它不局限于 Microsoft Office。我也在 Open Office 上测试过这种方法,并且有效。关联的代码支持在这两种 Office 产品上执行令牌替换。
背景
到目前为止,我一直在动态创建 HTML,并将其提供为可下载的 *.doc 或 *.xls 文件,利用 Office 应用程序处理 HTML 文档的功能。但是,这种方法有一个局限性。在 HTML 中精确放置方面存在明显限制,以及 Office 应用程序如何解释和显示它。更不用说生成动态 HTML 时缺失的 Office 文档的高级处理功能。
自从 Office 产品采用 XML 格式以来,一种新方法一直在我的脑海中盘旋。我们开发者一直在使用令牌替换来生成动态内容。用 Office 文档来做怎么样?!
因此,当我需要生成的文档超出了 HTML 格式化的能力时,我终于有机会实现了这一点。现在,我正在尝试创建一个 Word 文档,其中包含 [$TokenName$] 形式的令牌,并通过编程方式将令牌替换为实际文本。然而,这并不像我想的那么容易。
(续) 我试图在 Microsoft 论坛 和 Brian Jones 的博客 上讨论我的方法。但是,我没有得到任何有用的帮助。
所以,我决定自己解决这个问题。
更多背景信息
正如我所说,我将总体上讨论 Office XML,而不是特别涉及 Microsoft Office 或 Open Office。在继续之前,请考虑一点,只有 Microsoft Office 2007 应用程序使用完整的 XML 文档格式。Office 2003 可能使用 XML,但我**尚未**研究 Office 2003 的文档格式。我也未在 Office 2003 文档上测试过代码。
关于 OO,我特意为此项目下载了它。下载时版本是 2.4。所以,它应该可以在该版本及更高版本上运行。但是,你作为开发人员,不必担心。请继续阅读。
在实际使用我将要描述的这种方法(你可以说,我别无选择)之前,我评估了多种选择。VSTO 以 Microsoft Office 为中心。此外,根据我在论坛上收集到的关于 VSTO 的最少知识,我认为它无济于事。Microsoft 的 Open XML SDK 确实很有吸引力。但是,我一点也不对 Microsoft Office 使用的 Open XML 架构感兴趣,而且该产品的文档明确指出,你需要对 XML 架构有深入的了解才能利用这个库。
然后,我拆开了(它只是一个 zip 文件,解压缩即可)一个 Microsoft Office 2007 文档,并分析了其内容。它是纯 XML。我立即决定使用 .NET 的 XML 处理功能来处理它,它实际上只是纯粹的 XML,而不是将其视为任何特殊的架构。
因此,附件代码不需要安装任何特殊的东西。你可以在桌面应用程序以及 Web 应用程序中轻松使用它,唯一的要求是执行代码的机器上安装了 .NET 3.5。甚至不需要安装关联的 Office 产品。我将其视为纯 XML,因此**没有任何**其他要求。只需准备一个包含令牌的 Office 文档,将文档与代码一起部署,你就可以开始工作了。
现在开始讲代码
一切都始于打开一个 Office 包(现在技术上称为 Office 包,因为它不仅仅是一个文件)。一个好的起点是使用你喜欢的 Zip 工具打开任何 Office 文档并分析其内容。
在此令牌替换过程中,我使用 SharpZipLib 库来处理与 Zip 文件相关的一切。打开 Zip 文件后,你会发现有多个文件。“Microsoft Word 2007 的主要内容文件是“word/document.xml”,而“OO Writer 的是“content.xml”。目前,代码只能处理 Word 和 Writer 包。我会腾出时间来添加对 Excel 和 OO Calc 的支持。我承认附加代码目前还不够成熟。尽管如此,我还是写这篇文章是为了与大家讨论,以便你可以根据自己的情况进行改编和增强,如果你觉得它有用的话。我不确定我何时能自己进行增强。
(截至该库的第 3 个版本)整体代码结构分为 4 个组件
- Replacers - 这些表示 Office 文档的各个部分(页眉、内容、页脚等)。令牌替换可以独立地在每个部分上执行。这些是在该库的第 2 个版本中添加的。
- Documents - 这些类代表 Office 文档本身(.docx 等)。每个文档都可以选择公开所需的节。例如,Microsoft Word 和 OO Writer 文档公开页眉、内容和页脚节,其中每个节都有一个关联的 replacer,该 replacer 在该节中执行令牌替换。以下代码片段有助于阐明这一点
doc.header.replaceToken("[$Date$]", DateTime.Today) doc.content.replaceToken("[$Consignee$]", Loreium Ipsum)
- Interfaces - 这些接口由 Office 文档实现(在下面的 Add-In 部分中说明)。
- Helper classes - 这些类提供辅助函数以帮助令牌替换。
直接打开 Zip 文件后,我将其内容读入一个 XDocument
(代码在各处都使用了 LINQ 和 LINQ to XML)。目前,实际令牌替换的主要功能来自 TokenReplacerBase
类。这是 Office 文档不同部分的各种 Replacers 的基类。
但是,你作为用户需要一个 TokenizedDocumentBase
具体类的实例。你可以通过使用 TokenizedDocumentProxy
类的 static
工厂方法来获取此实例。你指定要打开的文件名(及其路径)、令牌开始符和令牌结束符(均为字符串)。此代理类再次出于 Add-In 部分说明的原因,在该库的第 3 个版本中引入。示例代码应有所帮助
Dim p As IWordProcessingTokenizedDocument = TokenizedDocumentProxy.getDocumentProcessor_
(Of IWordProcessingTokenizedDocument)( _
SupportedExtensions.docx, _
"ProcessedInvoice.docx", _
"[$", "$]", _
True)
OfficePackage
是用于读取和修改 Office 文档包的辅助类。Formatter
、MetadataProcessor
、Currency
等是令牌替换过程中用于各种目的的其他辅助类。
正则表达式
代码在很大程度上依赖于 .NET 的 System.Text.RegularExpressions
进行令牌替换。你应该小心选择令牌的开始和结束字符。目前,你的令牌分隔符中的字符**不**应该出现在你的内容中。我在附件的示例 docx 文件中使用了“[$”和“$]”作为分隔符。
在实例化合适的 Document
类(通过使用 TokenizedDocumentProxy
中的工厂方法)后,你可以开始调用 replaceToken()
方法(可能在一个循环中,以处理所有令牌),传入你的令牌(包括分隔符)和替换值。在第一次调用此方法时,代码会解析整个内容,查找令牌,并存储一个匹配项的字典,以令牌作为键。之后,在所有后续调用中,它只是查阅此字典进行替换。
或者,你可以创建一个 List
(包含 TokenReplacementInfo
)(项目中的一个辅助类),然后调用 replaceTokens()
,仅传入此 List
一次,它就会执行所有替换。
XmlUtil
是帮助进行 Office XML 特定文本匹配和替换的辅助类。
示例
假设你有以下令牌:[$Date$]。
你可以通过调用...进行替换
doc.content.replaceToken("[$Date$]", DateTime.Today)
...其中 t
是你正在处理的包的具体类的对象(Rahul.Office.MS.Word.WordTokenizedDocument
或 Rahul.Office.OO.Writer.WriterTokenizedDocument
)。
细节
- 令牌替换功能非常灵活。你可以在令牌中指定元数据,以精确控制令牌的替换方式。例如:
- 令牌
[$Date$<metadata><type>date </type><format>dd.MM.yyyy</format></metadata>]
替换后,将遵循你指定的日期格式。
- 另一个例子
[$InvoiceTotalValue$<metadata><type>money </type><format>text</format></metadata>]
会将替换值视为货币值,并自动将其转换为英文(例如,“十万零五百零六”等)。
- 目前支持的最后一种元数据是普通文本。
[$InvoiceTotalValue$<metadata> <type>money</type><format>text</format> <transform>upper</transform></metadata>
转换“
upper
”和“lower
”分别将文本转换为大写或小写。这可以应用于文本以及格式为文本的货币值。
请注意,元数据仅出现在你准备的 Office 文档中。在进行函数调用时,你仍然只需调用
t.replaceToken("[$Date$]", DateTime.Today)
代码会自动确定该令牌在文档中是否附加有任何元数据。元数据标签可以出现在令牌之间的任何位置。
- 令牌
- 同一个令牌可以出现多次。默认行为是用指定为第一次调用
replaceToken()
(以该令牌作为输入)的替换值替换该令牌的所有出现。但是,replaceToken()
有一个重载版本,你可以在其中指定替换令牌的第 i 个出现。因此,以下调用...t.replaceToken("[$Date$]", DateTime.Today, 2)
...将仅替换原始内容中令牌 [$Date$] 的第二次出现。(关于第 i 个出现替换有一些更精细的点,已在附件代码中得到妥善记录。)
同一个令牌可以出现多次,并带有不同的元数据。每次令牌出现都会在考虑其元数据(如果有)的情况下进行替换。
- 发票中的另一个常见情况是发票可能包含多个产品。但在开发时间准备令牌化文档时,你不知道有多少。无需担心。只需在表格中的一行准备一个示例。在任何单元格中留下一个唯一的令牌。然后,在调用
replaceToken()
之前,调用replicateRow()
,传入该唯一令牌以及要复制的行数。 - 所有替换都在内存中完成。完成后,请记住调用
save()
函数,它会替换构造函数中指定的原始 Office 包中的内容。
Using the Code
以下是使用代码的精确步骤
- 准备一个包含令牌的 Word 或 Writer 文档(不是模板)。
- 创建一个控制台应用程序。引用附件的 Rahul.Office 程序集
- 开始替换令牌。下面提供了一个示例代码。
示例
替换令牌所需的全部操作如下
Sub Main()
'Copy the Tokenized file. The new copy would actually be processed.
System.IO.File.Copy("TokenizedInvoice.docx", _
"ProcessedInvoice.docx", True)
'Construct an object to Microsoft Office Word Token Replacement.
Dim p As IWordProcessingTokenizedDocument = _
TokenizedDocumentProxy.getDocumentProcessor_
(Of IWordProcessingTokenizedDocument) _
(SupportedExtensions.docx, _
"ProcessedInvoice.docx", _
"[$", "$]", _
True)
'Construct a list of Token Info's to be replaced.
Dim list As New List(Of TokenReplacementInfo)
list.Add(New TokenReplacementInfo("[$LCNo$]", "11111"))
list.Add(New TokenReplacementInfo("[$LCInvoiceNo$]", "22222"))
'Pay attention here. The Date token has metadata in the Invoice.
'You do not (should not) specify the metadata here.
list.Add(New TokenReplacementInfo("[$LCInvoiceDate$]", DateTime.Today))
'Notice the Tokenized document has a token called [$ReplicateRow$]. That
'row has Product information, and suppose I have 2 products in this
'Invoice. True indicates to remove the Row Replication token after the
'replication process.
p.body.replicateRow("[$ReplicateRow$]", True, 2)
'I am substituting 2 products here. So, using 0 and 1 to indicate the
'occurrence of the token to be replaced, because the replicate rows would
'have identical Tokens.
'First good goes here. Last parameter is 0 for replacement of first
'occurrences of all tokens specified. (-1 would have replaced all),
'which also is the default behaviour without the third parameter.
list.Add(New TokenReplacementInfo("[$LCGoodOrderName$]", _
"My neighbour's car", 0))
list.Add(New TokenReplacementInfo("[$LCGoodOrderBrand$]", _
"Bentley", 0))
list.Add(New TokenReplacementInfo("[$LCGoodOrderSpecification$]", _
"Black, with leather upholstery", 0))
'Second good goes here Last parameter is 1 for replacement of second
'occurrences of all tokens specified.
'Omitted for brevity
'Check the Tokenized Invoice. There are multiple occurrences of
'[$LCInvoiceTotalValue$] with different metadata.
'Just one call replaces all, honoring each one's metadata individually.
list.Add(New TokenReplacementInfo("[$LCInvoiceTotalValue$]", 10010200))
list.Add(New TokenReplacementInfo("[$LCDeliveryType$]", "CNF"))
'Replace all in one go.
p.body.replaceTokens(list)
'New Feature: Html Replacement
If (TypeOf (p) Is IWordProcessingTokenizedDocumentExtension) Then
Dim pext As IWordProcessingTokenizedDocumentExtension = _
CType(p, IWordProcessingTokenizedDocumentExtension)
pext.replaceTokenWithHtml("[$HtmlToken$]", _
"Html text that replaced a token")
End If
'Don't forget the save the document.
p.save()
End Sub
与 Open XML SDK 或其他类似选项相比的优势
- 使用这些 SDK 需要你对 Open XML 架构有深入的了解。
- 使用它们时,你会局限于一个特定的产品。
- 使用它们需要在目标机器上进行安装。在这里,只需将 Rahul.Office 程序集放入 bin 文件夹,或将代码文件复制到你的项目中。
我无意贬低这些 SDK。它们非常强大。但我认为它们对于常规开发来说过于强大,除非你对架构有深入的了解。
第 3 版 - 引入了插件架构
该库的第二版引入了对页眉和页脚部分令牌替换的支持(参见此评论)(但请下载本文附带的代码,而不是评论中的代码,因为该代码已过时且文件已从 Rapidshare 中删除)。
发布第二版后不久,我遇到了一个有趣的情景,客户希望能够用动态生成的 HTML 替换令牌。正如任何人所能想象的那样,这是一个相当复杂的情景,因为你根本不能用 HTML 标记替换令牌。这会导致 Office 文档损坏,因为 HTML 与 Office 标记不兼容。
我需要提供此功能。但无法提供从 HTML 到 Office 标记的转换。这会太复杂,并且超出了该库的范围。通过一些 Google 搜索,我发现了 VSTO 对此类情景的支持。但是,请记住,VSTO 是以 Microsoft Office 为中心的库集合,用于使 .NET 代码能够处理 Microsoft Office 文档。更重要的是,VSTO 需要在机器上安装有效的 Microsoft Office 副本。
因此,我重构了这个库以实现插件架构。核心的令牌替换支持以及上述所有功能都来自核心的 Rahul.Office.dll 程序集。但是,该程序集本身会尝试加载 Rahul.Office.MS.dll 或 Rahul.Office.OO.dll 程序集。这些程序集可以为相应的 Office 产品提供扩展的令牌替换支持。但是,如果找不到,核心程序集将回退到自身来提供其令牌替换功能。
为了支持这种重构,TokenizedDocumentBase
被重构为一组接口。ITokenizedDocument
接口提供了所有 Tokenized Document 都应实现的メソッド。IWordProcessingTokenizedDocument
接口包含所有字处理程序(Microsoft Word、OO Writer 等)应提供的メソッド。这两个接口都由核心 Rahul.Office.dll 程序集中的类完全实现。
但是,另一个接口 IWordProcessingTokenizedDocumentExtension
提供了插件程序集可以选择实现的扩展メソッド。目前,它提供了一个名为 replaceTokenWithHtml
的メソッド,该メソッド由 Rahul.Office.MS.dll 程序集为 Microsoft Word Tokenized 文档实现。
为了支持此架构,创建了一个特殊的 TokenizedDocumentProxy
类,其中包含 static
工厂方法,例如
getDocumentProcessor(ByVal extension As SupportedExtensions, _
ByVal documentPath As String, ByVal tokenStart As String, _
ByVal tokenEnd As String, ByVal lookForDedicatedAssemblies As Boolean) _
As ITokenizedDocument
现在,如果你将 true
作为最后一个参数传递,它将在回退到自身之前查找插件程序集,以防找不到。如果你将 false
作为最后一个参数传递,则不会查找插件程序集。我强烈建议传递 false
,除非你需要插件程序集所需的附加功能。一些注意事项
- 如果你不需要扩展功能,请传递
false
,因为插件程序集是通过反射动态加载的,这可能会影响性能。 - 插件程序集为特定 Office 产品提供功能。因此,它们可以提供非标准实现,而其他 Office 产品则无法使用。
- 插件程序集可能有其自身的先决条件。例如,如果你选择下载带插件的源代码,你将获得 Rahul.Office.MS.dll,它提供 Microsoft Office 特定扩展。它通过 VSTO 提供这些功能,VSTO 要求在匹配的机器上安装 Microsoft Office 才能使用它。
因此,如果你不需要附加的插件功能,你应该下载不带插件的源代码。目前插件提供的唯一扩展功能是将令牌替换为 HTML 格式的字符串。
另外,请注意 VSTO 大量使用 Interop,因此速度相对较慢。 - 如果你正在使用插件程序集,请记住它们是通过反射加载的,并且应与核心 Rahul.Office.dll 程序集位于同一目录中。
仍需完成的工作
我需要快速地为客户交付功能,并迅速组装了原始代码。从那时起,我对它进行了一些增强,并在文章中进行了更新。
我使用了大量的正则表达式,它们可能可以进行调整以提高性能(尽管我已经能够几乎瞬时地对大型文档进行令牌替换)。还有很多功能或元数据扩展可以添加。至少希望支持 Excel 和 Calc。更多的替换选项,列表将永无止境。
我会尽量抽出时间来增强它。但目前,它应该在大多数情况下满足许多需求。
也可在我的博客上找到
本文的源代码也可在我的博客 Office 文档中的令牌替换 上找到。本文将与源代码一起始终在 CodeProject 上保持更新。但是,我注意到在我提交更新版本后,文章在 CodeProject 上更新需要一些时间(最后一个版本花了超过 2 周时间才更新)。所以,你可以从我的博客文章中下载最新代码。同时,更新后的代码也始终可在 CodeProject 上找到。
历史
- 2008 年 12 月 22 日:初次发布
- 2009 年 5 月 22 日:文章更新
- 2009 年 11 月 11 日:文章更新