Измените OuterXml данного узла, чтобы сохранить его в файле, но сделайте узел бесполезным

Я работаю над изменением XML-файлов из существующей системы с помощью PowerShell. Наша система поддерживает использование нескольких узлов в одном теле, и в зависимости от того, как установлен OuterXML, она будет обходить это значение и использовать другой узел с аналогичным значением (я полагаю, это может быть верно для всех файлов XML, но я не хочу делать неверные предположения).

Рассмотрим этот пример XML-файла. Чтобы изменить наши файлы, чтобы прекратить использование определенного узла, нужно добавить перед OuterXml узла x:

<Configuration>
  <MainBlock>
    <Object NO = "01" REVISION = "255">
       <CODE TRANSPARENT = "TRUE" />
    </Object>
    <Object NO = "02" REVISION = "255">
       <CODE TRANSPARENT = "TRUE" />
    </Object>
    <Object NO = "03" REVISION = "255">
       <CODE TRANSPARENT = "TRUE" />
    </Object>
    <Object NO = "04" REVISION = "255">
       <CODE TRANSPARENT = "TRUE" />
    </Object>
    <!--Note - this line is the line I wish to modify so it is no longer in use.  The example below was edited manually-->
    <xObject NO = "05" REVISION = "255">
       <CODE TRANSPARENT = "TRUE" />
    </xObject>
    <!--By prepending the "OuterXML" of the node above to "xObject" instead of "Object" I can keep the original values there, but use the "Object" below with a different code - this is an extremely simplified version of this concept.-->
    <Object NO = "05" REVISION = "255">
       <CODE TRANSPARENT = "FALSE" />
    </Object>
  </MainBlock>
</Configuration>

Проблема, с которой я сталкиваюсь, заключается в изменении OuterXml данного узла, поскольку OuterXml является атрибутом только для чтения. . . Когда я вижу узлы, использующие $nodes = $Xml.SelectNodes для сбора объектов, я возвращаю счетчик 5 (как и ожидалось в этом решении), но как мне изменить OuterXml узла xObject, чтобы он читался как Object и узел Object под ним читать как xObject? Вот моя попытка:

  $Xml=[XML] (Get-Content -Path C:\FooBar.xml)
  
  $nodes=$Xml.SelectNodes("//Configuration/MainBlock/Object")
  $node = $nodes.Item(4).OuterXml #<< Here I can set the $node value as a STRING but not an actual XML element, but the $node element DOES contain the contents of the OuterXml Content.
  $nodes.Item(4) = $node #This does not error out but it also does NOTHING to the existing XML content
  
  $Xml.Save("C:\FooBar.xml")

Поймите: существует около 25 различных атрибутов/значений для каждого узла Object в фактической структуре XML, с которой я имею дело - этот пример предназначен только для быстрой/простой иллюстрации концепции. Хотя в приведенном выше примере я мог бы легко просто изменить значение «Прозрачность» на «True» — это невозможно сделать для наших файлов.

Я знаю, что могу использовать команду -replace для замены «Object» на «xObject» с помощью PowerShell исключительно с точки зрения «Найти и заменить», но я пытаюсь использовать XML-функции PowerShell в полной мере.

  #While this works, it's kludgy and isn't fool proof.
  $XmlFile=(Get-Content -Path C:\FooBar.xml)
  $TextToChange=$XmlFile -replace 'xObject','fooObject'; -replace 'Object NO=\"5','xObject NO=\"5'; -replace 'fooObject','Object'
  Set-Content -Path C:\FooBar.xml -Value $TextToChange

Для таких задач лучше использовать XSLT и просто вызывать этот XSLT из PowerShell.

Yitzhak Khabinsky 23.07.2024 14:02

Спасибо @YitzhakKhabinsky, но в системе, в которой я работаю, у меня нет шаблона, необходимого для использования XSLT, и он мне не будет предоставлен. Подход, который я должен принять, потребует использования какой-либо прямой модификации XML.

k1dfr0std 23.07.2024 21:19
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
74
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Используйте Xml Linq с Powershell

using assembly System.Xml
using assembly System.Xml.Linq

$filename = 'c:\temp\test.xml'

$doc = [System.Xml.Linq.XDocument]::Load($filename)
$mainBlock = $doc.Descendants('MainBlock').Foreach([System.Xml.Linq.XElement])[0]
$objects = $mainBlock.Elements().Foreach([System.Xml.Linq.XElement])
$tags = [Linq.Enumerable]::Select($objects, [Func[object,string]] { param($x) $x.Name.LocalName }) | foreach { $_ }

$index = $tags.IndexOf('xObject')

$xObject = $objects[$index]
$xObject.ReplaceWith([System.Xml.Linq.XElement]::new('Object', @($xObject.Attributes(), $xObject.Elements())))

$xObject = $objects[$index + 1]
$xObject.ReplaceWith([System.Xml.Linq.XElement]::new('xObject', @($xObject.Attributes(), $xObject.Elements())))

$doc

Это хорошо подходит для начальных нужд — я надеялся взять это и расширить эту мысль, чтобы динамически сообщать системе об отключении object, когда другой xobject не существует. Если каждый из них имеет формат NO = "##", как мне получить такой уровень детализации с помощью параметров Linq? Я знаю, что с помощью другого метода XML я могу выбрать путь, указав что-то вроде //object[@NO='05' и изменив его - возможно ли использовать этот стиль манипуляции Linq?

k1dfr0std 23.07.2024 21:11

В настоящее время $x.Name.LocalName выдает только его основное имя, но я не могу напрямую увидеть, какое число связано с этим узлом, и количество объектов, хранящихся в этом файле, варьируется. Например, если я write-host содержимое |Foreach { $_ }, I see 5 Objects` со xObject предпоследним (как и ожидалось) - но что, если мне также нужно отключить Object NO 2? Не перебирая индексы - можно ли зацепиться за словоблудие, в котором явно указано NO = "02" ? Тем не менее, это фантастическое начало, и оно работает до тех пор, пока всегда есть ранее отключенный объект.

k1dfr0std 23.07.2024 21:15

В какой-то момент мне, возможно, придется освежить в памяти этот другой API — он работает по-другому, и я просто пытаюсь сдвинуть проект с мертвой точки — спасибо за эти примеры. — Он действительно решает исходную проблему, но необходимо также обнаружить отдельные записи и либо удалить их, либо включить их, невозможно было решить с помощью одного только этого метода, и я не мог понять его достаточно хорошо без дальнейших исследований. Хорошая вещь, тем не менее

k1dfr0std 23.07.2024 23:57

Вы можете получить значения атрибутов с помощью: $xObject.Attribute('NO').Value

jdweng 24.07.2024 11:49

Для поиска вы можете использовать методы PS или методы Linq. $objects.Elements() | Where-Object $_.Attribute('NO').Value -eq '02') или метод Linq: [System.Linq.Enumerable]::Where($objects, [Func[object,bool]]{ param($ x) [строка]$x.Attribute('НЕТ').Value -eq '02'})

jdweng 24.07.2024 11:57

PS написан в Microsoft Net/Core Library. Код, который я представил, взят из XDocument, который представляет собой расширенную библиотеку XML Linq. См. следующее: Learn.microsoft.com/en-us/dotnet/standard/linq/… Я часто тестирую код в Visual Studio с использованием C#, а затем конвертирую его в PS.

jdweng 24.07.2024 17:49
Ответ принят как подходящий
  • Вы ищете способ напрямую переименовать элемент XML, но он не поддерживается в API, связанных с [xml] (System.Xml.XmlDocument), и, если я догадался, также не поддерживается в большинстве, если не все, другие XML API.

  • Это $nodes.Item(4) = $node — это молчаливое отсутствие операции, а не ошибка (как и должно быть, потому что .Item() — это метод, поэтому его нельзя назначить) — это досадная ошибка PowerShell:


Обходные пути:

  • Если вы не против работать с разными API из пространства имен System.Xml.Linq, см. ответ jdweng.

    • Примечание:
      • API-интерфейсы, ориентированные на System.Xml.Linq.XDocument, являются современным преемником API-интерфейсов System.Xml.XmlDocument ([xml]), и с ними, несомненно, проще работать в C#, а для манипуляций с _structural DOM - таких как в этом случае - возможно, также в PowerShell. Однако, в отличие от [xml] , PowerShell не предлагает синтаксического сахара (см. ниже) для System.Xml.Linq.XDocument, поэтому изучение особенностей последних API является обязательным.

      • Напротив, в PowerShell чтение XML DOM, а также внесение неструктурных изменений обычно проще благодаря тесной интеграции PowerShell с API-интерфейсами [xml], что позволяет рассматривать XML DOM как граф объектов с элементами и атрибутами, доступными как свойства ( см. этот ответ для получения дополнительной информации). В простых случаях не требуется никаких специальных знаний API, что позволяет использовать единый подход ООП.

  • Также возможно решение на основе используемых вами API [xml]:

    • Как следует из примечаний выше, структурная модификация немного более громоздка, и для эмуляции переименования вам придется предпринять следующие шаги:

      • Создайте новый элемент с желаемым новым именем.
      • Переместите атрибуты исходного элемента и дочерние элементы в новый элемент.
      • Вставьте новый элемент после исходного.
      • Удалите исходный элемент.
    • Помимо неизбежного использования методов, связанных с [xml] для вышеизложенного, остальная часть кода использует преимущества вышеупомянутой интеграции PowerShell в стиле ООП, также известной как адаптация [xml] DOM для PowerShell.

# Load the XML file into an [xml] (System.Xml.XmlDocument) DOM.
# Note: 
#  * The form below is more robust than [xml] (Get-Content -Path C:\FooBar.xml)
#  * Be sure to use a *full* file path in .NET methods, because .NET's working
#    dir. usually differs from PowerShell's; use Convert-Path with a relative
#    path to get a full one, e.g., Convert-Path FooBar.xml
$fullFilePath = 'C:\FooBar.xml'
($xml = [xml]::new()).Load($fullFilePath)

# Get the target elements by their 'NO' attribute value, 
# both the <xObject> and the <Object> element.
[array] $targetElements = 
  $xml.Configuration.MainBlock.ChildNodes |
  Where-Object NO -EQ '05'

if ($targetElements.Count -eq 0) { throw "No matching element(s) found." }

# Determine the parent element.
$parentElement = $targetElements[0].ParentNode

# Construct the replacement elements.
$replacementElements = 
  $targetElements |
  ForEach-Object {
    # Change 'xObject' to 'Object' and vice versa.
    $newName = if ($_.Name -clike 'x*') { $_.Name.Substring(1) } else { 'x' + $_.Name }
    $replacementElement = $xml.CreateElement($newName, $_.NamespaceURI)
    # Move all attributes from the target node to the replacement...
    $null = foreach ($a in @($_.Attributes)) { $replacementElement.Attributes.Append($a) }
    # ... and all child elements.
    $null = foreach ($e in @($_.ChildNodes)) { $replacementElement.AppendChild($e) }
    # Output the newly constructed replacement element.
    $replacementElement
  } 

# Now insert the replacement elements, each right after its original.
$i = 0
$null = 
  $replacementElements | ForEach-Object { 
    $parentElement.InsertAfter($_, $targetElements[$i++])
  }
# ... and remove the original ones.
$null = $targetElements | ForEach-Object { $parentElement.RemoveChild($_) }

# Save back to the input file.
$xml.Save($fullFilePath)

Ваше решение решает вопрос, который я прокомментировал в ответе jdweng, однако, в отличие от ответа jdweng, где целевой узел размещается в том же порядке, ваш вариант помещает целевой узел в начало Mainblock, а не в строку. Я попытался изменить порядок, упомянутый в ваших комментариях, раскомментировав $replacementElements | ForEach-Object { $parentElement.InsertAfter($_, $targetElements[-1]) }, и закомментировал следующие 2 строки, но обновленный объект снова оказался вверху без измененного значения. Читаю еще раз - думаю надо подставить строку. Пробую это сейчас.

k1dfr0std 23.07.2024 22:18

@k1dfr0std, насколько я могу судить, порядок узлов сохраняется; однако (а) возникла ошибка в случае, если был найден только один целевой узел (поскольку исправлено с помощью ограничения типа [array]), и (б) сохранение порядка основано на предположении, что элементы <Object> и <xObject> являются соседними; код использует тот из двух, который идет последним, в качестве точки вставки для замен.

mklement0 23.07.2024 22:35

ДА - это то, что я только что обнаружил. Если бы я также хотел адаптировать этот код, чтобы также отключить (изменить Object на xObject) и сохранить порядок, нужно ли мне изменить строку $insertAfterElement = $argetElements[-1] на любой индекс $parentElement репрезентативного объекта, который нужно вставить на место при удалении исходного элемента?

k1dfr0std 23.07.2024 22:46

Я понял! ЕСЛИ есть ТОЛЬКО ОДИН Object одного и того же «НЕТ» - это решается изменением $parentElement = $targetElements[0].ParentNode на $parentElement = $targetElements.ParentNode, а затем действительно изменением строки $insertAfterElement = $targetElements[-1] на $insertAfterElement = $targetElements и полным удалением массивов. Я думаю, что это должно быть достаточно просто, чтобы смоделировать условие if — я попробую это дальше.

k1dfr0std 23.07.2024 23:11

@k1dfr0std, ограничение типа [array] устраняет необходимость различать поиск только одного или двух элементов. Пожалуйста, ознакомьтесь с моим обновленным кодом вставки, который, как я полагаю, теперь работает так, как вы хотите.

mklement0 23.07.2024 23:17

Используя ваш обновленный код вставки, я получаю ошибку cannot call method on null-valued expression как в строках вставки, так и в строках удаления, тогда как, если я изменю его, включив If ($targetElements.Count -gt 1) в начало, и укажу [0].ParentNode против .ParentNode для родительского элемента и установлю логическую переменную для ссылки позже для isArray - затем я могу проверить это условие для isArray и в точке вставки изменить $insertAfterElement = . . ., чтобы он был [-1] конкретным или просто единственным $targetElement вариантом.

k1dfr0std 23.07.2024 23:36

@ k1dfr0std, если вы хотите обработать случай, когда нет совпадающих элементов, используйте if ($targetElements.Count -eq 0) { throw "No matching element(s) found." } сразу после установки $targetElements (ответ обновлен). Кроме того, код в ответе не требует специальной обработки, чтобы различать случай нахождения одного или двух совпадающих элементов (или даже более того, каждый из которых может быть превращен <xSomeName> в <SomeName> и наоборот).

mklement0 23.07.2024 23:42

О, Боже мой. . . . Я так слеп. Я поместил ограничение типа [array] в строку $parentNode! ДА! Вы абсолютно правы - это действительно решило проблему. Я установил ограничение [array], как вы упомянули, в строке $targetElements, и вуаля - сработало как часы.

k1dfr0std 23.07.2024 23:54

Рад слышать, что мы докопались до сути, @k1dfr0std.

mklement0 23.07.2024 23:59

Давайте продолжим обсуждение в чате.

k1dfr0std 24.07.2024 03:10

Пересмотрите XSLT, как было предложено выше в комментариях к шаблону. Поскольку XSLT представляет собой XML, читайте и анализируйте таблицу стилей аналогично входному XML. Идя по этому пути, вы избегаете циклов forEach и логики if в PS. Кроме того, XSLT как отраслевой язык является переносимым и может работать вне PowerShell (Perl, Python, Java, Saxon, Xalan и т. д.), используя тот же входной XML.

В частности, ниже XSLT использует шаблон Identity Transform для копирования документа как есть, а затем переименовывает узел <Object> в <xObject>, содержащий атрибут NO = '05'. Потенциально «05» можно параметризовать, если PowerShell передает литерал в таблицу стилей. (Поскольку XSLT 1.0 запрещает использование параметров при сопоставлении с шаблоном, выполняется более подробная условная обработка).

XSLT (сохранить как файл .xsl)

<xsl:stylesheet version = "1.0" xmlns:xsl = "http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration = "yes" indent = "yes"/>
 <xsl:strip-space elements = "*"/>

 <xsl:param name = "no_param"/>

 <xsl:template match = "node()|@*">
   <xsl:copy>
     <xsl:apply-templates select = "node()|@*"/>
   </xsl:copy>
 </xsl:template>

 <xsl:template match = "Object">
   <xsl:choose>
     <xsl:when test = "@NO=$no_param">
       <xObject>
         <xsl:apply-templates select = "node()|@*"/>
       </xObject>
     </xsl:when>
     <xsl:otherwise>
       <Object>
         <xsl:apply-templates select = "node()|@*"/>
       </Object>
     </xsl:otherwise>
   </xsl:choose>
 </xsl:template>

</xsl:stylesheet>

Для процессоров XSLT 2.0 или 3.0 используйте этот более короткий второй шаблон:

<xsl:template match = "Object[@NO=$no_param]">
  <xObject>
     <xsl:apply-templates select = "node()|@*"/>
  </xObject>
</xsl:template>

PowerShell

$xslt = New-Object System.Xml.Xsl.XslCompiledTransform
$argList = New-Object System.Xml.Xsl.XsltArgumentList
$argList.AddParam("no_param", "", "05")

$xslt.Load("C:\Path\To\Style.xsl")
$xslt.Transform("C:\Path\To\Input.xml", "C:\Path\To\Output.xml");

Другие вопросы по теме

Возникает ошибка при попытке отключить конвейер Azure DevOps с помощью Powershell
Как просмотреть все видеофайлы (вид: видео) независимо от того, какое расширение имеют эти файлы (может быть .mkv/.mp4/.mov и т. д.)?
Как преобразовать вывод команды так, чтобы в powershell сохранялись только последние 1000 строк
Get-MgGroupMember по отображаемому имени вместо userID
Привести к сбою конвейера, если в журнале появляется определенное предложение. Найдите подстроку, но игнорируйте определенную подстроку
Родительская ссылка Azure DevOps REST API на существующую ошибку рабочего элемента
Сделать конвейер сбоем, если в журнале появляется определенное предложение
Извлечение имени и адреса электронной почты менеджеров пользователей AD в существующий сценарий
Неизвестное значение в пользовательском объекте PowerShell
Как использовать секреты в задаче сценария Azure DevOps Powershell