Робота з деревом вузлів документа
Робота з деревом вузлів документа
У цьому посібнику показано, як орієнтуватися та маніпулювати структурою документа безпосередньо через його дерево вузлів — об’єктну модель, на якій побудовані Document, Section, Paragraph, Run, Table та кожен інший елемент документа. Кожен елемент у документі успадковується від Node, а кожен елемент, здатний містити дочірні елементи, успадковується від CompositeNode, тому розуміння цих двох базових класів пояснює, як працювати з вмістом, який ще не охоплює жоден спеціалізований допоміжний API.
Класи Node та базовий клас CompositeNode
Node — це абстрактний базовий клас для кожного об’єкта в дереві документа. Він надає NodeType (значення enum, що ідентифікує тип вузла), 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, вбудованого в потік запуску: 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) |