Comment diviser un fichier texte à l’aide de PowerShell?

Je dois diviser un gros fichier texte (500 Mo) (un fichier d’exception log4net) en morceaux faciles à gérer, comme 100 fichiers de 5 Mo, ce serait bien.

Je pense que cela devrait être une promenade dans le parc pour PowerShell. Comment puis-je le faire?

C’est une tâche assez simple pour PowerShell, compliquée par le fait que l’applet de commande Get-Content standard ne gère pas trop de très gros fichiers. Ce que je suggère de faire est d’utiliser la classe .NET StreamReader pour lire le fichier ligne par ligne dans votre script PowerShell et utiliser l’applet de commande Add-Content pour écrire chaque ligne dans un fichier avec un index croissant dans le nom du fichier. Quelque chose comme ça:

 $upperBound = 50MB # calculated by Powershell $ext = "log" $rootName = "log_" $reader = new-object System.IO.StreamReader("C:\Exceptions.log") $count = 1 $fileName = "{0}{1}.{2}" -f ($rootName, $count, $ext) while(($line = $reader.ReadLine()) -ne $null) { Add-Content -path $fileName -value $line if((Get-ChildItem -path $fileName).Length -ge $upperBound) { ++$count $fileName = "{0}{1}.{2}" -f ($rootName, $count, $ext) } } $reader.Close() 

Un mot d’avertissement à propos de certaines des réponses existantes – elles fonctionneront très lentement pour les très gros fichiers. Pour un fichier journal de 1,6 Go, j’ai abandonné après quelques heures, réalisant qu’il ne se terminerait pas avant mon retour au travail le lendemain.

Deux problèmes: l’appel à Add-Content s’ouvre, recherche et ferme le fichier de destination actuel pour chaque ligne du fichier source. La lecture d’un peu du fichier source à chaque fois et la recherche des nouvelles lignes ralentissent également les choses, mais je pense que Add-Content est le principal responsable.

La variante suivante produit des résultats légèrement moins agréables: elle divisera les fichiers au milieu des lignes, mais elle divise mon journal de 1,6 Go en moins d’une minute:

 $from = "C:\temp\large_log.txt" $rootName = "C:\temp\large_log_chunk" $ext = "txt" $upperBound = 100MB $fromFile = [io.file]::OpenRead($from) $buff = new-object byte[] $upperBound $count = $idx = 0 try { do { "Reading $upperBound" $count = $fromFile.Read($buff, 0, $buff.Length) if ($count -gt 0) { $to = "{0}.{1}.{2}" -f ($rootName, $idx, $ext) $toFile = [io.file]::OpenWrite($to) try { "Writing $count to $to" $tofile.Write($buff, 0, $count) } finally { $tofile.Close() } } $idx ++ } while ($count -gt 0) } finally { $fromFile.Close() } 

Simple one-liner à diviser en fonction du nombre de lignes (100 dans ce cas):

 $i=0; Get-Content .....log -ReadCount 100 | %{$i++; $_ | Out-File out_$i.txt} 

Identique à toutes les réponses ici, mais en utilisant StreamReader / StreamWriter pour diviser sur de nouvelles lignes (ligne par ligne, au lieu d’essayer de lire tout le fichier en mémoire en même temps). Cette approche peut diviser les gros fichiers de la manière la plus rapide que je connaisse.

Note: je fais très peu de vérification des erreurs, donc je ne peux pas garantir que cela fonctionnera sans problème pour votre cas. Il l’a fait pour moi ( fichier TXT de 1,7 Go de 4 millions de lignes divisé en 100 000 lignes par fichier en 95 secondes ).

 #split test $sw = new-object System.Diagnostics.Stopwatch $sw.Start() $filename = "C:\Users\Vincent\Desktop\test.txt" $rootName = "C:\Users\Vincent\Desktop\result" $ext = ".txt" $linesperFile = 100000#100k $filecount = 1 $reader = $null try{ $reader = [io.file]::OpenText($filename) try{ "Creating file number $filecount" $writer = [io.file]::CreateText("{0}{1}.{2}" -f ($rootName,$filecount.ToSsortingng("000"),$ext)) $filecount++ $linecount = 0 while($reader.EndOfStream -ne $true) { "Reading $linesperFile" while( ($linecount -lt $linesperFile) -and ($reader.EndOfStream -ne $true)){ $writer.WriteLine($reader.ReadLine()); $linecount++ } if($reader.EndOfStream -ne $true) { "Closing file" $writer.Dispose(); "Creating file number $filecount" $writer = [io.file]::CreateText("{0}{1}.{2}" -f ($rootName,$filecount.ToSsortingng("000"),$ext)) $filecount++ $linecount = 0 } } } finally { $writer.Dispose(); } } finally { $reader.Dispose(); } $sw.Stop() Write-Host "Split complete in " $sw.Elapsed.TotalSeconds "seconds" 

Sortie fractionnant un fichier de 1,7 Go:

 ... Creating file number 45 Reading 100000 Closing file Creating file number 46 Reading 100000 Closing file Creating file number 47 Reading 100000 Closing file Creating file number 48 Reading 100000 Split complete in 95.6308289 seconds 

J’ai souvent besoin de faire la même chose. L’astuce consiste à obtenir l’en-tête répété dans chacun des morceaux divisés. J’ai écrit l’applet de commande suivante (PowerShell v2 CTP 3) et il fait l’affaire.

 ############################################################################## #.SYNOPSIS # Breaks a text file into multiple text files in a destination, where each # file contains a maximum number of lines. # #.DESCRIPTION # When working with files that have a header, it is often desirable to have # the header information repeated in all of the split files. Split-File # supports this functionality with the -rc (RepeatCount) parameter. # #.PARAMETER Path # Specifies the path to an item. Wildcards are permitted. # #.PARAMETER LiteralPath # Specifies the path to an item. Unlike Path, the value of LiteralPath is # used exactly as it is typed. No characters are interpreted as wildcards. # If the path includes escape characters, enclose it in single quotation marks. # Single quotation marks tell Windows PowerShell not to interpret any # characters as escape sequences. # #.PARAMETER Destination # (Or -d) The location in which to place the chunked output files. # #.PARAMETER Count # (Or -c) The maximum number of lines in each file. # #.PARAMETER RepeatCount # (Or -rc) Specifies the number of "header" lines from the input file that will # be repeated in each output file. Typically this is 0 or 1 but it can be any # number of lines. # #.EXAMPLE # Split-File bigfile.csv 3000 -rc 1 # #.LINK # Out-TempFile ############################################################################## function Split-File { [CmdletBinding(DefaultParameterSetName='Path')] param( [Parameter(ParameterSetName='Path', Position=1, Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [Ssortingng[]]$Path, [Alias("PSPath")] [Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] [Ssortingng[]]$LiteralPath, [Alias('c')] [Parameter(Position=2,Mandatory=$true)] [Int32]$Count, [Alias('d')] [Parameter(Position=3)] [Ssortingng]$Destination='.', [Alias('rc')] [Parameter()] [Int32]$RepeatCount ) process { # yeah! the cmdlet supports wildcards if ($LiteralPath) { $ResolveArgs = @{LiteralPath=$LiteralPath} } elseif ($Path) { $ResolveArgs = @{Path=$Path} } Resolve-Path @ResolveArgs | %{ $InputName = [IO.Path]::GetFileNameWithoutExtension($_) $InputExt = [IO.Path]::GetExtension($_) if ($RepeatCount) { $Header = Get-Content $_ -TotalCount:$RepeatCount } # get the input file in manageable chunks $Part = 1 Get-Content $_ -ReadCount:$Count | %{ # make an output filename with a suffix $OutputFile = Join-Path $Destination ('{0}-{1:0000}{2}' -f ($InputName,$Part,$InputExt)) # In the first iteration the header will be # copied to the output file as usual # on subsequent iterations we have to do it if ($RepeatCount -and $Part -gt 1) { Set-Content $OutputFile $Header } # write this chunk to the output file Write-Host "Writing $OutputFile" Add-Content $OutputFile $_ $Part += 1 } } } } 

J’ai trouvé cette question en essayant de séparer plusieurs contacts dans un seul fichier VCF vCard pour séparer les fichiers. Voici ce que j’ai fait sur la base du code de Lee. J’ai dû chercher comment créer un nouvel object StreamReader et changer la valeur null en $ null.

 $reader = new-object System.IO.StreamReader("C:\Contacts.vcf") $count = 1 $filename = "C:\Contacts\{0}.vcf" -f ($count) while(($line = $reader.ReadLine()) -ne $null) { Add-Content -path $fileName -value $line if($line -eq "END:VCARD") { ++$count $filename = "C:\Contacts\{0}.vcf" -f ($count) } } $reader.Close() 

Beaucoup de ces réponses étaient trop lentes pour mes fichiers sources. Mes fichiers sources étaient des fichiers SQL entre 10 Mo et 800 Mo qui devaient être divisés en fichiers de nombres de lignes à peu près égaux.

J’ai trouvé certaines des réponses précédentes qui utilisent Add-Content pour être assez lentes. Attendre de nombreuses heures pour qu’un split se termine ne fut pas rare.

Je n’ai pas essayé la réponse de Typhlosaurus , mais il semble que ce ne soit que des fractionnements par taille de fichier, pas par nombre de lignes.

Ce qui suit correspond à mes objectives.

 $sw = new-object System.Diagnostics.Stopwatch $sw.Start() Write-Host "Reading source file..." $lines = [System.IO.File]::ReadAllLines("C:\Temp\SplitTest\source.sql") $totalLines = $lines.Length Write-Host "Total Lines :" $totalLines $skip = 0 $count = 100000; # Number of lines per file # File counter, with sort friendly name $fileNumber = 1 $fileNumberSsortingng = $filenumber.ToSsortingng("000") while ($skip -le $totalLines) { $upper = $skip + $count - 1 if ($upper -gt ($lines.Length - 1)) { $upper = $lines.Length - 1 } # Write the lines [System.IO.File]::WriteAllLines("C:\Temp\SplitTest\result$fileNumberSsortingng.txt",$lines[($skip..$upper)]) # Increment counters $skip += $count $fileNumber++ $fileNumberSsortingng = $filenumber.ToSsortingng("000") } $sw.Stop() Write-Host "Split complete in " $sw.Elapsed.TotalSeconds "seconds" 

Pour un fichier de 54 Mo, j’obtiens la sortie …

 Reading source file... Total Lines : 910030 Split complete in 1.7056578 seconds 

J’espère que d’autres personnes qui recherchent un script de division simple basé sur des lignes et correspondant à mes besoins trouveront cela utile.

Il y a aussi ce one-liner rapide (et un peu sale):

 $linecount=0; $i=0; Get-Content .\BIG_LOG_FILE.txt | %{ Add-Content OUT$i.log "$_"; $linecount++; if ($linecount -eq 3000) {$I++; $linecount=0 } } 

Vous pouvez modifier le nombre de premières lignes par lot en modifiant la valeur 3000 codée en dur.

J’ai fait une petite modification pour diviser les fichiers en fonction de la taille de chaque partie.

 ############################################################################## #.SYNOPSIS # Breaks a text file into multiple text files in a destination, where each # file contains a maximum number of lines. # #.DESCRIPTION # When working with files that have a header, it is often desirable to have # the header information repeated in all of the split files. Split-File # supports this functionality with the -rc (RepeatCount) parameter. # #.PARAMETER Path # Specifies the path to an item. Wildcards are permitted. # #.PARAMETER LiteralPath # Specifies the path to an item. Unlike Path, the value of LiteralPath is # used exactly as it is typed. No characters are interpreted as wildcards. # If the path includes escape characters, enclose it in single quotation marks. # Single quotation marks tell Windows PowerShell not to interpret any # characters as escape sequences. # #.PARAMETER Destination # (Or -d) The location in which to place the chunked output files. # #.PARAMETER Size # (Or -s) The maximum size of each file. Size must be expressed in MB. # #.PARAMETER RepeatCount # (Or -rc) Specifies the number of "header" lines from the input file that will # be repeated in each output file. Typically this is 0 or 1 but it can be any # number of lines. # #.EXAMPLE # Split-File bigfile.csv -s 20 -rc 1 # #.LINK # Out-TempFile ############################################################################## function Split-File { [CmdletBinding(DefaultParameterSetName='Path')] param( [Parameter(ParameterSetName='Path', Position=1, Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [Ssortingng[]]$Path, [Alias("PSPath")] [Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)] [Ssortingng[]]$LiteralPath, [Alias('s')] [Parameter(Position=2,Mandatory=$true)] [Int32]$Size, [Alias('d')] [Parameter(Position=3)] [Ssortingng]$Destination='.', [Alias('rc')] [Parameter()] [Int32]$RepeatCount ) process { # yeah! the cmdlet supports wildcards if ($LiteralPath) { $ResolveArgs = @{LiteralPath=$LiteralPath} } elseif ($Path) { $ResolveArgs = @{Path=$Path} } Resolve-Path @ResolveArgs | %{ $InputName = [IO.Path]::GetFileNameWithoutExtension($_) $InputExt = [IO.Path]::GetExtension($_) if ($RepeatCount) { $Header = Get-Content $_ -TotalCount:$RepeatCount } Resolve-Path @ResolveArgs | %{ $InputName = [IO.Path]::GetFileNameWithoutExtension($_) $InputExt = [IO.Path]::GetExtension($_) if ($RepeatCount) { $Header = Get-Content $_ -TotalCount:$RepeatCount } # get the input file in manageable chunks $Part = 1 $buffer = "" Get-Content $_ -ReadCount:1 | %{ # make an output filename with a suffix $OutputFile = Join-Path $Destination ('{0}-{1:0000}{2}' -f ($InputName,$Part,$InputExt)) # In the first iteration the header will be # copied to the output file as usual # on subsequent iterations we have to do it if ($RepeatCount -and $Part -gt 1) { Set-Content $OutputFile $Header } # test buffer size and dump data only if buffer is greater than size if ($buffer.length -gt ($Size * 1MB)) { # write this chunk to the output file Write-Host "Writing $OutputFile" Add-Content $OutputFile $buffer $Part += 1 $buffer = "" } else { $buffer += $_ + "`r" } } } } } } 

Faites ceci:

FICHIER 1

Il y a aussi ce one-liner rapide (et un peu sale):

  $linecount=0; $i=0; Get-Content .\BIG_LOG_FILE.txt | % { Add-Content OUT$i.log "$_"; $linecount++; if ($linecount -eq 3000) {$I++; $linecount=0 } } 

Vous pouvez modifier le nombre de premières lignes par lot en modifiant la valeur 3000 codée en dur.

 Get-Content C:\TEMP\DATA\split\splitme.txt | Select -First 5000 | out-File C:\temp\file1.txt -Encoding ASCII 

FICHIER 2

 Get-Content C:\TEMP\DATA\split\splitme.txt | Select -Skip 5000 | Select -First 5000 | out-File C:\temp\file2.txt -Encoding ASCII 

FICHIER 3

 Get-Content C:\TEMP\DATA\split\splitme.txt | Select -Skip 10000 | Select -First 5000 | out-File C:\temp\file3.txt -Encoding ASCII 

etc…

Sonne comme un travail pour le partage de commandes UNIX:

 split MyBigFile.csv 

Il suffit de diviser mon fichier csv de 55 Go en blocs de 21 k en moins de 10 minutes.

Ce n’est pas natif à PowerShell, mais est livré, par exemple, avec le package git pour Windows https://git-scm.com/download/win

Mon exigence était un peu différente. Je travaille souvent avec des fichiers ASCII délimités par des virgules et délimités par des tabulations, où une seule ligne est un enregistrement unique de données. Et ils sont vraiment gros, alors je dois les diviser en parties gérables (tout en préservant la ligne d’en-tête).

Donc, je suis revenu à ma méthode VBScript classique et j’ai fusionné un petit script .vbs qui peut être exécuté sur n’importe quel ordinateur Windows (il est automatiquement exécuté par le moteur hôte de script WScript.exe sur Window).

L’avantage de cette méthode est qu’elle utilise des stream de texte, de sorte que les données sous-jacentes ne sont pas chargées en mémoire (ou du moins pas toutes en même temps). Le résultat est que c’est extrêmement rapide et qu’il n’a pas vraiment besoin de beaucoup de mémoire pour fonctionner. Le fichier de test que je viens de diviser en utilisant ce script sur mon i7 contenait environ 1 Go de fichier, contenait environ 12 millions de lignes de texte et était divisé en 25 fichiers de pièces (chacun contenant environ 500 000 lignes). il n’a pas dépassé la mémoire de 3 Mo utilisée à aucun moment.

La mise en garde est que cela dépend du fichier texte ayant des “lignes” (ce qui signifie que chaque enregistrement est délimité par un CRLF) car l’object Text Stream utilise la fonction “ReadLine” pour traiter une seule ligne à la fois. Mais bon, si vous travaillez avec des fichiers TSV ou CSV, c’est parfait.

 Option Explicit Private Const INPUT_TEXT_FILE = "c:\bigtextfile.txt" Private Const REPEAT_HEADER_ROW = True Private Const LINES_PER_PART = 500000 Dim oFileSystem, oInputFile, oOutputFile, iOutputFile, iLineCounter, sHeaderLine, sLine, sFileExt, sStart sStart = Now() sFileExt = Right(INPUT_TEXT_FILE,Len(INPUT_TEXT_FILE)-InstrRev(INPUT_TEXT_FILE,".")+1) iLineCounter = 0 iOutputFile = 1 Set oFileSystem = CreateObject("Scripting.FileSystemObject") Set oInputFile = oFileSystem.OpenTextFile(INPUT_TEXT_FILE, 1, False) Set oOutputFile = oFileSystem.OpenTextFile(Replace(INPUT_TEXT_FILE, sFileExt, "_" & iOutputFile & sFileExt), 2, True) If REPEAT_HEADER_ROW Then iLineCounter = 1 sHeaderLine = oInputFile.ReadLine() Call oOutputFile.WriteLine(sHeaderLine) End If Do While Not oInputFile.AtEndOfStream sLine = oInputFile.ReadLine() Call oOutputFile.WriteLine(sLine) iLineCounter = iLineCounter + 1 If iLineCounter Mod LINES_PER_PART = 0 Then iOutputFile = iOutputFile + 1 Call oOutputFile.Close() Set oOutputFile = oFileSystem.OpenTextFile(Replace(INPUT_TEXT_FILE, sFileExt, "_" & iOutputFile & sFileExt), 2, True) If REPEAT_HEADER_ROW Then Call oOutputFile.WriteLine(sHeaderLine) End If End If Loop Call oInputFile.Close() Call oOutputFile.Close() Set oFileSystem = Nothing Call MsgBox("Done" & vbCrLf & "Lines Processed:" & iLineCounter & vbCrLf & "Part Files: " & iOutputFile & vbCrLf & "Start Time: " & sStart & vbCrLf & "Finish Time: " & Now()) 

Comme les lignes peuvent être variables dans les journaux, j’ai pensé qu’il était préférable de prendre un certain nombre de lignes par approche de fichier. L’extrait de code suivant a traité un fichier journal de 4 millions de lignes en moins de 19 secondes (18,83 secondes) en le divisant en segments de 500 000 lignes:

 $sourceFile = "c:\myfolder\mylargeTextyFile.csv" $partNumber = 1 $batchSize = 500000 $pathAndFilename = "c:\myfolder\mylargeTextyFile part $partNumber file.csv" [System.Text.Encoding]$enc = [System.Text.Encoding]::GetEncoding(65001) # utf8 this one $fs=New-Object System.IO.FileStream ($sourceFile,"OpenOrCreate", "Read", "ReadWrite",8,"None") $streamIn=New-Object System.IO.StreamReader($fs, $enc) $streamout = new-object System.IO.StreamWriter $pathAndFilename $line = $streamIn.readline() $counter = 0 while ($line -ne $null) { $streamout.writeline($line) $counter +=1 if ($counter -eq $batchsize) { $partNumber+=1 $counter =0 $streamOut.close() $pathAndFilename = "c:\myfolder\mylargeTextyFile part $partNumber file.csv" $streamout = new-object System.IO.StreamWriter $pathAndFilename } $line = $streamIn.readline() } $streamin.close() $streamout.close() 

Cela peut facilement être transformé en fichier de fonction ou de script avec des parameters pour le rendre plus polyvalent. Il utilise un StreamWriter et un StreamWriter pour atteindre sa vitesse et son StreamWriter

Voici ma solution pour diviser un fichier appelé patch6.txt (environ 32 000 lignes) en fichiers séparés de 1 000 lignes chacun. Ce n’est pas rapide, mais ça fait le boulot.

 $infile = "D:\Malcolm\Test\patch6.txt" $path = "D:\Malcolm\Test\" $lineCount = 1 $fileCount = 1 foreach ($computername in get-content $infile) { write $computername | out-file -Append $path_$fileCount".txt" $lineCount++ if ($lineCount -eq 1000) { $fileCount++ $lineCount = 1 } }