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

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

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

Aspose.Words for .NET — мощная библиотека, позволяющая создавать документы MS Word с нуля. Кроме того, она позволяет вам манипулировать существующими документами Word для шифрования, преобразования, извлечения текста и т. д. Мы будем использовать эту библиотеку для извлечения текста из документов Word DOCX или DOC. Вы можете загрузить библиотеку DLL API или установить ее непосредственно из NuGet с помощью консоли диспетчера пакетов.

PM> Install-Package Aspose.Words

Извлечение текста в документах Word с использованием C#

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

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

Извлечь текст из документа Word в C#

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

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

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

  • Сначала загрузите документ Word, используя класс Document.
  • Получите ссылку на начальный и конечный абзацы на два объекта, используя метод Document.FirstSection.Body.GetChild(NodeType.PARAGRAPH, int, boolean).
  • Вызовите метод ExtractContent(startPara, endPara, True), чтобы извлечь узлы в объект.
  • Вызовите вспомогательный метод GenerateDocument(Document, ExtractedNodes), чтобы создать документ, состоящий из извлеченного содержимого.
  • Наконец, сохраните возвращенный документ с помощью метода Document.Save(string).

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

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

  • Загрузите документ Word, используя класс Document.
  • Получите ссылку на начальный и конечный узлы в два объекта, используя метод 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");

Извлечение текста между абзацами на основе стилей

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

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

Читать далее

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

Получите бесплатную лицензию API

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

Вывод

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

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

Совет: вы можете проверить Aspose PowerPoint to Word Converter, потому что он демонстрирует популярный процесс преобразования презентации в документ Word.