Работа с деревом узлов документа
Работ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) |