web-dev-qa-db-fra.com

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

Je dois diviser un fichier texte volumineux (500 Mo) (un fichier d'exception log4net) en morceaux gérables, tels que 100 fichiers de 5 Mo suffiraient.

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

58
Ralph Shillington

C'est une tâche assez facile pour PowerShell, compliquée par le fait que l'applet de commande Get-Content standard ne gère pas très bien les très gros fichiers. Ce que je suggérerais de faire est d'utiliser la classe .NET StreamReader pour lire le fichier ligne par ligne dans votre script PowerShell et d'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()
40
Lee

Un avertissement sur certaines des réponses existantes - elles seront très lentes 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 ouvre, cherche, puis ferme le fichier de destination actuel pour chaque ligne du fichier source. Lire un peu du fichier source à chaque fois et rechercher les nouvelles lignes ralentira également les choses, mais je suppose que Add-Content est le principal responsable.

La variante suivante produit un résultat légèrement moins agréable: elle scinde les fichiers au milieu de lignes, mais 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()
}
47
Typhlosaurus

Une ligne simple à scinder en fonction du nombre de lignes (100 dans ce cas):

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

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

Remarque: je vérifie très peu les erreurs. Je ne peux donc pas garantir que cela fonctionnera sans problème pour votre cas. Il l'a fait pour le mien ( Fichier de 1,7 Go TXT de 4 millions de lignes divisées 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.ToString("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.ToString("000"),$ext))
                $filecount++
                $linecount = 0
            }
        }
    } finally {
        $writer.Dispose();
    }
} finally {
    $reader.Dispose();
}
$sw.Stop()

Write-Host "Split complete in " $sw.Elapsed.TotalSeconds "seconds"

Sortie divisant 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
27
Vincent De Smet

J'ai souvent besoin de faire la même chose. L'astuce consiste à répéter l'en-tête dans chacun des fragments séparés. J'ai écrit l'applet de commande suivante (PowerShell v2 CTP 3) et cela 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)]
        [String[]]$Path,

        [Alias("PSPath")]
        [Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [String[]]$LiteralPath,

        [Alias('c')]
        [Parameter(Position=2,Mandatory=$true)]
        [Int32]$Count,

        [Alias('d')]
        [Parameter(Position=3)]
        [String]$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

            }

        }

    }

}
15
Josh

J'ai trouvé cette question en essayant de scinder plusieurs contacts dans un seul fichier VCF vCard en fichiers séparés. Voici ce que j'ai fait sur la base du code de Lee. J'ai dû chercher comment créer un nouvel objet StreamReader et changé 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()
14
user202448

Beaucoup de ces réponses étaient trop lentes pour mes fichiers source. Mes fichiers source étaient des fichiers SQL entre 10 et 800 Mo qui devaient être divisés en fichiers dont le nombre de lignes était à peu près égal.

J'ai trouvé certaines réponses précédentes utilisant Add-Content assez lentes. Il n’était pas rare d’attendre de nombreuses heures que la scission se termine.

Je n'ai pas essayé La réponse de Typhlosaurus , mais il semble ne séparer que les divisions en fonction de la taille du fichier, pas du nombre de lignes.

Ce qui suit a adapté mes objectifs.

$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
$fileNumberString = $filenumber.ToString("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$fileNumberString.txt",$lines[($skip..$upper)])

    # Increment counters
    $skip += $count
    $fileNumber++
    $fileNumberString = $filenumber.ToString("000")
}

$sw.Stop()

Write-Host "Split complete in " $sw.Elapsed.TotalSeconds "seconds"

Pour un fichier de 54 Mo, je reçois la sortie ...

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

J'espère que les autres personnes à la recherche d'un script de fractionnement basé sur les lignes simple et conforme à mes besoins le trouveront utile.

6
CVertex

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.

3
zroiy

J'ai apporté une petite modification pour fractionner 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)]
        [String[]]$Path,

        [Alias("PSPath")]
        [Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [String[]]$LiteralPath,

        [Alias('s')]
        [Parameter(Position=2,Mandatory=$true)]
        [Int32]$Size,

        [Alias('d')]
        [Parameter(Position=3)]
        [String]$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"
     }
    }
   }
        }
    }
}
2

Faire ceci:

DOSSIER 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

DOSSIER 3

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

etc…

2
ecciethetechie

Cela ressemble à un travail pour le fractionnement de commande UNIX:

split MyBigFile.csv

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

Ce n’est pas natif de PowerShell, mais vient avec, par exemple, le paquet git for windows https://git-scm.com/download/win

1
NicolasG

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 travail.

$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
    }
}
0
Bigdadda06

Mon besoin était un peu différent. Je travaille souvent avec des fichiers ASCII séparés par des virgules et par des tabulations, dans lesquels une seule ligne correspond à un seul enregistrement de données. Et comme ils sont vraiment gros, je dois les scinder en parties gérables (tout en préservant la ligne d'en-tête).

Alors, je suis revenu à ma méthode classique VBScript et ai construit ensemble 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 de Windows).

L'avantage de cette méthode est qu'elle utilise des flux 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 qu'il est exceptionnellement rapide et qu'il ne nécessite pas beaucoup de mémoire pour fonctionner. Le fichier de test que je viens de scinder à l'aide de ce script sur mon i7 avait une taille de fichier d'environ 1 Go. Il contenait environ 12 millions de lignes de texte et était divisé en 25 fichiers de parties (chacun d'environ 500 000 lignes). il n'a jamais dépassé 3 Mo de mémoire utilisée.

La mise en garde est qu'il repose sur le fichier texte ayant des "lignes" (ce qui signifie que chaque enregistrement est délimité par un CRLF) car l'objet Flux de texte 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())
0
Covenant

Comme les lignes peuvent être variables dans les journaux, j’ai pensé qu’il était préférable de choisir un nombre de lignes par 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 scindant en 500 000 fragments de ligne:

$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 une fonction ou un fichier de script avec des paramètres pour le rendre plus polyvalent. Il utilise StreamReader et StreamWriter pour atteindre sa rapidité et sa petite empreinte mémoire.

0
GMasucci