Как построчно обработать файл в 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


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очень медленно работает с большими файлами.
— Роман Кузьмин
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.