在 C# 中从 MS Word 文档中提取文本

从 Word 文档中提取文本通常在不同的场景中执行。例如,分析文本,提取文档的特定部分并将它们组合成单个文档,等等。在本文中,您将学习如何使用 C# 以编程方式从 Word 文档中提取文本。此外,我们将介绍如何动态提取段落、表格等特定元素之间的内容。

从 Word 文档中提取文本的 C# 库

Aspose.Words for .NET 是一个功能强大的库,可让您从头开始创建 MS Word 文档。此外,它可以让您操作现有的 Word 文档进行加密、转换、文本提取等。我们将使用这个库从 Word DOCX 或 DOC 文档中提取文本。您可以 下载 API 的 DLL 或使用包管理器控制台直接从 NuGet 安装它。

PM> Install-Package Aspose.Words

使用 C# 在 Word 文档中提取文本

MS Word 文档由各种元素组成,包括段落、表格、图像等。因此,文本提取的要求可能因一种情况而异。例如,您可能需要在段落、书签、评论等之间提取文本。

Word 文档中的每种类型的元素都表示为一个节点。因此,要处理文档,您将不得不使用节点。那么让我们开始看看如何在不同的场景下从 Word 文档中提取文本。

在 C# 中从 Word 文档中提取文本

在本节中,我们将为 Word 文档实现一个 C# 文本提取器,文本提取的工作流程如下:

  • 首先,我们将定义要包含在文本提取过程中的节点。
  • 然后,我们将提取指定节点之间的内容(包括或不包括开始和结束节点)。
  • 最后,我们将使用提取节点的克隆,例如创建一个包含提取内容的新 Word 文档。

现在让我们编写一个名为 ExtractContent 的方法,我们将向该方法传递节点和一些其他参数来执行文本提取。此方法将解析文档并克隆节点。以下是我们将传递给此方法的参数。

  1. StartNode 和 EndNode 分别作为内容提取的起点和终点。这些可以是块级(Paragraph、Table)或内联级(例如 Run、FieldStart、BookmarkStart 等)节点。
    1. 要传递一个字段,您应该传递相应的 FieldStart 对象。
    2. 要传递书签,应传递 BookmarkStart 和 BookmarkEnd 节点。
    3. 对于评论,应使用 CommentRangeStart 和 CommentRangeEnd 节点。
  2. IsInclusive 定义标记是否包含在提取中。如果此选项设置为 false 并且传递相同的节点或连续节点,则将返回一个空列表。

以下是提取传递的节点之间的内容的 ExtractContent 方法的完整实现。

public static ArrayList ExtractContent(Node startNode, Node endNode, bool isInclusive)
{
    // 首先检查传递给该方法的节点是否有效使用。
    VerifyParameterNodes(startNode, endNode);

    // 创建一个列表来存储提取的节点。
    ArrayList nodes = new ArrayList();

    // 保留传递给此方法的原始节点的记录,以便我们可以在需要时拆分标记节点。
    Node originalStartNode = startNode;
    Node originalEndNode = endNode;

    // 基于块级节点(段落和表格)提取内容。遍历父节点以找到它们。
    // 我们将根据标记节点是否内联来拆分第一个和最后一个节点的内容
    while (startNode.ParentNode.NodeType != NodeType.Body)
        startNode = startNode.ParentNode;

    while (endNode.ParentNode.NodeType != NodeType.Body)
        endNode = endNode.ParentNode;

    bool isExtracting = true;
    bool isStartingNode = true;
    bool isEndingNode = false;
    // 我们从文档中提取的当前节点。
    Node currNode = startNode;

    // 开始提取内容。处理所有块级节点,并在需要时专门拆分第一个和最后一个节点,以便保留段落格式。
    // 方法比常规提取器稍微复杂一点,因为我们需要考虑使用内联节点、字段、书签等进行提取,以使其真正有用。
    while (isExtracting)
    {
        // 克隆当前节点及其子节点以获取副本。
        Node cloneNode = currNode.Clone(true);
        isEndingNode = currNode.Equals(endNode);

        if ((isStartingNode || isEndingNode) && cloneNode.IsComposite)
        {
            // 我们需要分别处理每个标记,因此将其传递给单独的方法。
            if (isStartingNode)
            {
                ProcessMarker((CompositeNode)cloneNode, nodes, originalStartNode, isInclusive, isStartingNode, isEndingNode);
                isStartingNode = false;
            }

            // 条件需要分开,因为块级开始和结束标记可能是同一个节点。
            if (isEndingNode)
            {
                ProcessMarker((CompositeNode)cloneNode, nodes, originalEndNode, isInclusive, isStartingNode, isEndingNode);
                isExtracting = false;
            }
        }
        else
            // 节点不是开始或结束标记,只需将副本添加到列表中。
            nodes.Add(cloneNode);

        // 移动到下一个节点并提取它。如果下一个节点为空,则意味着其余内容位于不同的部分中。
        if (currNode.NextSibling == null && isExtracting)
        {
            // 移至下一部分。
            Section nextSection = (Section)currNode.GetAncestor(NodeType.Section).NextSibling;
            currNode = nextSection.Body.FirstChild;
        }
        else
        {
            // 移动到正文中的下一个节点。
            currNode = currNode.NextSibling;
        }
    }

    // 返回节点标记之间的节点。
    return nodes;
}

ExtractContent 方法还需要一些辅助方法来完成文本提取操作,如下所示。

public static List<Paragraph> ParagraphsByStyleName(Document doc, string styleName)
{
    // 创建一个数组来收集指定样式的段落。
    List<Paragraph> paragraphsWithStyle = new List<Paragraph>();

    NodeCollection paragraphs = doc.GetChildNodes(NodeType.Paragraph, true);

    // 浏览所有段落以找到具有指定样式的段落。
    foreach (Paragraph paragraph in paragraphs)
    {
        if (paragraph.ParagraphFormat.Style.Name == styleName)
            paragraphsWithStyle.Add(paragraph);
    }

    return paragraphsWithStyle;
}
private static void VerifyParameterNodes(Node startNode, Node endNode)
{
    // 这些检查的顺序很重要。
    if (startNode == null)
        throw new ArgumentException("Start node cannot be null");
    if (endNode == null)
        throw new ArgumentException("End node cannot be null");

    if (!startNode.Document.Equals(endNode.Document))
        throw new ArgumentException("Start node and end node must belong to the same document");

    if (startNode.GetAncestor(NodeType.Body) == null || endNode.GetAncestor(NodeType.Body) == null)
        throw new ArgumentException("Start node and end node must be a child or descendant of a body");

    // 检查结束节点是否在 DOM 树中的开始节点之后
    // 首先检查它们是否在不同的部分,然后如果它们不检查它们在它们所在的同一部分的正文中的位置。
    Section startSection = (Section)startNode.GetAncestor(NodeType.Section);
    Section endSection = (Section)endNode.GetAncestor(NodeType.Section);

    int startIndex = startSection.ParentNode.IndexOf(startSection);
    int endIndex = endSection.ParentNode.IndexOf(endSection);

    if (startIndex == endIndex)
    {
        if (startSection.Body.IndexOf(startNode) > endSection.Body.IndexOf(endNode))
            throw new ArgumentException("The end node must be after the start node in the body");
    }
    else if (startIndex > endIndex)
        throw new ArgumentException("The section of end node must be after the section start node");
}
private static bool IsInline(Node node)
{
    // 测试该节点是否是段落或表格节点的后代,也不是段落或表格,注释类中的段落是段落的后代是可能的。
    return ((node.GetAncestor(NodeType.Paragraph) != null || node.GetAncestor(NodeType.Table) != null) && !(node.NodeType == NodeType.Paragraph || node.NodeType == NodeType.Table));
}
private static void ProcessMarker(CompositeNode cloneNode, ArrayList nodes, Node node, bool isInclusive, bool isStartMarker, bool isEndMarker)
{
    // 如果我们正在处理块级节点,只需查看是否应该包含它并将其添加到列表中。
    if (!IsInline(node))
    {
        // 如果标记是同一个节点,则不要添加节点两次
        if (!(isStartMarker && isEndMarker))
        {
            if (isInclusive)
                nodes.Add(cloneNode);
        }
        return;
    }

    // 如果标记是 FieldStart 节点,请检查是否包含它。
    // 为简单起见,我们假设 FieldStart 和 FieldEnd 出现在同一段落中。
    if (node.NodeType == NodeType.FieldStart)
    {
        // 如果标记是开始节点并且不包括在内,则跳到字段的末尾。
        // 如果标记是结束节点并且要包含它,则移动到结束字段,这样该字段就不会被删除。
        if ((isStartMarker && !isInclusive) || (!isStartMarker && isInclusive))
        {
            while (node.NextSibling != null && node.NodeType != NodeType.FieldEnd)
                node = node.NextSibling;

        }
    }

    // 如果任一标记是评论的一部分,那么要包含评论本身,我们需要将指针向前移动到评论
    // 在 CommentRangeEnd 节点之后找到的节点。
    if (node.NodeType == NodeType.CommentRangeEnd)
    {
        while (node.NextSibling != null && node.NodeType != NodeType.Comment)
            node = node.NextSibling;

    }

    // 通过索引在我们克隆的节点中找到对应的节点并返回。
    // 如果开始节点和结束节点相同,则可能已经删除了一些子节点。减去
    // 差异以获得正确的索引。
    int indexDiff = node.ParentNode.ChildNodes.Count - cloneNode.ChildNodes.Count;

    // 子节点计数相同。
    if (indexDiff == 0)
        node = cloneNode.ChildNodes[node.ParentNode.IndexOf(node)];
    else
        node = cloneNode.ChildNodes[node.ParentNode.IndexOf(node) - indexDiff];

    // 从标记中删除节点。
    bool isSkip = false;
    bool isProcessing = true;
    bool isRemoving = isStartMarker;
    Node nextNode = cloneNode.FirstChild;

    while (isProcessing && nextNode != null)
    {
        Node currentNode = nextNode;
        isSkip = false;

        if (currentNode.Equals(node))
        {
            if (isStartMarker)
            {
                isProcessing = false;
                if (isInclusive)
                    isRemoving = false;
            }
            else
            {
                isRemoving = true;
                if (isInclusive)
                    isSkip = true;
            }
        }

        nextNode = nextNode.NextSibling;
        if (isRemoving && !isSkip)
            currentNode.Remove();
    }

    // 处理后复合节点可能会变为空。如果它没有包括它。
    if (!(isStartMarker && isEndMarker))
    {
        if (cloneNode.HasChildNodes)
            nodes.Add(cloneNode);
    }

}
public static Document GenerateDocument(Document srcDoc, ArrayList nodes)
{
    // 创建一个空白文档。
    Document dstDoc = new Document();
    // 从空文档中删除第一段。
    dstDoc.FirstSection.Body.RemoveAllChildren();

    // 将列表中的每个节点导入到新文档中。保留节点的原始格式。
    NodeImporter importer = new NodeImporter(srcDoc, dstDoc, ImportFormatMode.KeepSourceFormatting);

    foreach (Node node in nodes)
    {
        Node importNode = importer.ImportNode(node, true);
        dstDoc.FirstSection.Body.AppendChild(importNode);
    }

    // 返回生成的文档。
    return dstDoc;
}

现在我们准备好使用这些方法并从 Word 文档中提取文本。

在 Word 文档中的段落之间提取文本

让我们看看如何在 Word DOCX 文档的两个段落之间提取内容。以下是在 C# 中执行此操作的步骤。

  • 首先,使用 Document 类加载 Word 文档。
  • 使用 Document.FirstSection.Body.GetChild(NodeType.PARAGRAPH, int, boolean) 方法将开始和结束段落的引用获取到两个对象中。
  • 调用 ExtractContent(startPara, endPara, True) 方法将节点提取到对象中。
  • 调用 GenerateDocument(Document, extractNodes) 辅助方法来创建包含提取内容的文档。
  • 最后,使用 Document.Save(string) 方法保存返回的文档。

以下代码示例展示了如何在 C# 中提取 Word 文档中第 7 段和第 11 段之间的文本。

// 加载 Word 文档
Document doc = new Document("document.docx");

// 收集节点(GetChild 方法使用从 0 开始的索引)
Paragraph startPara = (Paragraph)doc.FirstSection.Body.GetChild(NodeType.Paragraph, 6, true);
Paragraph endPara = (Paragraph)doc.FirstSection.Body.GetChild(NodeType.Paragraph, 10, true);

// 提取文档中这些节点之间的内容。在提取中包含这些标记。
ArrayList extractedNodes = ExtractContent(startPara, endPara, true);

// 将内容插入新文档并将其保存到磁盘。
Document dstDoc = GenerateDocument(doc, extractedNodes);
dstDoc.Save("output.docx");

在 Word 文档中不同类型的节点之间提取文本

您还可以在不同类型的节点之间提取内容。为了演示,让我们提取段落和表格之间的内容并将其保存到新的 Word 文档中。以下是执行此操作的步骤。

  • 使用 Document 类加载 Word 文档。
  • 使用 Document.FirstSection.Body.GetChild(NodeType, int, boolean) 方法将起始节点和结束节点引用到两个对象中。
  • 调用 ExtractContent(startPara, endPara, True) 方法将节点提取到对象中。
  • 调用 GenerateDocument(Document, extractNodes) 辅助方法来创建包含提取内容的文档。
  • 使用 Document.Save(string) 方法保存返回的文档。

以下代码示例演示如何在 C# 中提取段落和表格之间的文本。

// 加载 Word 文档
Document doc = new Document("document.docx");

Paragraph startPara = (Paragraph)doc.LastSection.GetChild(NodeType.Paragraph, 2, true);
Table endTable = (Table)doc.LastSection.GetChild(NodeType.Table, 0, true);

// 提取文档中这些节点之间的内容。在提取中包含这些标记。
ArrayList extractedNodes = ExtractContent(startPara, endTable, true);

// 将内容插入新文档并将其保存到磁盘。
Document dstDoc = GenerateDocument(doc, extractedNodes);
dstDoc.Save("output.docx");

根据样式提取段落之间的文本

现在让我们看看如何根据样式提取段落之间的内容。为了演示,我们将提取 Word 文档中第一个“标题 1”和第一个“标题 3”之间的内容。以下步骤演示了如何在 C# 中实现此目的。

  • 首先,使用 Document 类加载 Word 文档。
  • 然后,使用 ParagraphsByStyleName(Document, “Heading 1”) 辅助方法将段落提取到对象中。
  • 使用 ParagraphsByStyleName(Document, “Heading 3”) 辅助方法将段落提取到另一个对象中。
  • 调用 ExtractContent(startPara, endPara, True) 方法并将两个段落数组中的第一个元素作为第一个和第二个参数传递。
  • 调用 GenerateDocument(Document, extractNodes) 辅助方法来创建包含提取内容的文档。
  • 最后,使用 Document.Save(string) 方法保存返回的文档。

以下代码示例展示了如何根据样式提取段落之间的内容。

// 加载 Word 文档
Document doc = new Document("document.docx");

// 使用相应的标题样式收集段落列表。
List<Paragraph> parasStyleHeading1 = ParagraphsByStyleName(doc, "Heading 1");
List<Paragraph> parasStyleHeading3 = ParagraphsByStyleName(doc, "Heading 3");

// 使用具有这些样式的段落的第一个实例。
Node startPara1 = (Node)parasStyleHeading1[0];
Node endPara1 = (Node)parasStyleHeading3[0];

// 提取文档中这些节点之间的内容。不要在提取中包含这些标记。
ArrayList extractedNodes = ExtractContent(startPara1, endPara1, false);

// 将内容插入新文档并将其保存到磁盘。
Document dstDoc = GenerateDocument(doc, extractedNodes);
dstDoc.Save("output.docx");

阅读更多

您可以使用 this 文档文章探索从 Word 文档中提取文本的其他方案。

获取免费 API 许可证

您可以获得 临时许可证 以使用 Aspose.Words for .NET,而不受评估限制。

结论

在本文中,您学习了如何使用 C# 从 MS Word 文档中提取文本。此外,您还了解了如何以编程方式在 Word 文档中相似或不同类型的节点之间提取内容。因此,您可以在 C# 中构建自己的 MS Word 文本提取器。此外,您可以使用 文档 探索 Aspose.Words for .NET 的其他功能。如果您有任何问题,请随时通过我们的 论坛 告诉我们。

也可以看看

提示:您可能需要检查 Aspose PowerPoint to Word Converter,因为它演示了流行的演示文稿到 Word 文档的转换过程。