Извлечение текста из документов MS Word в Java

Извлечение текста из документов Word часто выполняется по разным сценариям. Например, для анализа текста, выделения отдельных разделов документа и объединения их в один документ и т.д. В этой статье вы узнаете, как программно извлекать текст из документов Word на Java. Кроме того, мы рассмотрим, как динамически извлекать содержимое между определенными элементами, такими как абзацы, таблицы и т. д.

Библиотека Java для извлечения текста из документов Word

Aspose.Words for Java — мощная библиотека, позволяющая создавать документы MS Word с нуля. Кроме того, она позволяет вам манипулировать существующими документами Word для шифрования, преобразования, извлечения текста и т. д. Мы будем использовать эту библиотеку для извлечения текста из документов Word DOCX или DOC. Вы можете скачать JAR-файл API или установить его, используя следующие конфигурации 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>

Извлечение текста в Word DOC/DOCX на Java

Документ MS Word состоит из различных элементов, включая абзацы, таблицы, изображения и т. д. Поэтому требования к извлечению текста могут различаться в зависимости от сценария. Например, вам может понадобиться извлечь текст между абзацами, закладками, комментариями и т. д.

Каждый тип элемента в Word DOC/DOCX представлен как узел. Поэтому для обработки документа придется поиграться с узлами. Итак, давайте начнем и посмотрим, как извлечь текст из документов Word в разных сценариях.

Извлечь текст из Word DOC в Java

В этом разделе мы собираемся реализовать экстрактор текста Java для документов Word, и рабочий процесс извлечения текста будет следующим:

  • Во-первых, мы определим узлы, которые мы хотим включить в процесс извлечения текста.
  • Затем мы извлечем содержимое между указанными узлами (включая или исключая начальный и конечный узлы).
  • Наконец, мы будем использовать клон извлеченных узлов, например, для создания нового документа 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);

        // Перейдите к следующему узлу и извлеките его. Если следующий узел имеет значение null, это означает, что остальная часть содержимого находится в другом разделе.
        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.

  • Сначала загрузите документ Word, используя класс Document.
  • Получите ссылку на начальный и конечный абзацы на два объекта, используя метод Document.getFirstSection().getChild(NodeType.PARAGRAPH, int, bool).
  • Вызовите метод extractContent(startPara, endPara, true) для извлечения узлов в объект.
  • Вызовите вспомогательный метод generateDocument(Document, ExtractedNodes) для создания документа, состоящего из извлеченного содержимого.
  • Наконец, сохраните возвращенный документ с помощью метода Document.save(String).

В следующем примере кода показано, как извлечь текст между 7-м и 11-м абзацами в Word DOCX на Java.

// Загрузить документ
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. Ниже приведены шаги для извлечения текста между разными узлами в документе Word на Java.

  • Загрузите документ Word, используя класс Document.
  • Получите ссылку на начальный и конечный узлы в два объекта, используя метод Document.getFirstSection().getChild(NodeType, int, bool).
  • Вызовите метод extractContent(startPara, endPara, true) для извлечения узлов в объект.
  • Вызовите вспомогательный метод generateDocument(Document, ExtractedNodes) для создания документа, состоящего из извлеченного содержимого.
  • Сохраните возвращенный документ с помощью метода Document.save(String).

В следующем примере кода показано, как извлечь текст между абзацем и таблицей в DOCX с помощью Java.

// Загрузить документы
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 — между абзацами на основе стилей

Давайте теперь посмотрим, как извлечь содержимое между абзацами на основе стилей. Для демонстрации мы собираемся извлечь содержимое между первым «Заголовком 1» и первым «Заголовком 3» в документе Word. Следующие шаги демонстрируют, как добиться этого в Java.

  • Сначала загрузите документ Word, используя класс Document.
  • Затем извлеките абзацы в объект с помощью вспомогательного метода parapsByStyleName(Document, «Заголовок 1»).
  • Извлеките абзацы в другой объект с помощью вспомогательного метода parapsByStyleName(Document, «Заголовок 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 — Подробнее

Вы можете изучить другие сценарии извлечения текста из документов Word, используя эту статью документации.

Java API для извлечения текста из DOC/DOCX — получите бесплатную лицензию

Вы можете получить временную лицензию на использование Aspose.Words for Java без ограничений на пробную версию.

Вывод

В этой статье вы узнали, как извлечь текст из MS Word DOC DOCX в Java. Кроме того, вы видели, как программно извлекать содержимое между похожими или разными типами узлов в документе Word. Таким образом, вы можете создать свой собственный экстрактор текста MS Word на Java. Кроме того, вы можете изучить другие возможности Aspose.Words for Java, используя документацию. Если у вас возникнут какие-либо вопросы, сообщите нам об этом через наш форум.

Смотрите также