65.9K
CodeProject 正在变化。 阅读更多。
Home

HTML 作为 DOCX 文件的来源

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (27投票s)

2010 年 7 月 5 日

CPOL

10分钟阅读

viewsIcon

128746

downloadIcon

3938

从应用程序或网站表单提交中获取 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 块时,会对 StreamClose() 方法进行静默调用,然后 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。

© . All rights reserved.