Powershell out-file -append 在 foreach 循环中使用时会默默地跳过几行

问题描述 投票:0回答:1

我试图通过将标题和每 500 行写入一个新文件以供测试人员使用来提取 200MB csv 文件的代表性样本。我的第一次尝试是故意次优的,但对于 5 分钟的快速破解似乎是有效的,因为我依靠 out-file -append 将与模数条件匹配的每一行添加到网络共享上的目标文件,但我发现我示例文件中的行数略少于预期。重复运行在目标文件中产生的行数略有不同(预计 2014 年,实际范围在 1992-2011 之间)。

我重新编写了脚本,将 foreach 的结果收集到一个变量中,并在最后输出一次。这按预期工作(2014 年行),但对失败的原因感到好奇。我知道它会重复打开/关闭目标文件,但我预计它会报告错误。

这是脚本的原始版本:

$destfile = "\\UNCSHARE\Folder\Export_Sample_$(get-date -Format "yyyyMMdd_HHmmss").txt"

$Original = get-content "\\UNCSHARE\Folder\200MB_Export_20231208_1545.txt"

[int64]$ln = 0
[int64]$SampleCount = 0

foreach ($line in $Original) {
    $ln++
    if ($ln -eq 1 -or $ln % 500 -eq 0) {
        $line | Out-File -FilePath $destfile -Append -ErrorAction Stop
        $SampleCount++
    }
}
write-host $SampleCount

(get-content $destfile).count 

如果我使用本地硬盘驱动器上的位置作为目标文件,则不会发生错误。

我对照第一个版本检查了第二个(正确的输出)版本输出,可以看到丢失的行在整个文件中间隔不规则(例如,丢失的行位于 56,359,368,405,600,700,702,788,854...)。

我在加入 AD 域的 Windows 10 工作站上的 PS Core 7.4.2 中运行此程序。

编辑:我尝试用本机 API 调用替换 cmdlet

#$line | Out-File -FilePath $destfile -Append -ErrorAction Stop -Encoding utf8
[System.IO.File]::AppendAllText($destfile, "$line`n") 

但是我仍然得到了可变数量的缺失行并且没有报告错误。

Edit2:切换到 Windows Powershell 5.1.19041.3803,现在我确实在本机 API 调用中遇到错误(但在 Out-File 中没有)。

Exception calling "AppendAllText" with "2" argument(s): "The process cannot access the file 
'\\UNCSHARE\Export_sample20240424_164810.txt' because it is being used 
by another process."

在我的系统上 Get-Command Out-File 返回

版本 PS版
3.1.0.0 5.1
7.0.0.0 7.4.2

我在新的 shell 会话中再次进行了测试,结果保持一致。 Out-File 不会报告错误,而 [System.IO.File]::AppendAllText 会报告错误,但仅限于 Windows Powershell。

编辑3: 避免该问题的替换代码块(如 @Santiago Squarzon 建议)如下所示:

# Collect the line data in a variable
$Sample = foreach ($line in $Original) {
    $ln++
    if ($ln -eq 1 -or $ln % 500 -eq 0) {
        $line
        $SampleCount++
    }
# Single write to file  
$sample | Out-File -FilePath $destfile
powershell powershell-cmdlet powershell-core
1个回答
0
投票

很难确定此问题的原因,我确实同意 cmdlet

Out-File
和 .NET API
File.AppendAllText
都应该报告写入错误或无法关闭/打开打开的流关于您的连续附加操作。正如我们稍后发现的,在 PowerShell 5.1 (.NET Framework) 中使用 .NET API 确实会报告写入错误,因为该文件已获取句柄(造成这种情况的可能原因可能是先前的循环迭代未能正确关闭附加后的文件流),但是,.NET 8 API (PowerShell 7.4.2) 以及两个版本中的 cmdlet 均无法报告此问题。在这种情况下,我的建议是向 .NET 存储库提出问题:https://github.com/dotnet/runtime/issues 和/或向 PowerShell 存储库提出问题:https://github.com /PowerShell/PowerShell/issues 寻求此问题的答案。

对于问题的解决方案,强烈建议只执行一次此写入操作,而不是追加到文件中导致文件流连续打开和关闭。另外,对于这么大的文件,为了提高效率,我建议您使用

File.ReadLines
而不是
Get-Content

您还可以通过将循环表达式包装在脚本块中来避免将新内容存储在内存中(通过将其分配给变量),这允许在调用时从中进行流式传输,并且还允许将其输出通过管道传输到

Set-Content
Out-File 
如你所愿:

$destfile = "\\UNCSHARE\Folder\Export_Sample_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"

& {
    try {
        $sourcefile = '\\UNCSHARE\Folder\200MB_Export_20231208_1545.txt'
        $reader = [System.IO.File]::ReadLines($sourcefile)
        $null = $reader.MoveNext()
        # output the first line, we can avoid the `-or` condition here
        $reader.Current

        [int64] $ln = 1
        [int64] $SampleCount = 0

        foreach ($line in $reader) {
            if ($ln++ % 500 -eq 0) {
                $line
                $SampleCount++
            }
        }
        Write-Host $SampleCount
    }
    finally {
        # null conditional is pwsh 7 only
        # use `if ($reader) { $reader.Dispose() }`
        # for pwsh 5.1
        ${reader}?.Dispose()
    }
} | Out-File $destfile -ErrorAction Stop

从评论中的反馈来看,似乎

$reader.MoveNext()
进入
$reader.Current
无法获取文件的第一行,建议使用
StreamReader
代替。使用此方法的性能应该与
File.ReadLines
一样好,并且希望更可靠。

$destfile = "\\UNCSHARE\Folder\Export_Sample_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"

& {
    try {
        $sourcefile = '\\UNCSHARE\Folder\200MB_Export_20231208_1545.txt'
        $reader = [System.IO.StreamReader]::new($sourcefile)
        # output the first line, we can avoid the `-or` condition here
        $reader.ReadLine()

        [int64] $ln = 1
        [int64] $SampleCount = 0

        while ($line = $reader.ReadLine()) {
            if ($ln++ % 500 -eq 0) {
                $line
                $SampleCount++
            }
        }
        Write-Host $SampleCount
    }
    finally {
        # null conditional is pwsh 7 only
        # use `if ($reader) { $reader.Dispose() }`
        # for pwsh 5.1
        ${reader}?.Dispose()
    }
} | Out-File $destfile -ErrorAction Stop
© www.soinside.com 2019 - 2024. All rights reserved.