如何在 Powershell 中一次替换多个 .txt 文件中的数据?

Posted

技术标签:

【中文标题】如何在 Powershell 中一次替换多个 .txt 文件中的数据?【英文标题】:How can i replace data in multiple .txt files at once in Powershell? 【发布时间】:2021-05-17 18:49:15 【问题描述】:

我在一个文件夹中有多个 .txt 文件,它们具有特定的数据格式,其中每一行都从 3 位数字开始,例如。

101,3333,35899,BufferC1,99,02,333
102,3344,30079,BufferD2,89,03,444
.... and so on.

现在,如果记录以“101”开头,那么我必须用一些文本替换同一行的第 3 和第 5 个索引元素。替换所有文件后,我必须将所有修改后的文件复制到同一台机器上的另一个目录/文件夹中。

我写了一些不工作的代码。请帮助我,因为我是 powershell 的新手。我的方法是否正确?

 $FileNamesList = @(Get-ChildItem C:\TestFiles\*.txt | Select-Object -ExpandProperty Name)
    
     for($i=0; $i -lt $FileNamesList.count; $i++)
              
            # original folder path
            $FilePath1 = 'C:\TestFiles\' + $FileNamesList[$i]
            
            #modified files after text is replaced will be copied in another folder
            $FilePath2 = 'C:\TestFilesModified\' + $FileNamesList[$i]   
    
            $OriginalFileData = Get-Content $FilePath1
    
        # Is this correct, can i assign foreach cmd result to a variable?
        $ModifiedFileData= ForEach($Row in $OriginalFileData ) 
                                     
                         If($Row.Split(",")[0] -eq "101") 
                         
                            $Row -replace($Row.Split(",")[3]),"Test File"
                            $Row -replace($Row.Split(",")[5]),"Test Data"
                          

                     else
                       $Row
                        

             Out-File -FilePath $FilePath2 -InputObject $ModifiedFileData
            

【问题讨论】:

所有文件的列数是否相同?他们有标题行,还是只有数据? @ Mathias R. Jessen,这些只是普通的 .txt 文件,没有标题,只有数据。此外,这些文件没有相同的列数。我只需要更改第一个元素中以“101”开头的行。 【参考方案1】:

我喜欢@Theo's approach。我开始沿着同样的思路思考并编写类似的代码。但是,我对 CSV 的想法很感兴趣,最终找到了一些可供分享的替代方法。

CSV 方法存在一些问题:

    缺少标题 难以重新导出,因为 Export-Csv 没有 -NoHeader 参数 如 cmets 中所述处理可变数量的列

我的第一个方法是使用ConvertFrom-String cmdlet 从原始数据中获取正式对象。

$SourceFolder      = "C:\Temp\SourceFolder"
$DestinationFolder = "C:\Temp\destination"

ForEach( $File in Get-ChildItem $SourceFolder -Filter *.txt )

    $DestinationFile = Join-Path -Path $DestinationFolder -ChildPath $File.Name
    
    $File | 
    Get-Content | 
    ConvertFrom-String -Delimiter "," | 
    ForEach-Object
        If( $_.P1 -eq 101 ) 
            $_.P3 = "P3 Replacement Value"
            $_.P5 = "P5 Replacement Value"
        
        $_.PSObject.Properties.Value -join "," # Output from the loop
     |
    Set-Content -Path $DestinationFile

这样做的好处是您不需要知道任何 1 个文件甚至给定文件的任何 1 行中有多少字段。 ConvertFrom-String 只会为给定的行添加额外的属性。使用 $_.PSObject.Properties.Value 展开值还允许使用有限代码的任意数量的属性。

第二种方法需要知道最大列数。出于示例的目的,假设我们有可变数量的列,但最多只能说 8 个。我们可以将 -Header 参数与 Import-Csv 命令一起使用。

$Header            = "P1","P2","P3","P4","P5","P6","P7","P8"
$SourceFolder      = "C:\Temp\SourceFolder"
$DestinationFolder = "C:\Temp\destination"

ForEach( $File in Get-ChildItem C:\temp\SourceFolder -filter *.txt )

    $DestinationFile = Join-Path -Path $DestinationFolder -ChildPath $File.Name
    
    Import-Csv -Path $File.FullName -Header $Header |
    ForEach-Object
        If( $_.P1 -eq 101 ) 
            $_.P3 = "P3 Replacement Value"
            $_.P5 = "P5 Replacement Value"
        
        ($_.PSObject.Properties.Value -join ",").TrimEnd(",") # Output from the loop
     |
    Set-Content -Path $DestinationFile

注意:我想即使您不知道可以将大数组分配给 $Header 变量的最大字段数。

注意:.TrimEnd(",") 方法。由于给定行上的字段可能少于$Header 数组中的元素,Import-Csv 将添加一个属性并为其分配一个空值。反过来,这可能会导致-join 产生的字符串中出现额外的逗号。

警告:如果有合法的空尾随字段,这也可能会造成问题。

最后,我确实找到了一种进一步利用 *-Csv cmdlet 的方法,但它仅适用于 ConvertFrom-String 示例:

$SourceFolder      = "C:\Temp\SourceFolder"
$DestinationFolder = "C:\Temp\destination"

ForEach( $File in Get-ChildItem $SourceFolder -Filter *.txt )

    $DestinationFile = Join-Path -Path $DestinationFolder -ChildPath $File.Name
    
    $File | 
    Get-Content | 
    ConvertFrom-String -Delimiter "," | 
    ForEach-Object
        If( $_.P1 -eq 101 ) 
            $_.P3 = "P3 Replacement Value"
            $_.P5 = "P5 Replacement Value"
        
        $_
     |
    ConvertTo-Csv -NoTypeInformation |
    Select-Object -Skip 1 |
    Set-Content -Path $DestinationFile

这不需要-join 表达式,但是它以额外的Select-Object 管道为代价。

【讨论】:

感谢您提供非常详细的答案。代码中的P1、P3、P5是什么。我使用的方法是在 Array 中转换 Get-Content,然后通过 Index $Array[0]、$Array[3] ... 等引用元素? 此外,在输出文件夹中,新文件内容会不断地一次又一次地附加到同一个文件中。如果我想运行脚本并希望文件相同但每次都应使用最新的脚本运行内容完全修改整个文件内容,因为我每次都会在文件中显示 Get-Date,这可能吗? 嗨史蒂文,我面临一个问题。现在,在修改后的文件中,开始的 0 被截断。就像该行从 023,3333,4444,Test3....an 开始一样,然后在修改后的文件中我看到:23,3333,4444,Test3 P1...P3...P5 是由ConvertFrom-String cmdlet 生成的通用属性。在*-csv 示例中,P 是因为我给它的标题名称。您可以更改这些以更好地表示您的数据。一旦它被转换为一个对象,我们将引用属性名称而不是数组元素。 csv 示例不会截断。但是,那里也存在潜在的问题。他们已经在答案中注明。请注意:虽然这是一个有趣的思考练习,但@theo 的回答是完全有效的,并且在您不必在预先存在的 cmdlet 中寻找警告的意义上更方便。您可能应该在接受该答案的同时追求这些样本或 som 混合。【参考方案2】:

很遗憾,这些文件没有标题,这将使它们成为正确的 Csv 文件,并且使用起来更加可靠..

您现在可以做的是逐行遍历这些文件并在分隔符 , 处拆分。

编辑

代码现在使用正则表达式来分割逗号上的每个字符串,除非这个逗号在带引号的字段内。

# create a regex string to split on the delimiter character ',' unless it isinside quotes
$commaUnlessQuoted = ',(?=(?:[^"]*"[^"]*")*[^"]*$)'
Get-ChildItem -Path 'D:\Test'-Filter '*.txt' -File | ForEach-Object 
    $file = $_.FullName
    $data = switch -Regex -File $file 
        '^101,' 
            # first field is '101', replace element index 3 with "Test File" and element index 5 with "Test Data"
            $fields = $_ -split $commaUnlessQuoted
            $fields[3] = "Test File"
            $fields[5] = "Test Data"
            # rejoin the fields with a comma
            $fields -join ','
        
        '^002,' 
            # first field is '002', replace element index 3 with the current date
            $fields = $_ -split $commaUnlessQuoted
            $fields[3] = (Get-Date).ToLongDateString()
            $fields -join ','
        
        # you can add as many regex conditions here as you like.
        # default means no conditions above matched, so return te line as-is
        default $_
    
    $data | Set-Content -Path $file -Force
    # copy the modified file to somewhere else
    Copy-Item -Path $file -Destination 'C:\TestFilesModified'

$commaUnlessQuoted 的正则表达式详细信息:

,               Match the character “,” literally
(?=             Assert that the regex below can be matched, starting at this position (positive lookahead)
   (?:          Match the regular expression below
      [^"]      Match any character that is NOT a “"”
         *      Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
      "         Match the character “"” literally
      [^"]      Match any character that is NOT a “"”
         *      Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
      "         Match the character “"” literally
   )*           Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
   [^"]         Match any character that is NOT a “"”
      *         Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
   $            Assert position at the end of the string (or before the line break at the end of the string, if any)
)

根据您对this question 的评论,如果您还需要抛出非终止异常,只需更改以下几行:

$data | Set-Content -Path $file -Force
# copy the modified file to somewhere else
Copy-Item -Path $file -Destination 'C:\TestFilesModified'

进入

try 
    $data | Set-Content -Path $file -Force -ErrorAction Stop
    # copy the modified file to somewhere else
    Copy-Item -Path $file -Destination 'C:\TestFilesModified' -ErrorAction Stop

catch  throw   # just rethrow the exception so it 'bubbles up' to the calling script

【讨论】:

感谢您的帮助!这是一个非常好的方法。 @nick235 这也是最快的方法,而且.. ConvertFrom-String 也有它的错误,您可以在 this question 中看到 @nick235 我已编辑 -split 以使用正则表达式,以确保代码不会在引号字段内的逗号上拆分。 1.如果我需要替换其他行中的数据,比如第一个元素是 ^002,那么我想要 $_.P4 = Get-Date。我可以在代码中使用 Elseif 吗? 2.如果其中一行有空元素,那么脚本会失败,例如; 002,3333,4444,Test2,ccd3,,,,,210215。有空元素,我仍然想更新 P4。 3.如果我有多个记录,例如2000行以“01”开头,那我该如何实现? 你的答案是最好的;0

以上是关于如何在 Powershell 中一次替换多个 .txt 文件中的数据?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用java代码在单个数据库中一次连接多个模式

在 Python 中一次更改多个元素

如何在java中一次查找和替换一个单词?

如何在 Firestore 中一次创建/更新多个文档

如何在 Eclipse 中一次显示 100 多个任务 //TODO?

如何在 C# 中一次播放多个声音