HTML 作为 DOCX 文件的来源
从应用程序或网站表单提交中获取 HTML 或纯文本输入,并生成有效的 WordML (.docx) 文件。
背景
我时常需要将用户从网站表单提交的文本保存到数据库之外。虽然存储文本文件始终是一个选项,但有时需要更多一点的优雅。我需要能够将富内容编辑器中的 HTML 保存为 Word 文档,有点像我们过去使用 Word 2007 以前的版本所能做到的(将几个元标签放入 HTML 文件中,Word 就被愚弄了,以为它是一个 Word HTML 文件)。使用新的 DOCX 格式,这根本不可能。这次搜索和随后的修补所产生的结果证明非常有用(至少对我来说),可以根据网站表单提交或数据库字段内容生成即时 Word 文档,这些文档需要以可用格式提供给访问者。
一个警告
虽然这是一种创建 *docx* 文件的快速简单方法,但绝不应将其视为除此之外的任何东西。您绝不应读取 *docx* 文件的内容并将其放入网站上的 textarea 标签(或应用程序中的 textbox)进行编辑,或更改任何预先存在的 HTML 的内容。这不仅违反了标准,而且以这种方式做事只是不良的编码实践。对现有 docx 文件的任何编辑都应由从一开始就设计用于此目的的程序进行,例如 Microsoft Word 2007。
关于 DOCX 格式的一些信息
DOCX 文件通常是 Microsoft Word 文件。该格式是 Microsoft Word 2007 的新格式。它是 XML 文件(文档)和 ZIP 压缩的组合,用于减小大小。如果您创建一个包含完全相同内容的文档,并以旧格式和新格式保存,您会看到文件大小的巨大差异。旧版本的 Word 需要安装补丁才能打开 *docx* 文件。此外,此格式与 OpenDocument 标准不同。
关于架构的说明
您可能会注意到 *docx* 文件引用了托管在 schemas.openxmlformats.org 上的架构。这很常见,所以不用担心。如果您可以在自己的服务器上托管这些架构,那么除了您之外,对任何人都将是一个安全风险,因为他们不知道您是谁。将架构放在这个中心位置,您就拥有了一个安全、已知的位置,您的客户可以信任。
Using the Code
设置
我们需要正确设置项目,为此,我们需要在 using
块中包含 System.IO.Packaging
。
using System.IO.Packaging;
没有它,整个项目将无法工作。此外,您还需要为 WindowsBase
创建引用。如果 WindowsBase
没有出现在您的列表中,您通常可以在 *c:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll* 找到它。
创建文件
我们需要创建 *.docx* 文件的基础。这在 SaveDOCX
函数中完成。
private static void SaveDOCX(string fileName, string BodyText, bool IncludeHTML)
{
string WordprocessingML =
"http://schemas.openxmlformats.org/wordprocessingml/2006/main";
XmlDocument xmlStartPart = new XmlDocument();
XmlElement tagDocument = xmlStartPart.CreateElement("w:document", WordprocessingML);
xmlStartPart.AppendChild(tagDocument);
XmlElement tagBody = xmlStartPart.CreateElement("w:body", WordprocessingML);
tagDocument.AppendChild(tagBody);
这里最重要的是正确的命名空间架构。我在研究这个过程中发现了很多使用“*http://schemas.openxmlformats.org/wordprocessingml/2006/3/main*”的代码。这个架构是用于该格式的 Beta 版本的。最终版本的架构,也就是被引用的架构,才是必须使用的。通过将其包含在主体元素的创建中,我们确保最终得到正确类型的文件。
这段代码的其余部分处理构建最终文档的基础。特别值得注意的是 WordprocessingML 文档(“起始部分”)的标签嵌套。
w:document contains ...
w:body
这种嵌套对于创建有效文件至关重要。它定义了文件的结构。如果顺序错误,文件将无效,并且可能无法打开或读取。XML 文档由一个或多个 XML 元素组成。什么是元素?元素是介于一对“<”和“>”字符之间的任何内容。在这种情况下,它表示文档包含一个主体元素。元素按照嵌套的顺序创建并相互添加。元素可以包含其他元素,也可以包含数据。这就是 XML 文档的构建方式。
处理 HTML
首先,让我们处理 HTML。由于 HTML 是一个包含在文档中的预格式化文本块(又名元素),我们应该能够对其进行处理。通过利用 XmlElement
"altChunk
",我们基本上可以将有效的 HTML 放入一个文件中,然后该文件在尚未创建的 *.docx* 文件中被引用。我是这样设置 altChunk
标签和所需引用的。
string relationshipNamespace =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships";
XmlElement tagAltChunk = xmlStartPart.CreateElement("w:altChunk", WordprocessingML);
XmlAttribute RelID = tagAltChunk.Attributes.Append
(xmlStartPart.CreateAttribute("r:id", relationshipNamespace));
RelID.Value = "rId2";
tagBody.AppendChild(tagAltChunk);
包含关系很重要,关系 ID 也是如此。没有它们,文件将无法工作。此文件中的关系将告诉 Word 每个元素应具有的处理类型。我们在创建 XML 元素('altChunk
' 元素)时再次使用了“WordprocessingML”,这将是整个项目的标准。我们还为该元素添加了一个属性。属性出现在元素的结构中,并且总是看起来像“something='something'”。属性的值出现在引号内,在这种情况下,我们已分配了一个值“rId2
”。如果您不打算分配值,在大多数情况下,您可以跳过添加属性。此代码块中的最后一行通过 AppendChild
方法将新元素附加到主体。这实现了文档所需的嵌套。
有效 HTML 的重要性
正如我们所看到的,XML 文档必须包含正确的格式和元素嵌套才能有效。无效的 XML 文档可能无法打开,也可能无法以可访问的方式包含数据。HTML 也是如此。如果它没有正确格式化和嵌套,页面就不会按预期显示。如果您希望您的 DOCX 文件能够打开和使用,您必须从有效的 HTML 开始。目前可用于浏览器和应用程序的大多数富内容编辑器都生成有效的 HTML,因此无需担心。如果您是手动生成 HTML,我强烈建议您通过 W3C HTML 验证器对其进行验证。
处理纯文本
纯文本的处理方式与 HTML 大致相同,但它在文档中嵌套得更深,并且不需要 altChunk
元素。不过,我们仍然需要创建元素并将它们附加到其他元素。
XmlElement tagParagraph = xmlStartPart.CreateElement("w:p", WordprocessingML);
tagBody.AppendChild(tagParagraph);
XmlElement tagRun = xmlStartPart.CreateElement("w:r", WordprocessingML);
tagParagraph.AppendChild(tagRun);
XmlElement tagText = xmlStartPart.CreateElement("w:t", WordprocessingML);
tagRun.AppendChild(tagText);
XmlNode nodeText = xmlStartPart.CreateNode(XmlNodeType.Text, "w:t", WordprocessingML);
nodeText.Value = BodyText;
tagText.AppendChild(nodeText);
我们可以看到 WordprocessingML 文档(“起始部分”)的更多标签嵌套
w:document contains ...
w:body, which contains ...
w:p (paragraph), which contains ...
w:r (run), which contains ...
w:t (text), which contains…
w:t (text)
在此嵌套中,我们创建了一个段落元素(w:p
)并将其附加到正文元素,一个运行元素(w:r
)并将其附加到段落元素,以及一个附加到 run
元素的文本元素。这实现了我们元素的嵌套并构建了结构。我们还引入了 XmlNode
。这里的区别在于 XmlNode
表示 XML 文件中的单个节点(或元素),而 XMLDocument
将整个内容扩展以表示一个文件。XMLElement
也类似,但再次提供了比 XMLNode
更多功能。
干净文本的重要性
在您将纯文本放入其中之前,您必须绝对确定它不包含任何 HTML 标记。如果包含,您将得到一个文件,其中 HTML 标记清晰可见,并且很可能让用户感到困惑。通常的规则是:永远不要相信来自用户的数据!无论您是从网站上的 textarea 标签还是从应用程序获取输入,您都应该考虑剥离所有 HTML 标签。我更喜欢在我将文本发送到此工具之前完成此操作,在那里我可以随意将其发送回用户,但如果您愿意,您可以将其构建到您自己的版本中。
创建文件
这是一个两步过程。我们将从创建主文档开始。
Uri docuri = new Uri("/word/document.xml", UriKind.Relative);
PackagePart docpartDocumentXML = pkgOutputDoc.CreatePart(docuri,
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml");
StreamWriter streamStartPart = new StreamWriter(docpartDocumentXML.GetStream
(FileMode.Create, FileAccess.Write));
xmlStartPart.Save(streamStartPart);
streamStartPart.Close();
pkgOutputDoc.Flush();
pkgOutputDoc.CreateRelationship(docuri, TargetMode.Internal,
"http://schemas.openxmlformats.org/officeDocument/" +
"2006/relationships/officeDocument",
"rId1");
pkgOutputDoc.Flush();
我们首先通过创建一个名为 *document.xml* 的文件来创建主文档,在主 XML 文件中为其创建一个地址,然后将该文件放置在特定文件夹中。这个文件是空的,但这并不是我们关心的。我们只需要它存在。这个创建过程的一部分涉及使用 StreamWriter
来帮助创建、捕获并将文档发送到主 XML 文件。一旦完成,我们就可以关闭 StreamWriter
并通过 Flush()
方法将输出发送到我们的主 XML 文件。现在我们已经完成了这些,我们还需要另一个关系。这个关系决定了打开程序将如何处理 *document.xml* 文件。添加关系后,我们可以再次使用 Flush()
方法将输出发送到主 XML 文件。
Flush()
方法实际上相当重要。随着程序运行并接收更多数据,它会占用越来越多的内存。通过调用 Flush()
方法,我们告诉程序将其在内存中构建的内容提交到文件,从而释放内存供此程序或另一个程序重用。如果您不调用 Flush()
,您的程序将继续保留此信息,然后随着我们继续构建文件而添加更多信息。就其本身而言,像这样创建的小文件不是问题。但是,如果您将它放在一个非常活跃的网站上,该网站允许创建非常大的文档,结果是服务器随着创建的文件增多而逐渐变慢,从而导致网站处理和加载页面所需的时间越来越长。所有这些都会累积起来,所以我们尽可能地清除。
完成所有这些之后,我们将通过告诉 XML 文档从何处获取数据、关闭 SaveDOCX
函数并将其发送出去来将所有内容连接起来。
Uri uriBase = new Uri("/word/document.xml", UriKind.Relative);
PackagePart partDocumentXML = pkgOutputDoc.GetPart(uriBase);
Uri uri = new Uri("/word/websiteinput.html", UriKind.Relative);
string html = string.Concat("<!DOCTYPE HTML PUBLIC \
"-//W3C//DTD HTML 4.0 Transitional//EN\"><html>" +
"<head><title></title></head><body>",
BodyText, "</body></html>");
byte[] Origem = Encoding.UTF8.GetBytes(html);
PackagePart altChunkpart = pkgOutputDoc.CreatePart(uri, "text/html");
using (Stream targetStream = altChunkpart.GetStream())
{
targetStream.Write(Origem, 0, Origem.Length);
}
Uri relativeAltUri = PackUriHelper.GetRelativeUri(uriBase, uri);
partDocumentXML.CreateRelationship(relativeAltUri, TargetMode.Internal,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk",
"rId2");
pkgOutputDoc.Close();
}
我们首先引用刚才创建的 *document.xml* 文件,然后继续创建实际的 HTML 文件来保存我们的内容。无论我们处理纯文本还是 HTML,我们都使用同一个文件,因为我们已将此文件定义为文档中文本的所在地,并且 HTML 能够很好地处理纯文本。完成此操作后,我们现在必须将此文件转换为字节数组,然后可以写入该文件。UTF8 编码是公认的标准,因此使用它以后不会给我们带来任何问题。在创建包的最后一部分(保存实际数据的部分)之后,我们将其一个字节一个字节地流式传输到文件中。此处使用 using
块为我们提供了内置的流清理功能。这意味着当代码的执行超出 using
块时,会对 Stream
的 Close()
方法进行静默调用,然后 Stream
将被置空并清除内存。
我们的最后一步是创建此文件中的最终关系,告诉程序在哪里找到用于 altChunk
处理的信息,然后关闭包,这将关闭文件并将其保存到您指示的任何位置。
用法
将项目编译成 DLL 并将其放在您网站的 *Bin* 文件夹或您的项目中,并进行必要的引用,然后按如下方式使用它
NoInkSoftware.HTMLtoDOCX NewFile = new NoInkSoftware.HTMLtoDOCX();
NewFile.CreateFileFromHTML(MyHTMLSource, MyDestination);
或者
NoInkSoftware.HTMLtoDOCX NewFile = new NoInkSoftware.HTMLtoDOCX();
NewFile.CreateFileFromText(MyTextSource, MyDestination);
功劳归于应得的人
我需要把功劳归于应得之人。我第一次寻找解决方案的搜索把我带到了 Doug Mahugh 的一些工作,这里(http://openxmldeveloper.org/archive/2006/07/20/388.aspx)和这里(http://blogs.msdn.com/dmahugh/archive/2006/06/27/649007.aspx)。有了这些信息,我接下来开始在 CodeProject 上搜索类似的东西,希望能满足我的需求。在出版时,Paulo Vaz 的这篇文章(https://codeproject.org.cn/KB/aspnet/HTMLtoWordML.aspx)是唯一一个部分解决了我想做的事情的。然而,我不想在我的网站上放置一个模板,因此在标准中进一步搜索(在主页上找到了一些可下载的文件,网址是 http://openxmldeveloper.org/)使我能够直接将 altChunk
放入文件中。
关注点
工作示例
如果您想查看此代码的实际运行情况,可以在此处找到一个工作示例。
历史
这是版本 1。