Я пытаюсь извлечь репрезентативную выборку 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 возвращает
Я снова протестировал новые сеансы оболочки, и результаты остались неизменными. 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
Если это проблема с сетью, то вы уже знаете об устранении проблемы, как вы заявили, не открывайте и не закрывайте поток / не используйте -Append
и попробуйте написать всю информацию сразу, а затем проверьте, продолжает ли эта проблема возникать.
Спасибо за комментарии: @jdweng — utf8 не изменил результат.
Спасибо за комментарии: @SantiagoSquarzon — сбор строк в переменных работает, но мне любопытно, почему я не получаю ошибок, если исходящий файл не может успешно записать содержимое в пункт назначения. За пределами этого конкретного примера я надеюсь получить информацию о надежности/устойчивости командлета. Я думаю, что из других инструментов, которые я использовал, у меня могла быть ошибка типа «не удалось открыть доступ для записи».
ну да, я согласен, но у нас, со своей стороны, нет возможности проверить правильность того, что вы говорите... что вы можете попробовать, если считаете, что виноват командлет Out-File
, вот прямой вызов API с использованием [System.IO.File]::AppendAllText($destfile, "$line`n")
и посмотрите, не дает ли API сбой/выдает ошибку. но окончательное решение проблемы — написать только один раз
Обратите внимание, что исходящий файл может смешивать разные кодировки в одном файле.
Спасибо за комментарии: @SantiagoSquarzon — я получаю такое же поведение при прямом вызове API (на этот раз при записи в UNC отсутствует 17 записей, а при записи в C:). Я по-прежнему не получаю сообщений об ошибках, хотя я также добавил $ErrorActionPreference = "stop".
Итак, просматривая обновления, я вижу, что .net framework на самом деле выдает ошибку. Out-File -Append
тоже (в 5.1)?
@SantiagoSquarzon Out-File -append не выдавал ошибку в версии 5.1. Сегодня у меня не хватило времени для тестирования, но я подумал, что мне следует тестировать в новых сеансах -noprofile из оболочек, поскольку мои первоначальные результаты выполнялись в VSCode, и задавался вопросом, можно ли F8 запускать выбранный код (без перезагрузки переменной get-content) не был надежным.
да, я не думаю, что вы сможете получить ответ на этот вопрос, если его не увидит настоящий разработчик команды .net: P это может быть ошибка в .net (ядро), который не сообщает об ошибке при записи через сетевой поток. возможно, вы могли бы открыть проблему с репозиторием .net или pwsh, но, по крайней мере, вы знаете, что решение проблемы состоит в том, чтобы открыть и закрыть поток только один раз (записывая весь контент одновременно)
@SantiagoSquarzon – спасибо за ваши комментарии. Хотите опубликовать свое последнее сообщение в качестве ответа, и я приму его. Я бы также отредактировал свой вопрос, включив в него версию сценария «собери и напиши один раз».
Трудно определить причину этой проблемы, я согласен, что и командлет 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 и не совсем понимаю, как решить эту проблему.
ммм странно, разве $reader = [System.IO.File]::ReadLines($sourcefile)
затем $null = $reader.MoveNext()
и наконец $reader.Current
не выводит заголовки вашего файла? @AutumnalSigh
Когда я просматриваю код в VSCode, он кажется нулевым. Это сработает, если я прокомментирую эти 3 строки и вернусь к if ($ln -eq 1 -or $ln % 500 -eq 0)
, и, похоже, это не окажет заметного влияния на производительность.
@AutumnalSigh, я не уверен, почему это может быть определенно связано с чтением удаленного файла. не могли бы вы попробовать новый фрагмент, используя вместо этого StreamReader
? надеюсь, должно быть надежнее
Да, это работает :) Большое спасибо. Я также бегло взглянул на StreamReader, пока пытался его исправить, но не реализовал версию, использующую его. Я очень благодарен за время, которое вы потратили, помогая мне, и узнали гораздо больше, чем тема моего первоначального вопроса (например, оператор вызова), поэтому я не могу вас отблагодарить.
@AutumnalSigh, рад, что ответ оказался полезным :) кстати, используйте обновленную версию (с while (-not $reader.EndOfStream) {...
)
билет [ссылка] (github.com/PowerShell/PowerShell/issues/21545), созданный на Powershell GitHub
Попробуйте сменить кодировку на utf8