Powershell out-file -append автоматически пропускает несколько строк при использовании внутри цикла foreach

Я пытаюсь извлечь репрезентативную выборку CSV-файла размером 200 МБ, записывая заголовок и каждую 500-ю строку в новый файл для использования тестировщиками. Моя первая попытка была заведомо неоптимальной, но казалась подходящей для быстрого 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...).

Я запускаю это в PS Core 7.4.2 на рабочей станции Windows 10, присоединенной к домену AD.

Обновлено: я попытался заменить командлет собственным вызовом API.

#$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

Я снова протестировал новые сеансы оболочки, и результаты остались неизменными. 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

Попробуйте сменить кодировку на utf8

jdweng 24.04.2024 14:44

Если это проблема с сетью, то вы уже знаете об устранении проблемы, как вы заявили, не открывайте и не закрывайте поток / не используйте -Append и попробуйте написать всю информацию сразу, а затем проверьте, продолжает ли эта проблема возникать.

Santiago Squarzon 24.04.2024 14:44

Спасибо за комментарии: @jdweng — utf8 не изменил результат.

Autumnal Sigh 24.04.2024 14:53

Спасибо за комментарии: @SantiagoSquarzon — сбор строк в переменных работает, но мне любопытно, почему я не получаю ошибок, если исходящий файл не может успешно записать содержимое в пункт назначения. За пределами этого конкретного примера я надеюсь получить информацию о надежности/устойчивости командлета. Я думаю, что из других инструментов, которые я использовал, у меня могла быть ошибка типа «не удалось открыть доступ для записи».

Autumnal Sigh 24.04.2024 14:57

ну да, я согласен, но у нас, со своей стороны, нет возможности проверить правильность того, что вы говорите... что вы можете попробовать, если считаете, что виноват командлет Out-File, вот прямой вызов API с использованием [System.IO.File]::AppendAllText($destfile, "$line`n") и посмотрите, не дает ли API сбой/выдает ошибку. но окончательное решение проблемы — написать только один раз

Santiago Squarzon 24.04.2024 15:04

Обратите внимание, что исходящий файл может смешивать разные кодировки в одном файле.

js2010 24.04.2024 16:15

Спасибо за комментарии: @SantiagoSquarzon — я получаю такое же поведение при прямом вызове API (на этот раз при записи в UNC отсутствует 17 записей, а при записи в C:). Я по-прежнему не получаю сообщений об ошибках, хотя я также добавил $ErrorActionPreference = "stop".

Autumnal Sigh 24.04.2024 16:49

Итак, просматривая обновления, я вижу, что .net framework на самом деле выдает ошибку. Out-File -Append тоже (в 5.1)?

Santiago Squarzon 24.04.2024 18:12

@SantiagoSquarzon Out-File -append не выдавал ошибку в версии 5.1. Сегодня у меня не хватило времени для тестирования, но я подумал, что мне следует тестировать в новых сеансах -noprofile из оболочек, поскольку мои первоначальные результаты выполнялись в VSCode, и задавался вопросом, можно ли F8 запускать выбранный код (без перезагрузки переменной get-content) не был надежным.

Autumnal Sigh 24.04.2024 22:12

да, я не думаю, что вы сможете получить ответ на этот вопрос, если его не увидит настоящий разработчик команды .net: P это может быть ошибка в .net (ядро), который не сообщает об ошибке при записи через сетевой поток. возможно, вы могли бы открыть проблему с репозиторием .net или pwsh, но, по крайней мере, вы знаете, что решение проблемы состоит в том, чтобы открыть и закрыть поток только один раз (записывая весь контент одновременно)

Santiago Squarzon 24.04.2024 22:53

@SantiagoSquarzon – спасибо за ваши комментарии. Хотите опубликовать свое последнее сообщение в качестве ответа, и я приму его. Я бы также отредактировал свой вопрос, включив в него версию сценария «собери и напиши один раз».

Autumnal Sigh 25.04.2024 09:00
Стоит ли изучать 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
11
138
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Трудно определить причину этой проблемы, я согласен, что и командлет Out-File, и .NET API File.AppendAllText должны сообщать об ошибке записи или невозможности закрыть/открыть открытый поток на вашем компьютере. последовательные операции добавления. Как мы обнаружили позже, использование .NET API в PowerShell 5.1 (.NET Framework) сообщает об ошибке записи из-за дескриптора, уже использованного для этого файла (вероятной причиной этого может быть то, что предыдущая итерация цикла не смогла правильно закрыть файл). поток файла после добавления), однако API .NET 8 (PowerShell 7.4.2), а также командлет в обеих версиях не сообщают об этой проблеме. Мой совет в этом случае — открыть проблему в репозитории .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 (-not $reader.EndOfStream) {
            $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

Большое спасибо @Santiago за ваш качественный ответ и предложенное улучшение, которое действительно намного быстрее. Единственная проблема, с которой я столкнулся, заключается в том, что она не записывает строку заголовка в выходной файл. Я не играл с ReadLines в PS и не совсем понимаю, как решить эту проблему.

Autumnal Sigh 26.04.2024 00:53

ммм странно, разве $reader = [System.IO.File]::ReadLines($sourcefile) затем $null = $reader.MoveNext() и наконец $reader.Current не выводит заголовки вашего файла? @AutumnalSigh

Santiago Squarzon 26.04.2024 01:00

Когда я просматриваю код в VSCode, он кажется нулевым. Это сработает, если я прокомментирую эти 3 строки и вернусь к if ($ln -eq 1 -or $ln % 500 -eq 0) , и, похоже, это не окажет заметного влияния на производительность.

Autumnal Sigh 26.04.2024 01:19

@AutumnalSigh, я не уверен, почему это может быть определенно связано с чтением удаленного файла. не могли бы вы попробовать новый фрагмент, используя вместо этого StreamReader? надеюсь, должно быть надежнее

Santiago Squarzon 26.04.2024 01:28

Да, это работает :) Большое спасибо. Я также бегло взглянул на StreamReader, пока пытался его исправить, но не реализовал версию, использующую его. Я очень благодарен за время, которое вы потратили, помогая мне, и узнали гораздо больше, чем тема моего первоначального вопроса (например, оператор вызова), поэтому я не могу вас отблагодарить.

Autumnal Sigh 26.04.2024 01:38

@AutumnalSigh, рад, что ответ оказался полезным :) кстати, используйте обновленную версию (с while (-not $reader.EndOfStream) {...)

Santiago Squarzon 26.04.2024 01:39

билет [ссылка] (github.com/PowerShell/PowerShell/issues/21545), созданный на Powershell GitHub

Autumnal Sigh 27.04.2024 15:18

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