Comment traiter un fichier dans PowerShell ligne par ligne en tant que flux

87

Je travaille avec des fichiers texte de plusieurs gigaoctets et je souhaite effectuer un traitement de flux sur eux à l'aide de PowerShell. C'est des choses simples, il suffit d'analyser chaque ligne et d'extraire des données, puis de les stocker dans une base de données.

Malheureusement, get-content | %{ whatever($_) }semble garder en mémoire l'ensemble des lignes à ce stade du tuyau. C'est aussi étonnamment lent, prenant beaucoup de temps pour tout lire.

Ma question est donc en deux parties:

  1. Comment puis-je faire en sorte qu'il traite le flux ligne par ligne et ne garde pas le tout en mémoire tampon? Je voudrais éviter d'utiliser plusieurs Go de RAM à cette fin.
  2. Comment puis-je le faire fonctionner plus rapidement? L'itération de PowerShell sur un get-contentsemble être 100 fois plus lente qu'un script C #.

J'espère qu'il y a quelque chose de stupide que je fais ici, comme manquer un -LineBufferSizeparamètre ou quelque chose ...

Scobi
la source
9
Pour accélérer get-content, définissez -ReadCount sur 512. Notez qu'à ce stade, $ _ dans Foreach sera un tableau de chaînes.
Keith Hill
1
Pourtant, j'irais avec la suggestion de Roman d'utiliser le lecteur .NET - beaucoup plus rapidement.
Keith Hill
Par curiosité, que se passe-t-il si je ne me soucie pas de la vitesse, mais juste de la mémoire? Très probablement, j'irai avec la suggestion du lecteur .NET, mais je suis également intéressé de savoir comment l'empêcher de mettre en mémoire tampon tout le tube en mémoire.
scobi
7
Pour minimiser la mise en mémoire tampon, évitez d'assigner le résultat de Get-Contentà une variable car cela chargera le fichier entier en mémoire. Par défaut, dans un pipleline, Get-Contenttraite le fichier une ligne à la fois. Tant que vous n'accumulez pas les résultats ou n'utilisez pas une applet de commande qui s'accumule en interne (comme Sort-Object et Group-Object), le coup de mémoire ne devrait pas être trop mauvais. Foreach-Object (%) est un moyen sûr de traiter chaque ligne, une à la fois.
Keith Hill
2
@dwarfsoft cela n'a aucun sens. Le bloc -End ne s'exécute qu'une fois après que tout le traitement est terminé. Vous pouvez voir que si vous essayez d'utiliser, get-content | % -End { }cela se plaint parce que vous n'avez pas fourni de bloc de processus. Il ne peut donc pas utiliser -End par défaut, il doit utiliser -Process par défaut. Et essayez de 1..5 | % -process { } -end { 'q' }voir que le bloc de fin ne se produit qu'une seule fois, l'habituel gc | % { $_ }ne fonctionnerait pas si le scriptblock était par défaut -End ...
TessellatingHeckler

Réponses:

91

Si vous êtes vraiment sur le point de travailler sur des fichiers texte de plusieurs gigaoctets, n'utilisez pas PowerShell. Même si vous trouvez un moyen de le lire plus rapidement, le traitement d'un grand nombre de lignes sera de toute façon lent dans PowerShell et vous ne pouvez pas éviter cela. Même les boucles simples coûtent cher, disons pour 10 millions d'itérations (bien réelles dans votre cas), nous avons:

# "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' } }

MISE À JOUR: Si vous n'avez toujours pas peur, essayez d'utiliser le lecteur .NET:

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

MISE À JOUR 2

Il y a des commentaires sur un code éventuellement meilleur / plus court. Il n'y a rien de mal avec le code d'origine avec foret ce n'est pas un pseudo-code. Mais la variante la plus courte (la plus courte?) De la boucle de lecture est

$reader = [System.IO.File]::OpenText("my.log")
while($null -ne ($line = $reader.ReadLine())) {
    $line
}
Roman Kuzmin
la source
3
Pour info, la compilation de scripts dans PowerShell V3 améliore un peu la situation. La boucle «vrai travail» est passée de 117 secondes sur V2 à 62 secondes sur V3 tapé sur la console. Lorsque je mets la boucle dans un script et que je mesure l'exécution du script sur V3, elle tombe à 34 secondes.
Keith Hill
J'ai mis les trois tests dans un script et j'ai obtenu ces résultats: V3 Beta: 20/27/83 secondes; V2: 14/21/101. Il semble que dans mon expérience, V3 est plus rapide dans le test 3 mais il est assez lent dans les deux premiers. Eh bien, c'est Beta, j'espère que les performances seront améliorées dans RTM.
Roman Kuzmin
pourquoi les gens insistent-ils pour utiliser une pause dans une boucle comme celle-là? Pourquoi ne pas utiliser une boucle qui n'en a pas besoin et qui se lit mieux, comme remplacer la boucle for pardo { $line = $reader.ReadLine(); $line } while ($line -neq $null)
BeowulfNode42
1
oups c'est censé être -ne pour pas égal. Cette boucle do.. while particulière a le problème que le null à la fin du fichier sera traité (dans ce cas, la sortie). Pour contourner cela aussi, vous pourriez avoirfor ( $line = $reader.ReadLine(); $line -ne $null; $line = $reader.ReadLine() ) { $line }
BeowulfNode42
4
@ BeowulfNode42, nous pouvons le faire encore plus court: while($null -ne ($line = $read.ReadLine())) {$line}. Mais le sujet ne concerne pas vraiment de telles choses.
Roman Kuzmin
51

System.IO.File.ReadLines()est parfait pour ce scénario. Il renvoie toutes les lignes d'un fichier, mais vous permet de commencer immédiatement à itérer sur les lignes, ce qui signifie qu'il n'a pas à stocker tout le contenu en mémoire.

Nécessite .NET 4.0 ou supérieur.

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

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

Despertar
la source
6
Une note est nécessaire: .NET Framework - Pris en charge dans: 4.5, 4. Ainsi, cela peut ne pas fonctionner en V2 ou V1 sur certaines machines.
Roman Kuzmin
Cela m'a donné une erreur System.IO.File n'existe pas, mais le code ci-dessus de Roman a fonctionné pour moi
Kolob Canyon
C'était exactement ce dont j'avais besoin, et c'était facile à insérer directement dans un script PowerShell existant.
user1751825
5

Si vous souhaitez utiliser PowerShell directement, consultez le code ci-dessous.

$content = Get-Content C:\Users\You\Documents\test.txt
foreach ($line in $content)
{
    Write-Host $line
}
Chris Blydenstein
la source
16
C'est ce dont l'OP voulait se débarrasser car il Get-Contentest très lent sur les gros fichiers.
Roman Kuzmin