从 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 分别作为内容提取的起点和终点。这些可以是块级(Paragraph、Table)或内联级(例如 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();

		}
	}

	// 如果任一标记是评论的一部分,那么为了包含评论本身,我们
	// 需要将指针向前移动到评论
	// 在 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, extractNodes) 辅助方法来创建包含提取内容的文档。
  • 最后,使用 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, extractNodes) 辅助方法来创建包含提取内容的文档。
  • 使用 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 文档。
  • 然后,使用parametersByStyleName(Document, “Heading 1”) 辅助方法将段落提取到一个对象中。
  • 使用parametersByStyleName(Document, “Heading 3”) 辅助方法将段落提取到另一个对象中。
  • 调用 extractContent(startPara, endPara, true) 方法并将两个段落数组中的第一个元素作为第一个和第二个参数传递。
  • 调用 generateDocument(Document, extractNodes) 辅助方法来创建包含提取内容的文档。
  • 最后,使用 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 DOCX DOCX 中提取文本。此外,您还了解了如何以编程方式在 Word 文档中相似或不同类型的节点之间提取内容。因此,您可以在 Java 中构建自己的 MS Word 文本提取器。此外,您可以使用 文档 探索 Aspose.Words for Java 的其他功能。如果您有任何问题,请随时通过我们的 论坛 告诉我们。

也可以看看