Витяг тексту з документів MS Word на Java

У різних випадках вам може знадобитися програмно витягувати текст із документів Word. Наприклад, для аналізу тексту, виділення окремих розділів тощо. Для таких випадків ця стаття пропонує швидкий і простий у реалізації метод вилучення тексту з документів Word у Java. Крім того, це дозволить витягувати текст між різними розділами/елементами документа Word.

Бібліотека 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

Документ MS Word складається з різних елементів, які включають абзаци, таблиці, зображення тощо. Тому вимоги до вилучення тексту можуть відрізнятися від одного сценарію до іншого. Наприклад, вам може знадобитися витягти текст між абзацами, закладки, коментарі тощо.

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

Витягніть текст із Word DOC у Java

У цьому розділі ми збираємося запровадити екстрактор тексту Java для документів Word, і робочий процес вилучення тексту буде таким:

  • Спочатку ми визначимо вузли, які ми хочемо включити в процес вилучення тексту.
  • Потім ми витягнемо вміст між вказаними вузлами (включаючи або виключаючи початковий і кінцевий вузли).
  • Нарешті, ми використаємо клон витягнутих вузлів, наприклад, щоб створити новий документ Word, що складається з вилученого вмісту.

Давайте тепер напишемо метод з назвою extractContent, якому ми передамо вузли та деякі інші параметри для виконання вилучення тексту. Цей метод розбере документ і клонує вузли. Нижче наведено параметри, які ми передамо цьому методу.

  1. startNode і endNode як початкова і кінцева точки для вилучення вмісту відповідно. Це можуть бути вузли рівня блоку (Абзац, Таблиця) або вбудованого рівня (наприклад, Виконати, 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.

Вилучення тексту між абзацами документа Word

Давайте подивимося, як витягти вміст між двома абзацами в документі 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");

Вилучення тексту між різними вузлами документа Word

Ви також можете видобувати вміст між різними типами вузлів. Для демонстрації давайте витягнемо вміст між абзацом і таблицею та збережемо його в новому документі Word. Нижче наведено кроки для вилучення тексту між різними вузлами в документі Word у Java.

  • Завантажте документ Word за допомогою класу Document.
  • Отримайте посилання на початковий і кінцевий вузли в два об’єкти за допомогою методу Document.getFirstSection().getChild(NodeType, int, bool).
  • Викличте метод extractContent(startPara, endPara, true), щоб витягнути вузли в об’єкт.
  • Викличте допоміжний метод generateDocument(Document, extractedNodes), щоб створити документ із витягнутим вмістом.
  • Збережіть повернутий документ за допомогою методу Document.save(String).

У наведеному нижче прикладі коду показано, як витягти текст між абзацом і таблицею у Word 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");

Отримайте текст між абзацами на основі стилів

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

  • Спочатку завантажте документ Word за допомогою класу Document.
  • Потім витягніть абзаци в об’єкт за допомогою допоміжного методу параграфівByStyleName(Document, “Heading 1”).
  • Витягніть абзаци в інший об’єкт за допомогою допоміжного методу параграфівByStyleName(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");

Безкоштовний екстрактор тексту Word для Java

Ви можете отримати безкоштовну тимчасову ліцензію, щоб видобувати текст із документів Word без обмежень оцінки.

Дослідіть бібліотеку Java DOCX

Ви можете вивчити інші сценарії вилучення тексту з документів Word, використовуючи цю статтю документації. Крім того, ви можете досліджувати інші функції Aspose.Words for Java за допомогою документації. Якщо у вас виникнуть запитання, не соромтеся повідомити нас через наш форум.

Висновок

У цій статті ви дізналися, як видобувати текст із документів Word у Java. Крім того, ви бачили, як програмно видобувати вміст між подібними або різними типами вузлів у DOC або DOCX. Таким чином, ви можете створити свій власний екстрактор тексту MS Word на Java.

Дивись також