在 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 分別作為提取內容的起點和終點。這些可以是塊級別(段落、表格)或內聯級別(例如 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, extractedNodes) 輔助方法來創建包含提取內容的文檔。
  • 最後,使用 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, extractedNodes) 輔助方法來創建包含提取內容的文檔。
  • 使用 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, extractedNodes) 輔助方法來創建包含提取內容的文檔。
  • 最後,使用 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 文檔的轉換過程。