使用OpenXml和C#复制Word文档

12
我正在使用Word和OpenXml在C# ASP.NET Web应用程序中提供邮件合并功能:
1)上传一个带有预定义字符串的文档。
2)使用OpenXML SDK 2.0打开Word文档,将主文档部分作为字符串获取并使用正则表达式进行替换。
3)然后,使用OpenXML创建一个新文档,添加一个新的主文档部分,并将替换结果的字符串插入到此主文档部分中。
但是,所有格式/样式等在新文档中都会丢失。
我猜想我可以单独复制和添加样式、定义、注释等部分来模拟原始文档。
但是,是否有一种方法可以使用Open XML复制文档,使我能够对新副本执行替换操作?
谢谢。

1
为什么不使用File.Copy(docName, newName);呢? - Kiwimanshare
请看下面我的答案,了解自2014/15年以来使用Open XML SDK的选项更新。 - Thomas Barnekow
6个回答

16

这段代码应该将现有文档中的所有部分复制到新文档中。

using (var mainDoc = WordprocessingDocument.Open(@"c:\sourcedoc.docx", false))
using (var resultDoc = WordprocessingDocument.Create(@"c:\newdoc.docx",
  WordprocessingDocumentType.Document))
{
  // copy parts from source document to new document
  foreach (var part in mainDoc.Parts)
    resultDoc.AddPart(part.OpenXmlPart, part.RelationshipId);
  // perform replacements in resultDoc.MainDocumentPart
  // ...
}

1
我已经在那个MemoryStream的问题上纠结了几个小时,感觉自己一直撞墙。这个方法非常好用且更加简洁。非常感谢! - lukiffer
有没有一种方法可以实现类似的事情,唯一的区别是需要将mainDoc中的内容附加到现有文档的末尾? - Soul Slayer
是的,虽然更加困难,因为需要合并两个文档部分的大量数据。值得庆幸的是,Eric White已经构建了一组OpenXML PowerTools来处理这个看似艰巨的任务。特别是要看一下DocumentBuilder,我过去用它来将一个文档附加到另一个文档中。效果非常好! - bernhof
但是为什么不直接将sourcedoc.docx复制到文件中的newdoc.docx,然后更新newdoc.docx呢? - Steven.Xi

8

我赞同使用内容控件建议。在文档中标记您想要执行替换的区域,这是迄今为止最简单的方法。

至于复制文档(并保留整个文档内容、样式等),这相对容易:

string documentURL = "full URL to your document";
byte[] docAsArray = File.ReadAllBytes(documentURL);

using (MemoryStream stream = new MemoryStream)
{
    stream.Write(docAsArray, 0, docAsArray.Length);    // THIS performs doc copy
    using (WordprocessingDocument doc = WordprocessingDocument.Open(stream, true))
    {
        // perform content control substitution here, making sure to call .Save()
        // on any documents Part's changed.
    }
    File.WriteAllBytes("full URL of your new doc to save, including .docx", stream.ToArray());
}

使用LINQ找到内容控件其实很简单。下面的例子可以找到所有类型为SdtRun的简单文本内容控件:

using (WordprocessingDocument doc = WordprocessingDocument.Open(stream, true))
{                    
    var mainDocument = doc.MainDocumentPart.Document;
    var contentControls = from sdt in mainDocument.Descendants<SdtRun>() select sdt;

    foreach (var cc in contentControls)
    {
        // drill down through the containment hierarchy to get to 
        // the contained <Text> object
        cc.SdtContentRun.GetFirstChild<Run>().GetFirstChild<Text>().Text = "my replacement string";
    }
}
<Run><Text>元素可能不存在,但创建它们很简单:
cc.SdtContentRun.Append(new Run(new Text("my replacement string")));

希望这能对某些人有所帮助。 :D

非常感谢!我因为每次加载模板并进行更改时,保存为新文件名后两个文件都会被更新而感到非常烦恼!感谢您分享使用MemoryStream来保存模板副本的方法,从而防止实际模板文件被损坏:D :D - vahanpwns

4

在添加了许多有用功能到Open XML SDK之前,最初的问题被提出。现在,如果你已经打开了一个WordprocessingDocument,你只需克隆原始文档并对该副本进行任何转换即可。

// Say you have done this somewhere before you want to duplicate your document.
using WordprocessingDocument originalDoc = WordprocessingDocument.Open("original.docx", false);

// Then this is how you can clone the opened WordprocessingDocument.
using var newDoc = (WordprocessingDocument) originalDoc.Clone("copy.docx", true);

// Perform whatever transformation you want to do.
PerformTransformation(newDoc);

你也可以在 Stream 或者 Package 上进行克隆。总体而言,你有以下几个选项:

OpenXmlPackage Clone()

OpenXmlPackage Clone(Stream stream)
OpenXmlPackage Clone(Stream stream, bool isEditable)
OpenXmlPackage Clone(Stream stream, bool isEditable, OpenSettings openSettings)

OpenXmlPackage Clone(string path)
OpenXmlPackage Clone(string path, bool isEditable)
OpenXmlPackage Clone(string path, bool isEditable, OpenSettings openSettings)

OpenXmlPackage Clone(Package package)
OpenXmlPackage Clone(Package package, OpenSettings openSettings)

请查看Open XML SDK文档以了解这些方法的详细信息。
话虽如此,如果您尚未打开WordprocessingDocument,则至少有更快的方法来复制或克隆文档。我在最有效地克隆Office Open XML文档的答案中演示了这一点。

2
作为上述内容的补充,更有用的是找到已经标记了内容控件(使用GUI这个词)。我最近编写了一些软件,用于填充带有附加标签的文档模板中的内容控件。找到它们只是上述LINQ查询的扩展:
var mainDocument = doc.MainDocumentPart.Document;
var taggedContentControls = from sdt in mainDocument.Descendants<SdtElement>()
                            let sdtPr = sdt.GetFirstChild<SdtProperties>()
                            let tag = (sdtPr == null ? null : sdtPr.GetFirstChild<Tag>())
                            where (tag != null)
                            select new
                            {
                                SdtElem = sdt,
                                TagName = tag.GetAttribute("val", W).Value
                            };   

我从其他地方得到了这段代码,但目前无法记起它来自哪里;全部功劳归于他们。

该查询只创建一个匿名类型的IEnumerable,其中包含内容控件及其关联标签作为属性。非常方便!


2
我已经做过一些非常相似的事情,但是我使用的不是文本替换字符串,而是使用Word内容控件。我在以下博客文章中记录了一些细节:SharePoint和Open Xml。这种技术并不特定于SharePoint。你可以在纯ASP.NET或其他应用程序中重用该模式。
另外,我强烈建议您查看Eric White的博客,以获取有关Open Xml的提示、技巧和技术。具体来说,请查看内存中操作Open Xml帖子Word内容控件帖子。我认为这些对您长期来说会更有帮助。
希望这可以帮到您。

0

当您将扩展名更改为zip并打开openxml文档时,您会发现单词子文件夹包含一个_rels文件夹,其中列出了所有关系。这些关系指向您提到的部分(样式...)。实际上,您需要这些部分,因为它们包含格式定义。因此,不复制它们将导致新文档使用normal.dot文件中定义的格式,而不是原始文档中定义的格式。所以我认为您必须复制它们。


不是真正回答问题。在回答之前阅读相关资料。 - Anonymous Type

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接