Как построчно обработать файл в PowerShell как поток

87

Я работаю с некоторыми текстовыми файлами размером несколько гигабайт и хочу обработать их потоком с помощью PowerShell. Это просто: нужно просто проанализировать каждую строку и извлечь некоторые данные, а затем сохранить их в базе данных.

К сожалению, get-content | %{ whatever($_) }похоже, что на этом этапе конвейера в памяти сохраняется весь набор строк. К тому же это на удивление медленно, ведь на то, чтобы все это прочитать, уходит очень много времени.

Итак, мой вопрос состоит из двух частей:

  1. Как я могу заставить его обрабатывать поток построчно и не хранить все в буфере в памяти? Я бы не хотел использовать для этого несколько гигабайт оперативной памяти.
  2. Как заставить его работать быстрее? PowerShell, перебирающий a, get-contentоказывается в 100 раз медленнее, чем сценарий C #.

Я надеюсь, что я здесь что-то делаю глупо, например, упустил -LineBufferSizeпараметр или что-то в этом роде ...

Скоби
источник
9
Для ускорения get-contentустановите -ReadCount равным 512. Обратите внимание, что на этом этапе $ _ в Foreach будет массивом строк.
Кейт Хилл,
1
Тем не менее, я бы согласился с предложением Романа использовать читатель .NET - намного быстрее.
Кейт Хилл
Из любопытства, что будет, если меня не волнует скорость, а только память? Скорее всего, я приму предложение читателя .NET, но мне также интересно узнать, как предотвратить буферизацию всего канала в памяти.
scobi
7
Чтобы свести к минимуму буферизацию, избегайте присвоения результата Get-Contentпеременной, так как это загрузит весь файл в память. По умолчанию в конвейере Get-Contentобрабатывает файл по одной строке за раз. Пока вы не накапливаете результаты или не используете командлет, который накапливается внутри (например, Sort-Object и Group-Object), попадание в память не должно быть слишком большим. Foreach-Object (%) - это безопасный способ обрабатывать каждую строку по одной.
Keith Hill
2
@dwarfsoft, что не имеет никакого смысла. Блок -End запускается только один раз после завершения всей обработки. Вы можете видеть, что если вы попытаетесь использовать, get-content | % -End { }он пожалуется, потому что вы не предоставили блок процесса. Таким образом, он не может использовать -End по умолчанию, он должен использовать -Process по умолчанию. И попытайтесь 1..5 | % -process { } -end { 'q' }увидеть, что конечный блок происходит только один раз, обычное дело gc | % { $_ }не сработает, если бы блок сценария по умолчанию был -End ...
TessellatingHeckler

Ответы:

92

Если вы действительно собираетесь работать с текстовыми файлами размером в несколько гигабайт, не используйте PowerShell. Даже если вы найдете способ прочитать его, более быстрая обработка огромного количества строк в PowerShell все равно будет медленной, и вы не сможете этого избежать. Даже простые циклы дороги, скажем, для 10 миллионов итераций (вполне реально в вашем случае) у нас есть:

# "empty" loop: takes 10 seconds
measure-command { for($i=0; $i -lt 10000000; ++$i) {} }

# "simple" job, just output: takes 20 seconds
measure-command { for($i=0; $i -lt 10000000; ++$i) { $i } }

# "more real job": 107 seconds
measure-command { for($i=0; $i -lt 10000000; ++$i) { $i.ToString() -match '1' } }

ОБНОВЛЕНИЕ: если вам все еще не страшно, попробуйте использовать .NET reader:

$reader = [System.IO.File]::OpenText("my.log")
try {
    for() {
        $line = $reader.ReadLine()
        if ($line -eq $null) { break }
        # process the line
        $line
    }
}
finally {
    $reader.Close()
}

ОБНОВЛЕНИЕ 2

Есть комментарии по поводу возможно лучшего / более короткого кода. В исходном коде нет ничего плохого, forи это не псевдокод. Но более короткий (самый короткий?) Вариант цикла чтения - это

$reader = [System.IO.File]::OpenText("my.log")
while($null -ne ($line = $reader.ReadLine())) {
    $line
}
Роман Кузьмин
источник
3
К вашему сведению, компиляция скриптов в PowerShell V3 немного улучшает ситуацию. Цикл «реальной работы» увеличился со 117 секунд на V2 до 62 секунд на V3, набранных с консоли. Когда я помещаю цикл в сценарий и измеряю выполнение сценария на V3, оно падает до 34 секунд.
Кейт Хилл
Я поместил все три теста в сценарий и получил следующие результаты: V3 Beta: 20/27/83 секунды; V2: 14/21/101. Похоже, что в моем эксперименте V3 был быстрее в тесте 3, но довольно медленнее в первых двух. Что ж, это бета, надеюсь, производительность будет улучшена в RTM.
Роман Кузьмин
почему люди настаивают на использовании такого прерывания в цикле. Почему бы не использовать цикл, который не требует этого и который лучше читается, например, заменив цикл for наdo { $line = $reader.ReadLine(); $line } while ($line -neq $null)
BeowulfNode42
1
ой, это должно быть -ne для не равных. В этом конкретном цикле do..time есть проблема, заключающаяся в том, что будет обработан нуль в конце файла (в данном случае вывод). Чтобы обойти это, вы тоже могли быfor ( $line = $reader.ReadLine(); $line -ne $null; $line = $reader.ReadLine() ) { $line }
BeowulfNode42
4
@ BeowulfNode42, мы можем сделать это еще короче: while($null -ne ($line = $read.ReadLine())) {$line}. Но тема не совсем о таких вещах.
Роман Кузьмин
51

System.IO.File.ReadLines()идеально подходит для этого сценария. Он возвращает все строки файла, но позволяет сразу же начать итерацию строк, что означает, что ему не нужно сохранять все содержимое в памяти.

Требуется .NET 4.0 или выше.

foreach ($line in [System.IO.File]::ReadLines($filename)) {
    # do something with $line
}

http://msdn.microsoft.com/en-us/library/dd383503.aspx

Despertar
источник
6
Необходимо примечание: .NET Framework - поддерживается в версиях 4.5, 4. Таким образом, на некоторых машинах это может не работать в версиях V2 или V1.
Роман Кузьмин
Это дало мне ошибку System.IO.File не существует, но приведенный выше код, написанный Романом, работал у меня
Каньон Колоб
Это было как раз то, что мне было нужно, и его легко было вставить прямо в существующий сценарий PowerShell.
user1751825
5

Если вы хотите использовать обычный PowerShell, ознакомьтесь с приведенным ниже кодом.

$content = Get-Content C:\Users\You\Documents\test.txt
foreach ($line in $content)
{
    Write-Host $line
}
Крис Блайденштейн
источник
16
Это то, от чего OP хотел избавиться, потому что Get-Contentочень медленно работает с большими файлами.
Роман Кузьмин