Работа с деревом узлов документа

Работа с деревом узлов документа

Работa с деревом узлов документа

Это руководство показывает, как перемещаться по структуре документа и манипулировать ею напрямую через дерево узлов — объектную модель, на которой построены Document, Section, Paragraph, Run, Table и каждый другой элемент документа. Каждый элемент документа наследуется от Node, а каждый элемент, способный содержать дочерние элементы, наследуется от CompositeNode, поэтому понимание этих двух базовых классов объясняет, как работать с содержимым, которое не покрывается ни одним специализированным помощником API.


Базовые классы Node и CompositeNode

Node — абстрактный базовый класс для каждого объекта в дереве документа. Он предоставляет NodeType (значение перечисления, определяющее тип узла), ParentNode (вложенный CompositeNode), Document (владелец документа) и PreviousSibling/NextSibling для перемещения по узлам одного уровня. Node.IsComposite сообщает, может ли данный узел содержать дочерние элементы. CompositeNode расширяет Node для контейнерных элементов — FirstChild, LastChild и Count описывают его непосредственных детей, а HasChildNodes сообщает, существуют ли они. CompositeNode.GetChildNodes(nodeType, isDeep) получает всех дочерних элементов заданного NodeType, при желании выполняя поиск по всему поддереву (isDeep) вместо только прямых детей, а CompositeNode.GetChild(nodeType, index, isDeep) получает один элемент по индексу.

Перечисление NodeType

NodeType перечисляет все типы узлов, определённые моделью документа — структурные типы, такие как Document, Section, Body, Paragraph, Table, Row и Cell; типы встроенного контента, такие как Run, Shape и Comment; и типы полевой механики, такие как FieldStart, FieldSeparator и FieldEnd, которые обозначают границы и переключающий текст поля Word в потоке run. Передача значения NodeType в GetChildNodes или GetChild ограничивает поиск узлами именно этого типа, либо передайте NodeType.Any, чтобы сопоставить каждый тип узла.

Обход и изменение дерева

CompositeNode.GetEnumerator() позволяет итерировать прямых потомков узла, а CompositeNode также реализует SelectNodes(xpath) и SelectSingleNode(xpath) для XPath-запросов по поддереву. Чтобы изменить дерево, AppendChild(newChild) и PrependChild(newChild) добавляют узел в начало или конец списка детей, InsertBefore(newChild, refChild) и InsertAfter(newChild, refChild) вставляют относительно существующего дочернего узла, а RemoveChild(oldChild) или RemoveAllChildren() удаляют узлы. CompositeNode.IndexOf(child) возвращает позицию дочернего узла. На самом Node Clone(isCloneChildren) создаёт копию — при желании глубокую — и Remove() отсоединяет узел от его родителя. Node.NextPreOrder(rootNode) и PreviousPreOrder(rootNode) проходят по дереву в порядке документа, ограниченном заданным корневым узлом, что является распространённым способом сканировать всё поддерево узел за узлом без рекурсивного кода.

Коллекции узлов и уведомления об изменениях

NodeCollection — конкретная изменяемая коллекция узлов, поддерживающая Add(node), Insert(index, node), Remove(node), RemoveAt(index), Clear(), Contains(node), IndexOf(node) и ToArray(), а также свойство Count. NodeList и NodeEnumerator поддерживают итерацию в режиме чтения над результатами запросов, например над узлами, возвращёнными SelectNodes. Для наблюдения за структурными изменениями в реальном времени реализуйте INodeChangingCallback — его методы NodeInserting(args), NodeInserted(args), NodeRemoving(args) и NodeRemoved(args) каждый получают NodeChangingArgs, описывающий затронутый Node, его OldParent/NewParent и произошедшее NodeChangingAction.

Обход узлов с помощью DocumentVisitor

Для обработки каждого типа узла структурированным способом без ручного обхода дерева, создайте подкласс DocumentVisitor и переопределите методы Visit*Start/Visit*End для интересующих вас типов узлов — VisitParagraphStart/VisitParagraphEnd, VisitTableStart/VisitTableEnd, VisitRun и т.д. — затем вызовите Node.Accept(visitor) на корне поддерева, чтобы запустить его. Это стандартный шаблон для экспорта, трансформации или инспекции документа без написания собственного рекурсивного обхода для каждого случая.


Советы и лучшие практики

  • Используйте NodeType.Any вместе с GetChildNodes(NodeType.Any, true), когда нужны все потомки независимо от типа, вместо вызова GetChildNodes по одному разу для каждого типа.
  • Отдавайте предпочтение Node.NextPreOrder(rootNode) / PreviousPreOrder(rootNode) для плоского, нерекурсивного сканирования поддерева вместо написания ручного рекурсивного спуска.
  • Реализуйте DocumentVisitor, а не произвольный код обхода дерева, когда одинаковая обработка должна применяться к множеству разных типов узлов.
  • Проверьте Node.IsComposite перед приведением к CompositeNode, когда работаете с узлом неизвестного типа.
  • Зарегистрируйте INodeChangingCallback, когда необходимо реагировать на вставки или удаления, выполненные другим кодом (в том числе встроенными операциями), а не только на изменения, которые ваш собственный код вносит напрямую.

Общие проблемы

ПроблемаПричинаИсправление
Приведение Node к CompositeNode вызывает ошибку неверного приведенияУзел является листовым типом (например Run или FieldStart), который не может содержать дочерние элементыСначала проверьте Node.IsComposite или используйте NodeType, чтобы подтвердить, что узел является типом-контейнером, перед приведением
GetChildNodes возвращает только прямых потомковisDeep был оставлен false (или опущен, где перегрузка по умолчанию поверхностная)Передайте isDeep: true, чтобы выполнить поиск по всему поддереву
Изменение коллекции во время её обхода вызывает ошибкуУзлы были добавлены или удалены внутри foreach над NodeCollection или результата GetChildNodesСначала материализуйте узлы, которые нужно изменить, с помощью ToArray(), затем проходите массив, изменяя дерево
Подкласс DocumentVisitor не вызывается для некоторого контентаПосетитель был вызван через Accept() на узле, который не содержит целевой контент, либо соответствующий метод Visit* не был переопределёнВызовите Accept() у предка, содержащего контент, и переопределите каждую пару Visit*Start/Visit*End, относящуюся к типам обрабатываемых узлов

FAQ

В чём разница между Node и CompositeNode?

Node — это базовый класс, от которого наследуются все элементы документа. CompositeNode расширяет Node для элементов, которые могут содержать дочерние элементы (например, Body, Paragraph и Table); листовые элементы (например, Run) наследуются напрямую от Node и не могут содержать дочерних элементов.

Как найти все узлы определённого типа в документе?

Вызовите CompositeNode.GetChildNodes(nodeType, isDeep) на узле, который является корнем вашего поиска (часто сам Document), передавая нужный NodeType и isDeep: true, чтобы искать во всём поддереве, а не только в прямых дочерних элементах.

Как безопасно удалять узлы во время итерации?

Сначала соберите узлы для удаления в массив — например с помощью NodeCollection.ToArray() — затем пройдитесь по этому массиву и вызовите Remove() для каждого узла, вместо того чтобы изменять живую коллекцию, пока foreach её перебирает.

Что представляют собой узлы FieldStart, FieldSeparator и FieldEnd?

Это значения NodeType, обозначающие механизм поля Word, встроенного в поток run: FieldStart и FieldEnd ограничивают поле, а FieldSeparator разделяют код поля и его текущий отображаемый результат.

Как обработать каждый абзац, таблицу и run в документе?

Создайте подкласс DocumentVisitor, переопределите VisitParagraphStart/VisitParagraphEnd, VisitTableStart/VisitTableEnd и VisitRun для нужных вам типов узлов, затем вызовите Accept(visitor) на Document (или любом корне поддерева), чтобы выполнить его.


API Reference Резюме

Класс/МетодОписание:
NodeАбстрактный базовый класс для каждого узла в дереве документа
CompositeNodeБазовый класс для узлов, которые могут содержать дочерние элементы
CompositeNode.GetChildNodes(nodeType, isDeep)Получить всех дочерних элементов заданного типа, при необходимости по всему поддереву
CompositeNode.AppendChild(newChild) / InsertBefore(newChild, refChild)Добавить узел в контейнер в конец или перед/после существующего дочернего элемента
NodeTypeПеречисление, определяющее тип узла (Paragraph, Table, Run, FieldStart и т.д.)
NodeCollectionКонкретная, изменяемая коллекция объектов Node
Node.Clone(isCloneChildren)Скопировать узел, опционально включая его дочерние элементы
Node.NextPreOrder(rootNode) / PreviousPreOrder(rootNode)Обойти поддерево в порядке документа
INodeChangingCallbackИнтерфейс обратного вызова, уведомляющий о вставке или удалении узлов
DocumentVisitorБазовый класс для структурированного обхода с диспетчеризацией по типу через Node.Accept(visitor)

См. также:

 Русский