【问题标题】:PowerShell - Removing multiple lines of text between delimiters in a text filePowerShell - 删除文本文件中分隔符之间的多行文本
【发布时间】:2020-03-28 09:52:55
【问题描述】:

我编辑 XML 文件并使用 PowerShell 在记事本中打开它们并替换文本字符串。给定两个不同的分隔符,开始和停止,在 XML 文件中出现 多次,我想完全删除分隔符之间的文本(分隔符是否也被删除并不重要给我)。

在下面的示例文本中,我想完全删除开始和结束分隔符之间的文本,但保留它之前和之后的所有文本。

我面临的问题是,每行文本的末尾都有 换行符,这使我无法进行简单的操作:

-replace "<!--A6-->.*?<!--A6 end-->", "KEVIN"

起始分隔符:

<!--A6-->

停止分隔符:

<!--A6 end-->

示例文本:

<listItem>
<para>Apple iPhone 6</para>
</listItem>
<listItem>
<para>Apple iPhone 8</para>
</listItem>
<!--A6-->
<listItem>
<para>Apple iPhone X</para>
</listItem>
<!--A6 end-->
</randomList></para>
</levelledPara>
<levelledPara>
<!--A6-->
<title>Available Apple iPhone Colors</title>
<para>The current iPhone model is available in
the follow colors.  You can purchase this model
in store, or online.</para>
<!--A6 end-->
<para>If the color option that you want is out
of stock, you can find them at the following
website link.</para>

当前代码:

$Directory = "C:\Users\hellokevin\Desktop\PSTest"

$FindBook = "Book"

$ReplaceBook = "Novel"

$FindBike = "Bike"

$ReplaceBike = "Bicycle"

Get-ChildItem -Path $Directory -Recurse |
    Select-Object -Expand FullName|
        ForEach-Object {
            (Get-Content $_) -replace $FindBook,$ReplaceBook -replace "<!--A6-->.*?<!--A6 end-->", "KEVIN" |
            Set-Content ($_ + "_new.xml")
        }

任何帮助将不胜感激。作为 PowerShell 的新手,我不知道如何在代码中的每一行末尾考虑换行符。感谢收看!

【问题讨论】:

  • 做最明智的方式&lt;!--A6--&gt;[\S\s]*?&lt;!--A6 end--&gt;

标签: regex xml powershell replace


【解决方案1】:

在 XML 文件上使用搜索和替换是非常不可取的,应该不惜一切代价避免,因为这种方式太容易损坏 XML。

有更好的修改 XML 的方法,它们都遵循这个模式:

  • 加载 XML 文档
  • 修改文档树
  • 将 XML 文档写回到文件中。

对于您的情况(“删除标记之间的节点”),可能如下:

  • 加载 XML 文档
  • 按文档顺序查看所有 XML 节点
  • 当我们看到“A6”的评论时,设置一个标志以从现在开始删除节点
  • 当我们看到“A6 结束”的评论时,取消设置该标志
  • 收集所有应删除的节点(在标志打开时出现)
  • 在最后一步中,删除它们
  • 将 XML 文档写回到文件中。

以下程序将完全做到这一点(并且还删除“A6”cmets 本身):

$doc = New-Object xml
$doc.Load("C:\path\to\your.xml")

$toRemove = @()
$A6flag = $false
foreach ($node in $doc.SelectNodes('//node()')) {
    if ($node.NodeType -eq "Comment") {
        if ($node.Value -eq 'A6') {
            $A6flag = $true
            $toRemove += $node
        } elseif ($node.Value -eq 'A6 end') {
            $A6flag = $false
            $toRemove += $node
        }
    } elseif ($A6flag) {
        $toRemove += $node
    }
}
foreach ($node in $toRemove) {
    [void]$node.ParentNode.RemoveChild($node)
}

$doc.Save("C:\path\to\your_modified.xml")

您也可以在 foreach 循环内进行字符串替换:

if ($node.NodeType -eq "Text") {
    $node.Value = $node.Value -replace "Apple","APPLE"
}

在单个$node.Value 上执行-replace 是安全的。对整个 XML 执行 -replace 不是。

【讨论】:

  • &lt;!--A6--&gt;&lt;!--A6 end--&gt; 不太可能出现在 CDATA 块或隐藏的内容部分中,并且在另一个评论片段中没有意义。鉴于此,正则表达式方式更快。
  • 有趣。我不知道 XML 文档是如此不稳定,简单的文本删除可能会导致这么大的问题。我会花一些时间研究你的程序来理解它。谢谢托马莱克。
  • 他们是。在手工制作的实验室场景之外,将 XML 文件视为纯文本会很快给您带来麻烦。 XML 文档是树形结构,它们必须遵循的规则众多且复杂。唯一明智的选择是加载树,对其进行修改,然后再次将其保存到文件中。其他所有方法迟早都会失败。
【解决方案2】:

注意:

  • 一般来说,为了进行稳健的处理,您应该使用专用的 XML 解析器来解析 XML 文本。

  • 在手头的特定情况下,使用 regex 是一种方便的快捷方式,但需要注意的是它只有效,因为被移除的行块是独立的元素或元素序列;如果这个假设不成立,修改将使 XML 文档无效。

    • 此外,可能存在字符编码问题,因为读取 XML 文件作为文本不遵守显式encoding 属性可能存在于文件的 XML 声明中 - 有关详细信息,请参阅底部部分。

    • 也就是说,以下技术适用于修改没有特定正式结构的纯文本文件


  • 您需要使用s (SingleLine) regex option 以确保. 也匹配换行符 - 如果内联使用此类选项,则必须放在(?...) 内在正则表达式的开头;也就是说,在这种情况下,'(?s)...'

    • 特设,您也可以按照x15 的建议使用解决方法[\s\S] 而不是.;此表达式匹配任何作为空白字符的字符。或非空白字符,因此匹配任何字符,包括换行符。
  • 要完全删除感兴趣的行,您还必须匹配前面和后面的换行符

(Get-Content -Raw file.xml) -replace '(?s)\r?\n<!--A6-->.*?<!--A6 end-->\r?\n'
  • Get-Content -Raw file.xml 将文件读入内存作为一个整体(单个字符串)。

    • Get-Content 在没有 BOM 的情况下对文件的字符编码做出假设:Windows PowerShell 采用 ANSI 编码,而 PowerShell [Core] v6+ 现在明智地采用 UTF-8。由于Get-Content 是一个通用的文本文件读取cmdlet,它知道XML 输入文件的XML 声明中潜在的encoding 属性(例如,
      &lt;?xml version="1.0" encoding="ISO-8859-1"?&gt; )
    • 同样,Set-Content 在 Windows PowerShell 和 BOM-less UTF-8 PowerShell [Core] v6+ 中默认为 ANSI。
    • 如有疑问,请使用-Encoding 参数,同时使用Get-ContentSet-Content
    • 有关详细信息,请参阅底部部分。
  • \r?\n 匹配 Windows 样式的 CRLF 换行符和 Unix 样式的 LF-only 换行符。

  • 如果不能保证换行符在感兴趣的行之前/之后,则使用(?:\r?\n)? 而不是\r?\n

要验证生成的字符串是否仍然是有效的 XML 文档,只需将命令(或其捕获的结果)转换为 [xml][xml] ((Get-Content ...) -replace ...)

如果发现文档损坏,请使用Tomalak's fully robust, but more complex XML-parsing answer


XML 文件和字符编码:

如果您使用Get-Content 读取 XML 文件作为文本,并且该文件既没有 UTF-8 BOM 也没有 UTF-16 / UTF-32 BOM,Get-Content 会生成一个假设:它在 Windows PowerShell 中采用 ANSI 编码(例如,Windows-1252),更明智的是,在 PowerShell [Core] v6+ 中采用 UTF-8 编码。由于 Get-Content 是一个通用的文本文件读取 cmdlet,它知道 XML 输入文件的 XML 声明中潜在的 encoding 属性

  • 如果知道实际编码,使用-Encoding参数指定。

  • 使用具有相同值的 -Encoding 稍后保存带有 Set-Content 的文件:与 PowerShell 中的通常情况一样,一旦数据已通过文件读取 cmdlet 加载到内存中,否保留有关其原始编码的信息,并且稍后使用诸如Set-Content 之类的文件写入cmdlet 使用其固定的默认编码,这又是Windows PowerShell 中的ANSI 和BOM- PowerShell [Core] v6+ 中的 UTF-8 更少。请注意,不幸的是,不同的 cmdlet 在 Windows PowerShell 中具有不同的默认值,而 PowerShell [Core] v6+ 值得称道的是始终默认为 UTF-8。

System.Xml.XmlDocument .NET 类型(其 PowerShell 类型加速器为 [xml])提供强大的 XML 解析,并使用其 .Load().Save() 方法提供更好的编码支持如果文档的 XML 声明包含显式的 encoding 属性 命名使用的编码:

  • 如果存在这样的属性(例如,&lt;?xml version="1.0" encoding="ISO-8859-1"?&gt;),.Load().Save() 都会使用它。

    • 即具有encoding 属性的输入文件将被正确读取,并以相同的编码保存。
    • 当然,这假定encoding 属性中指定的编码反映的是输入文件的实际编码。
  • 否则,如果文件没有 BOM,则 (BOM-less) UTF-8 被假定,与 PowerShell [Core] v6+ 的 Get-Content / @987654369 一样@ - 这是明智的,因为根据 W3C XML Recommendation,既没有 encoding 属性也没有 UTF-8 或 UTF-16 BOM 的 XML 文档应该默认为 UTF-8;如果文件确实有 BOM,则只允许使用 UTF-8 和 UTF-16而无需在 encoding 属性中命名编码,尽管在​​实践中 XmlDocument还可以正确读取带有 BOM 的 UTF-32 文件。

    • 这意味着.Save()不会保留没有@987654374 的(带BOM)UTF-16 或UTF-32 文件的编码 @attribute,而是创建一个无 BOM 的 UTF-8 文件。

    • 如果你想检测一个文件的实际编码 - 从它的 BOM 推断/没有它,或者,如果存在,encoding 属性,通过XmlTextReader 实例读取你的文件:

      # Create an XML reader.
      $xmlReader = [System.Xml.XmlTextReader]::new(
        "$pwd/some.xml" # IMPORTANT: use a FULL PATH
      )
      
      # Read past the declaration, which detects the encoding,
      # whether via the presence / absence of a BOM or an explicit
      # `encoding` attribute.
      $null = $xmlReader.MoveToContent()
      
      # Report the detected encoding.
      $xmlReader.Encoding
      
      # You can now pass the reader to .Load(), if needed
      # See next section for how to *save* with the detected encoding.
      $xmlDoc = [xml]::new()
      $xmlDoc.Load($xmlReader)
      $xmlReader.Close()
      
    • 如果给定文件不兼容并且您知道使用的实际编码和/或您想使用给定编码保存(请确保它不t 与encoding 属性相矛盾,如果有的话),您可以显式指定编码(相当于将-EncodingGet-Content / Set-Content 一起使用),使用.Load() / .Save() 方法重载接受Stream 实例,通过使用给定编码构造的 StreamReader / StreamWriter 实例;例如:

      # Get the encoding to use, matching the input file's.
      # E.g., if the input file is ISO-8859-1-encoded, but lacks
      # an `encoding` attribute in the XML declaration.
      $enc = [System.Text.Encoding]::GetEncoding('ISO-8859-1')
      
      # Create a System.Xml.XmlDocument instance.
      $xmlDoc = [xml]::new()
      # Create a stream reader for the input XML file
      # with explicit encoding.
      $streamIn = [System.IO.StreamReader]::new(
        "$pwd/some.xml", # IMPORTANT: use a FULL PATH
        $enc
      )
      # Read and parse the file.
      $xmlDoc.Load($streamIn)
      # Close the stream
      $streamIn.Close()
      
      # ... process the XML DOM.
      
      # Create a stream *writer* for saving back to the file
      # with the same encoding.
      $streamOut = [System.IO.StreamWriter]::new(
        "$pwd/t.xml", # IMPORTANT: use a FULL PATH
        $false, # don't append
        $enc    # same encoding as above in this case.
      )
      
      # Save the XML DOM to the file.
      $xmlDoc.Save($streamOut)
      # Close the stream
      $streamOut.Close()
      

将文件路径传递给 .NET 方法的一般警告:始终使用完整路径,因为 .NET 对当前目录的想法通常与 PowerShell 不同。

【讨论】:

  • 编码,如果未在 XML 声明中定义,则假定为 UTF-8,所有其他情况都需要 XML 声明,这是 XML 序列化程序会知道的。这就是它的优雅之处,如果您“使用 XML 解析器与序列化的 XML 交互,编码问题就完全被抽象掉了。每个解析器都可以读取任何编码,并为它透明地配置自己。人们接受“方便”的捷径是引入问题的捷径。我很抱歉成为这样的 PITA,但看到到处都做错了这件事让我感到很痛苦。;)
  • 哇,这是相当重要的补充。有多少免责声明和警告以及最终需要多少额外的解释和代码来手动解决处理 XML 文件编码的完全解决的问题,这具有一定的讽刺意味。 (并且具有一定讽刺意味的是,OP 接受了这个答案,因为他们开始使用最糟糕的 XML 处理方式 - 正则表达式 - 并且对正则表达式 都不太了解> XML 以了解其中的任何含义。)
  • @Tomalak:确实想要使用 XML API 的人也很感兴趣。答案中肯定有足够的警告和足够的信息来做出明智的决定关于您是否要使用快捷方式,以及由于通用标题(“在文本文件中”而来到这里的用户"),该方法是合适的。简而言之:答案是这样的:你可能不想这样做,因为这种方法很脆弱,但如果你知道自己在做什么,这里就是如何去做。我已经删除了我以前的 cmets,因为我觉得答案现在说明了完整的故事(顺便说一句,+1)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-02-09
  • 2019-05-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-05-15
  • 1970-01-01
相关资源
最近更新 更多