tutorial - Eliminar la línea superior del archivo de texto con PowerShell
powershell tutorial (9)
Estoy tratando de eliminar la primera línea de unos 5000 archivos de texto antes de importarlos.
Todavía soy muy nuevo en PowerShell, por lo que no estoy seguro de qué buscar ni cómo abordarlo. Mi concepto actual usando pseudo-código:
set-content file (get-content unless line contains amount)
Sin embargo, parece que no puedo entender cómo hacer algo como contener.
Acabo de aprender de un sitio web:
Get-ChildItem *.txt | ForEach-Object { (get-Content $_) | Where-Object {(1) -notcontains $_.ReadCount } | Set-Content -path $_ }
O puede usar los alias para abreviar, como:
gci *.txt | % { (gc $_) | ? { (1) -notcontains $_.ReadCount } | sc -path $_ }
Inspirado por la respuesta de AASoft , salí a mejorarlo un poco más:
- Evite la variable de bucle
$i
y la comparación con0
en cada bucle - Envuelva la ejecución en un bloque
try..finally
para cerrar siempre los archivos en uso - Haga que la solución funcione para un número arbitrario de líneas para eliminar desde el principio del archivo
- Use una variable
$p
para hacer referencia al directorio actual
Estos cambios conducen al siguiente código:
$p = (Get-Location).Path
(Measure-Command {
# Number of lines to skip
$skip = 1
$ins = New-Object System.IO.StreamReader ($p + "/test.log")
$outs = New-Object System.IO.StreamWriter ($p + "/test-1.log")
try {
# Skip the first N lines, but allow for fewer than N, as well
for( $s = 1; $s -le $skip -and !$ins.EndOfStream; $s++ ) {
$ins.ReadLine()
}
while( !$ins.EndOfStream ) {
$outs.WriteLine( $ins.ReadLine() )
}
}
finally {
$outs.Close()
$ins.Close()
}
}).TotalSeconds
El primer cambio redujo el tiempo de procesamiento de mi archivo de 60 MB de 5.3s
a 4s
. El resto de los cambios es más cosmético.
No es el más eficiente del mundo, pero debería funcionar:
get-content $file |
select -Skip 1 |
set-content "$file-temp"
move "$file-temp" $file -Force
Para archivos más pequeños, puede usar esto:
& C: / windows / system32 / more +1 oldfile.csv> newfile.csv | fuera-nulo
... pero no es muy efectivo en el procesamiento de mi archivo de ejemplo de 16 MB. Parece que no termina y libera el bloqueo en newfile.csv.
Si bien admiro la respuesta de @hoge tanto por una técnica muy concisa como por una función de envoltura para generalizarla y aliento a los votantes por ella, me veo obligado a comentar las otras dos respuestas que usan archivos temporales (me muerde las uñas en una pizarra!).
Suponiendo que el archivo no es enorme, puede forzar a la canalización a operar en secciones discretas, obviando así la necesidad de un archivo temporal, con un uso juicioso de paréntesis:
(Get-Content $file | Select-Object -Skip 1) | Set-Content $file
... o en forma corta:
(gc $file | select -Skip 1) | sc $file
Solo tenía que hacer la misma tarea, y gc | select ... | sc
gc | select ... | sc
gc | select ... | sc
tomó más de 4 GB de RAM en mi máquina mientras leía un archivo de 1.6 GB. No terminó durante al menos 20 minutos después de leer todo el archivo (como lo informó Read Bytes en Process Explorer ), en cuyo punto tuve que matarlo.
Mi solución fue utilizar un enfoque más .NET: StreamReader
+ StreamWriter
. Consulte esta respuesta para obtener una excelente respuesta sobre la presentación: En Powershell, ¿cuál es la forma más eficiente de dividir un archivo de texto grande por tipo de registro?
A continuación está mi solución. Sí, usa un archivo temporal, pero en mi caso, no importaba (era un archivo de sentencias de creación e inserción de tablas SQL enorme).
PS> (measure-command{
$i = 0
$ins = New-Object System.IO.StreamReader "in/file/pa.th"
$outs = New-Object System.IO.StreamWriter "out/file/pa.th"
while( !$ins.EndOfStream ) {
$line = $ins.ReadLine();
if( $i -ne 0 ) {
$outs.WriteLine($line);
}
$i = $i+1;
}
$outs.Close();
$ins.Close();
}).TotalSeconds
Regresó:
188.1224443
Usando la notación variable, puede hacerlo sin un archivo temporal:
${C:/file.txt} = ${C:/file.txt} | select -skip 1
function Remove-Topline ( [string[]]$path, [int]$skip=1 ) {
if ( -not (Test-Path $path -PathType Leaf) ) {
throw "invalid filename"
}
ls $path |
% { iex "`${$($_.fullname)} = `${$($_.fullname)} | select -skip $skip" }
}
skip` no funcionó, por lo que mi solución es
$LinesCount = $(get-content $file).Count
get-content $file |
select -Last $($LinesCount-1) |
set-content "$file-temp"
move "$file-temp" $file -Force
$x = get-content $file
$x[1..$x.count] | set-content $file
Solo eso. Una larga y aburrida explicación sigue. Get-content devuelve una matriz. Podemos "indexar" variables de matriz, como se demuestra en this y other publicaciones de Guionistas.
Por ejemplo, si definimos una variable de matriz como esta,
$array = @("first item","second item","third item")
entonces $ array regresa
first item
second item
third item
entonces podemos "indexar" esa matriz para recuperar solo su primer elemento
$array[0]
o solo su segundo
$array[1]
o un range de valores de índice desde el 2do hasta el último.
$array[1..$array.count]