Я работаю над изменением 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
Спасибо @YitzhakKhabinsky, но в системе, в которой я работаю, у меня нет шаблона, необходимого для использования XSLT, и он мне не будет предоставлен. Подход, который я должен принять, потребует использования какой-либо прямой модификации XML.
Используйте 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?
В настоящее время $x.Name.LocalName
выдает только его основное имя, но я не могу напрямую увидеть, какое число связано с этим узлом, и количество объектов, хранящихся в этом файле, варьируется. Например, если я write-host
содержимое |Foreach { $_ }, I see 5
Objects` со xObject
предпоследним (как и ожидалось) - но что, если мне также нужно отключить Object
NO 2? Не перебирая индексы - можно ли зацепиться за словоблудие, в котором явно указано NO = "02"
? Тем не менее, это фантастическое начало, и оно работает до тех пор, пока всегда есть ранее отключенный объект.
В какой-то момент мне, возможно, придется освежить в памяти этот другой API — он работает по-другому, и я просто пытаюсь сдвинуть проект с мертвой точки — спасибо за эти примеры. — Он действительно решает исходную проблему, но необходимо также обнаружить отдельные записи и либо удалить их, либо включить их, невозможно было решить с помощью одного только этого метода, и я не мог понять его достаточно хорошо без дальнейших исследований. Хорошая вещь, тем не менее
Вы можете получить значения атрибутов с помощью: $xObject.Attribute('NO').Value
Для поиска вы можете использовать методы 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'})
PS написан в Microsoft Net/Core Library. Код, который я представил, взят из XDocument, который представляет собой расширенную библиотеку XML Linq. См. следующее: Learn.microsoft.com/en-us/dotnet/standard/linq/… Я часто тестирую код в Visual Studio с использованием C#, а затем конвертирую его в PS.
Вы ищете способ напрямую переименовать элемент 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, насколько я могу судить, порядок узлов сохраняется; однако (а) возникла ошибка в случае, если был найден только один целевой узел (поскольку исправлено с помощью ограничения типа [array]
), и (б) сохранение порядка основано на предположении, что элементы <Object>
и <xObject>
являются соседними; код использует тот из двух, который идет последним, в качестве точки вставки для замен.
ДА - это то, что я только что обнаружил. Если бы я также хотел адаптировать этот код, чтобы также отключить (изменить Object
на xObject
) и сохранить порядок, нужно ли мне изменить строку $insertAfterElement = $argetElements[-1]
на любой индекс $parentElement
репрезентативного объекта, который нужно вставить на место при удалении исходного элемента?
Я понял! ЕСЛИ есть ТОЛЬКО ОДИН Object
одного и того же «НЕТ» - это решается изменением $parentElement = $targetElements[0].ParentNode
на $parentElement = $targetElements.ParentNode
, а затем действительно изменением строки $insertAfterElement = $targetElements[-1]
на $insertAfterElement = $targetElements
и полным удалением массивов. Я думаю, что это должно быть достаточно просто, чтобы смоделировать условие if — я попробую это дальше.
@k1dfr0std, ограничение типа [array]
устраняет необходимость различать поиск только одного или двух элементов. Пожалуйста, ознакомьтесь с моим обновленным кодом вставки, который, как я полагаю, теперь работает так, как вы хотите.
Используя ваш обновленный код вставки, я получаю ошибку cannot call method on null-valued expression
как в строках вставки, так и в строках удаления, тогда как, если я изменю его, включив If ($targetElements.Count -gt 1)
в начало, и укажу [0].ParentNode
против .ParentNode
для родительского элемента и установлю логическую переменную для ссылки позже для isArray
- затем я могу проверить это условие для isArray
и в точке вставки изменить $insertAfterElement = . . .
, чтобы он был [-1]
конкретным или просто единственным $targetElement
вариантом.
@ k1dfr0std, если вы хотите обработать случай, когда нет совпадающих элементов, используйте if ($targetElements.Count -eq 0) { throw "No matching element(s) found." }
сразу после установки $targetElements
(ответ обновлен). Кроме того, код в ответе не требует специальной обработки, чтобы различать случай нахождения одного или двух совпадающих элементов (или даже более того, каждый из которых может быть превращен <xSomeName>
в <SomeName>
и наоборот).
О, Боже мой. . . . Я так слеп. Я поместил ограничение типа [array]
в строку $parentNode
! ДА! Вы абсолютно правы - это действительно решило проблему. Я установил ограничение [array]
, как вы упомянули, в строке $targetElements
, и вуаля - сработало как часы.
Рад слышать, что мы докопались до сути, @k1dfr0std.
Давайте продолжим обсуждение в чате.
Пересмотрите 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");
Для таких задач лучше использовать XSLT и просто вызывать этот XSLT из PowerShell.