在 Java 中從 MS Word 文檔中提取文本

從 Word 文檔中提取文本通常在不同的場景中進行。例如,分析文本,提取文檔的特定部分並將它們組合成一個文檔,等等。在本文中,您將學習如何使用 Java 以編程方式從 Word 文檔中提取文本。此外,我們還將介紹如何動態提取段落、表格等特定元素之間的內容。

從 Word 文檔中提取文本的 Java 庫

Aspose.Words for Java 是一個功能強大的庫,可讓您從頭開始創建 MS Word 文檔。此外,它還允許您對現有的 Word 文檔進行加密、轉換、文本提取等操作。我們將使用該庫從 Word DOCX 或 DOC 文檔中提取文本。您可以下載 API 的 JAR 或使用以下 Maven 配置安裝它。

<repository>
    <id>AsposeJavaAPI</id>
    <name>Aspose Java API</name>
    <url>https://repository.aspose.com/repo/</url>
</repository>
<dependency>
    <groupId>com.aspose</groupId>
    <artifactId>aspose-words</artifactId>
    <version>22.6</version>
    <type>pom</type>
</dependency>

Java 中 Word DOC/DOCX 中的文本提取

MS Word 文檔由各種元素組成,包括段落、表格、圖像等。因此,文本提取的要求可能因場景而異。例如,您可能需要提取段落、書籤、評論等之間的文本。

Word DOC/DOCX 中的每種類型的元素都表示為一個節點。因此,要處理文檔,您將不得不使用節點。那麼讓我們開始吧,看看如何在不同的場景下從Word文檔中提取文本。

從 Java 中的 Word DOC 中提取文本

在本節中,我們將為 Word 文檔實現一個 Java 文本提取器,文本提取的工作流程如下:

  • 首先,我們將定義要包含在文本提取過程中的節點。
  • 然後,我們將提取指定節點之間的內容(包括或不包括起始和結束節點)。
  • 最後,我們將使用提取節點的克隆,例如創建一個包含提取內容的新 Word 文檔。

現在讓我們編寫一個名為 extractContent 的方法,我們將向其傳遞節點和一些其他參數以執行文本提取。此方法將解析文檔並克隆節點。以下是我們將傳遞給此方法的參數。

  1. startNode 和 endNode 分別作為提取內容的起點和終點。這些可以是塊級別(段落、表格)或內聯級別(例如 Run、FieldStart、BookmarkStart 等)節點。
    1. 要傳遞字段,您應該傳遞相應的 FieldStart 對象。
    2. 要傳遞書籤,應傳遞 BookmarkStart 和 BookmarkEnd 節點。
    3. 對於註釋,應使用 CommentRangeStart 和 CommentRangeEnd 節點。
  2. isInclusive 定義標記是否包含在提取中。如果此選項設置為 false 並傳遞相同的節點或連續的節點,則將返回一個空列表。

以下是提取傳遞的節點之間的內容的 extractContent 方法的完整實現。

// 有關完整示例和數據文件,請訪問 https://github.com/aspose-words/Aspose.Words-for-Java
public static ArrayList extractContent(Node startNode, Node endNode, boolean isInclusive) throws Exception {
    // 首先檢查傳遞給此方法的節點是否有效。
    verifyParameterNodes(startNode, endNode);

    // 創建一個列表來存儲提取的節點。
    ArrayList nodes = new ArrayList();

    // 記錄傳遞給此方法的原始節點,以便我們可以在需要時拆分標記節點。
    Node originalStartNode = startNode;
    Node originalEndNode = endNode;

    // 基於塊級節點(段落和表格)提取內容。遍歷父節點以找到它們。
    // 我們將根據標記節點是否內聯來拆分第一個和最後一個節點的內容
    while (startNode.getParentNode().getNodeType() != NodeType.BODY)
        startNode = startNode.getParentNode();

    while (endNode.getParentNode().getNodeType() != NodeType.BODY)
        endNode = endNode.getParentNode();

    boolean isExtracting = true;
    boolean isStartingNode = true;
    boolean isEndingNode;
    // 我們從文檔中提取的當前節點。
    Node currNode = startNode;

    // 開始提取內容。處理所有塊級節點,並在需要時專門拆分第一個和最後一個節點,以便保留段落格式。
    // 方法比常規提取器複雜一點,因為我們需要考慮使用內聯節點、字段、書籤等進行提取,以使其真正有用。
    while (isExtracting) {
        // 克隆當前節點及其子節點以獲得副本。
        /*System.out.println(currNode.getNodeType());
        if(currNode.getNodeType() == NodeType.EDITABLE_RANGE_START
                || currNode.getNodeType() == NodeType.EDITABLE_RANGE_END)
        {
            currNode = currNode.nextPreOrder(currNode.getDocument());
        }*/
        System.out.println(currNode);
        System.out.println(endNode);

        CompositeNode cloneNode = null;
        ///cloneNode = (CompositeNode) currNode.deepClone(true);

        Node inlineNode = null;
        if(currNode.isComposite())
        {
            cloneNode = (CompositeNode) currNode.deepClone(true);
        }
        else
        {
            if(currNode.getNodeType() == NodeType.BOOKMARK_END)
            {
                Paragraph paragraph = new Paragraph(currNode.getDocument());
                paragraph.getChildNodes().add(currNode.deepClone(true));
                cloneNode = (CompositeNode)paragraph.deepClone(true);
            }
        }

        isEndingNode = currNode.equals(endNode);

        if (isStartingNode || isEndingNode) {
            // 我們需要單獨處理每個標記,因此將其傳遞給單獨的方法。
            if (isStartingNode) {
                processMarker(cloneNode, nodes, originalStartNode, isInclusive, isStartingNode, isEndingNode);
                isStartingNode = false;
            }

            // 條件需要分開,因為塊級開始和結束標記可能是同一個節點。
            if (isEndingNode) {
                processMarker(cloneNode, nodes, originalEndNode, isInclusive, isStartingNode, isEndingNode);
                isExtracting = false;
            }
        } else
            // 節點不是開始或結束標記,只需將副本添加到列表中即可。
            nodes.add(cloneNode);

        // 移動到下一個節點並提取它。如果下一個節點為空,則意味著其餘內容位於不同的部分。
        if (currNode.getNextSibling() == null && isExtracting) {
            // 移至下一節。
            Section nextSection = (Section) currNode.getAncestor(NodeType.SECTION).getNextSibling();
            currNode = nextSection.getBody().getFirstChild();
        } else {
            // 移動到正文中的下一個節點。
            currNode = currNode.getNextSibling();
        }
    }

    // 返回節點標記之間的節點。
    return nodes;
}

extractContent 方法還需要一些輔助方法來完成文本提取操作,如下所示。

/**
 * 檢查輸入參數是否正確,是否可以使用。拋出異常
 * 如果有任何問題。
 */
private static void verifyParameterNodes(Node startNode, Node endNode) throws Exception {
	// 這些檢查的執行順序很重要。
	if (startNode == null)
		throw new IllegalArgumentException("Start node cannot be null");
	if (endNode == null)
		throw new IllegalArgumentException("End node cannot be null");

	if (!startNode.getDocument().equals(endNode.getDocument()))
		throw new IllegalArgumentException("Start node and end node must belong to the same document");

	if (startNode.getAncestor(NodeType.BODY) == null || endNode.getAncestor(NodeType.BODY) == null)
		throw new IllegalArgumentException("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.getParentNode().indexOf(startSection);
	int endIndex = endSection.getParentNode().indexOf(endSection);

	if (startIndex == endIndex) {
		if (startSection.getBody().indexOf(startNode) > endSection.getBody().indexOf(endNode))
			throw new IllegalArgumentException("The end node must be after the start node in the body");
	} else if (startIndex > endIndex)
		throw new IllegalArgumentException("The section of end node must be after the section start node");
}

/**
 * 檢查傳遞的節點是否為內聯節點。
 */
private static boolean isInline(Node node) throws Exception {
	// 測試該節點是否是段落或表節點的後代並且也不是
	// 段落或表格評論類中的段落是
	// 一個段落是可能的。
	return ((node.getAncestor(NodeType.PARAGRAPH) != null || node.getAncestor(NodeType.TABLE) != null)
			&& !(node.getNodeType() == NodeType.PARAGRAPH || node.getNodeType() == NodeType.TABLE));
}

/**
 * 刪除克隆節點中標記之前或之後的內容,具體取決於
 * 關於標記的類型。
 */
private static void processMarker(CompositeNode cloneNode, ArrayList nodes, Node node, boolean isInclusive,
		boolean isStartMarker, boolean isEndMarker) throws Exception {
	// 如果我們正在處理一個塊級節點,看看它是否應該被包括在內
	// 並將其添加到列表中。
	if (!isInline(node)) {
		// 如果標記是同一個節點,則不要添加節點兩次
		if (!(isStartMarker && isEndMarker)) {
			if (isInclusive)
				nodes.add(cloneNode);
		}
		return;
	}

	// 如果標記是 FieldStart 節點,請檢查它是否包含在內。
	// 為簡單起見,我們假設 FieldStart 和 FieldEnd 出現在同一個
	// 段落。
	if (node.getNodeType() == NodeType.FIELD_START) {
		// 如果標記是起始節點且未被包含,則跳至
		// 場。
		// 如果標記是結束節點並且要包含它則移動到結束
		// 字段,因此該字段不會被刪除。
		if ((isStartMarker && !isInclusive) || (!isStartMarker && isInclusive)) {
			while (node.getNextSibling() != null && node.getNodeType() != NodeType.FIELD_END)
				node = node.getNextSibling();

		}
	}

	// 如果任一標記是評論的一部分,那麼為了包含評論本身,我們
	// 需要將指針向前移動到 Comment
	// 在 CommentRangeEnd 節點之後找到的節點。
	if (node.getNodeType() == NodeType.COMMENT_RANGE_END) {
		while (node.getNextSibling() != null && node.getNodeType() != NodeType.COMMENT)
			node = node.getNextSibling();

	}

	// 通過索引在我們克隆的節點中找到對應的節點並返回。
	// 如果起始節點和結束節點相同,一些子節點可能已經有
	// 被刪除。減去
	// 差異以獲得正確的索引。
	int indexDiff = node.getParentNode().getChildNodes().getCount() - cloneNode.getChildNodes().getCount();

	// 子節點計數相同。
	if (indexDiff == 0)
		node = cloneNode.getChildNodes().get(node.getParentNode().indexOf(node));
	else
		node = cloneNode.getChildNodes().get(node.getParentNode().indexOf(node) - indexDiff);

	// 從標記中刪除節點。
	boolean isSkip;
	boolean isProcessing = true;
	boolean isRemoving = isStartMarker;
	Node nextNode = cloneNode.getFirstChild();

	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.getNextSibling();
		if (isRemoving && !isSkip)
			currentNode.remove();
	}

	// 處理後復合節點可能變為空。如果有不包括
	// 它。
	if (!(isStartMarker && isEndMarker)) {
		if (cloneNode.hasChildNodes())
			nodes.add(cloneNode);
	}
}

public static Document generateDocument(Document srcDoc, ArrayList nodes) throws Exception {

	// 創建一個空白文檔。
	Document dstDoc = new Document();
	// 從空文檔中刪除第一段。
	dstDoc.getFirstSection().getBody().removeAllChildren();

	// 將列表中的每個節點導入到新文檔中。保持原來的
	// 節點的格式化。
	NodeImporter importer = new NodeImporter(srcDoc, dstDoc, ImportFormatMode.KEEP_SOURCE_FORMATTING);

	for (Node node : (Iterable<Node>) nodes) {
		Node importNode = importer.importNode(node, true);
		dstDoc.getFirstSection().getBody().appendChild(importNode);
	}

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

現在我們已準備好使用這些方法並從 Word 文檔中提取文本。

Java 提取 Word DOC 中段落之間的文本

讓我們看看如何提取 Word DOCX 文檔中兩個段落之間的內容。以下是在 Java 中執行此操作的步驟。

  • 首先,使用 Document 類加載 Word 文檔。
  • 使用 Document.getFirstSection().getChild(NodeType.PARAGRAPH, int, bool) 方法將開始和結束段落引用到兩個對像中。
  • 調用 extractContent(startPara, endPara, true) 方法將節點提取到對像中。
  • 調用 generateDocument(Document, extractedNodes) 輔助方法來創建包含提取內容的文檔。
  • 最後,使用 Document.save(String) 方法保存返回的文檔。

以下代碼示例顯示瞭如何在 Java 中提取 Word DOCX 中第 7 段和第 11 段之間的文本。

// 載入文件
Document doc = new Document("TestFile.doc");

// 收集節點。 GetChild 方法使用基於 0 的索引
Paragraph startPara = (Paragraph) doc.getFirstSection().getChild(NodeType.PARAGRAPH, 6, true);
Paragraph endPara = (Paragraph) doc.getFirstSection().getChild(NodeType.PARAGRAPH, 10, true);
// 提取文檔中這些節點之間的內容。包括這些
// 提取中的標記。
ArrayList extractedNodes = extractContent(startPara, endPara, true);

// 將內容插入新的單獨文檔並將其保存到磁盤。
Document dstDoc = generateDocument(doc, extractedNodes);
dstDoc.save("output.doc");

Java從DOC中提取文本——不同類型節點之間

您還可以提取不同類型節點之間的內容。為了演示,讓我們提取段落和表格之間的內容並將其保存到一個新的 Word 文檔中。以下是在 Java 中提取 Word 文檔中不同節點之間的文本的步驟。

  • 使用 Document 類加載 Word 文檔。
  • 使用 Document.getFirstSection().getChild(NodeType, int, bool) 方法將開始和結束節點的引用獲取到兩個對像中。
  • 調用 extractContent(startPara, endPara, true) 方法將節點提取到對像中。
  • 調用 generateDocument(Document, extractedNodes) 輔助方法來創建包含提取內容的文檔。
  • 使用 Document.save(String) 方法保存返回的文檔。

以下代碼示例顯示瞭如何使用 Java 在 DOCX 中提取段落和表格之間的文本。

// 裝入文檔
Document doc = new Document("TestFile.doc");

// 獲取起始段落的引用
Paragraph startPara = (Paragraph) doc.getLastSection().getChild(NodeType.PARAGRAPH, 2, true);
Table endTable = (Table) doc.getLastSection().getChild(NodeType.TABLE, 0, true);

// 提取文檔中這些節點之間的內容。在提取中包括這些標記。
ArrayList extractedNodes = extractContent(startPara, endTable, true);

// 讓我們反轉數組,以便更容易地將內容插入回文檔中。
Collections.reverse(extractedNodes);

while (extractedNodes.size() > 0) {
    // 插入反向列表中的最後一個節點
    endTable.getParentNode().insertAfter((Node) extractedNodes.get(0), endTable);
    // 插入後從列表中刪除該節點。
    extractedNodes.remove(0);
}

// 將生成的文檔保存到磁盤。
doc.save("output.doc");

Java從DOCX中提取文本-基於樣式的段落之間

現在讓我們看看如何根據樣式提取段落之間的內容。為了演示,我們將提取 Word 文檔中第一個“標題 1”和第一個“標題 3”之間的內容。以下步驟演示瞭如何在 Java 中實現此目的。

  • 首先,使用 Document 類加載 Word 文檔。
  • 然後,使用 paragraphsByStyleName(Document, “Heading 1”) 輔助方法將段落提取到一個對像中。
  • 使用 paragraphsByStyleName(Document, “Heading 3”) 輔助方法將段落提取到另一個對像中。
  • 調用 extractContent(startPara, endPara, true) 方法並將兩個段落數組中的第一個元素作為第一個和第二個參數傳遞。
  • 調用 generateDocument(Document, extractedNodes) 輔助方法來創建包含提取內容的文檔。
  • 最後,使用 Document.save(String) 方法保存返回的文檔。

以下代碼示例顯示瞭如何根據樣式提取段落之間的內容。

// 載入文件
Document doc = new Document(dataDir + "TestFile.doc");

// 使用各自的標題樣式收集段落列表。
ArrayList parasStyleHeading1 = paragraphsByStyleName(doc, "Heading 1");
ArrayList parasStyleHeading3 = paragraphsByStyleName(doc, "Heading 3");

// 使用具有這些樣式的段落的第一個實例。
Node startPara1 = (Node) parasStyleHeading1.get(0);
Node endPara1 = (Node) parasStyleHeading3.get(0);

// 提取文檔中這些節點之間的內容。不要在提取中包含這些標記。
ArrayList extractedNodes = extractContent(startPara1, endPara1, false);

// 將內容插入新的單獨文檔並將其保存到磁盤。
Document dstDoc = generateDocument(doc, extractedNodes);
dstDoc.save("output.doc");

Java Word 文本提取器 - 了解更多

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

從 DOC/DOCX 中提取文本的 Java API - 獲得免費許可證

您可以獲得 臨時許可 以在沒有評估限制的情況下使用 Aspose.Words for Java。

結論

在本文中,您學習瞭如何使用 Java 從 MS Word DOC DOCX 中提取文本。此外,您還了解瞭如何以編程方式提取 Word 文檔中相似或不同類型節點之間的內容。因此,您可以用 Java 構建自己的 MS Word 文本提取器。此外,您可以使用 文檔 探索 Aspose.Words for Java 的其他功能。如果您有任何疑問,請隨時通過我們的 論壇 告訴我們。

也可以看看