¿Cómo puedo eliminar la primera línea de un archivo de texto usando el script bash / sed?

555

Necesito eliminar repetidamente la primera línea de un gran archivo de texto usando un script bash.

En este momento lo estoy usando sed -i -e "1d" $FILE, pero me lleva alrededor de un minuto eliminarlo.

¿Hay una manera más eficiente de lograr esto?

Brent
fuente
¿Qué significa -i?
cikatomo
44
@cikatomo: significa edición en línea: edita el archivo con lo que genere.
drewrockshard
44
la cola es MUCHO MÁS LENTA que sed. la cola necesita 13.5s, sed necesita 0.85s. Mi archivo tiene ~ 1M líneas, ~ 100MB. MacBook Air 2013 con SSD.
jcsahnwaldt dice GoFundMonica

Respuestas:

1031

Prueba la cola :

tail -n +2 "$FILE"

-n x: Solo imprime las últimas xlíneas. tail -n 5le daría las últimas 5 líneas de la entrada. El +tipo de signo invierte el argumento y hace tailimprimir cualquier cosa menos las primeras x-1líneas. tail -n +1imprimiría todo el archivo, tail -n +2todo menos la primera línea, etc.

GNU tailes mucho más rápido que sed. tailtambién está disponible en BSD y el -n +2indicador es coherente en ambas herramientas. Consulte las páginas de manual de FreeBSD o OS X para obtener más información.

Sin sedembargo, la versión BSD puede ser mucho más lenta que . Me pregunto cómo lograron eso; taildebería leer un archivo línea por línea mientras sedrealiza operaciones bastante complejas que implican interpretar un script, aplicar expresiones regulares y similares.

Nota: es posible que tengas la tentación de usar

# THIS WILL GIVE YOU AN EMPTY FILE!
tail -n +2 "$FILE" > "$FILE"

pero esto te dará un archivo vacío . La razón es que la redirección ( >) ocurre antes tailde que el shell la invoque:

  1. Shell trunca el archivo $FILE
  2. Shell crea un nuevo proceso para tail
  3. Shell redirige stdout del tailproceso a$FILE
  4. tail lee desde el ahora vacío $FILE

Si desea eliminar la primera línea dentro del archivo, debe usar:

tail -n +2 "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE"

Se &&asegurará de que el archivo no se sobrescriba cuando haya un problema.

Aaron Digulla
fuente
3
De acuerdo con esto ss64.com/bash/tail.html, el búfer típico tiene un valor predeterminado de 32k cuando se usa BSD 'tail' con la -ropción. ¿Tal vez hay una configuración de búfer en algún lugar del sistema? ¿O -nes un número con signo de 32 bits?
Yzmir Ramirez
41
@Eddie: user869097 dijo que no funciona cuando una sola línea es de 15Mb o más. Siempre que las líneas sean más cortas, tailfuncionará para cualquier tamaño de archivo.
Aaron Digulla
66
¿podrías explicar estos argumentos?
Dreampuf
17
@Dreampuf - desde la página de manual:-n N means output the last N lines, instead of the last 10; or use +N to output lines starting with the Nth
Will Sheppard
11
Iba a estar de acuerdo con @JonaChristopherSahnwaldt: la cola es mucho, mucho más lenta que la variante sed, en un orden de magnitud. Lo estoy probando en un archivo de 500,000K líneas (no más de 50 caracteres por línea). Sin embargo, luego me di cuenta de que estaba usando la versión de cola FreeBSD (que viene con OS X por defecto). Cuando cambié a la cola de GNU, la llamada de cola fue 10 veces más rápida que la llamada sed (y la llamada sed de GNU también). AaronDigulla es correcto aquí, si está utilizando GNU.
Dan Nguyen
179

Puede usar -i para actualizar el archivo sin usar el operador '>'. El siguiente comando eliminará la primera línea del archivo y la guardará en el archivo.

sed -i '1d' filename
amit
fuente
1
Me sale un error:unterminated transform source string
Daniel Kobe
10
¡Esto funciona siempre y realmente debería ser la mejor respuesta!
xtheking
44
Solo para recordar, Mac requiere que se proporcione un sufijo cuando se usa sed con ediciones in situ. Ejecute lo anterior con -i.bak
mjp
3
Solo una nota: para eliminar varias líneas, usesed -i '1,2d' filename
The Godfather
44
Esta versión es realmente mucho más legible y más universal que tail -n +2. No estoy seguro de por qué no es la respuesta principal.
Luke Davis
74

Para aquellos que están en SunOS que no es GNU, el siguiente código ayudará:

sed '1d' test.dat > tmp.dat 
Nasri Najib
fuente
18
Datos demográficos interesantes
capitán
17

No, eso es lo más eficiente que vas a conseguir. Podría escribir un programa en C que podría hacer el trabajo un poco más rápido (menos tiempo de inicio y argumentos de procesamiento), pero probablemente tenderá a la misma velocidad a medida que los archivos crecen (y supongo que son grandes si toma un minuto )

Pero su pregunta tiene el mismo problema que tantos otros, ya que supone la solución. Si nos dijera en detalle qué está tratando de hacer en lugar de cómo , podríamos sugerirle una mejor opción.

Por ejemplo, si este es un archivo A que procesa algún otro programa B, una solución sería no quitar la primera línea, sino modificar el programa B para procesarlo de manera diferente.

Digamos que todos sus programas se agregan a este archivo A y el programa B actualmente lee y procesa la primera línea antes de eliminarlo.

Puede rediseñar el programa B para que no intente eliminar la primera línea pero mantenga un desplazamiento persistente (probablemente basado en archivos) en el archivo A para que, la próxima vez que se ejecute, pueda buscar ese desplazamiento, procesar la línea allí y actualice el desplazamiento.

Luego, en un momento de silencio (¿medianoche?), Podría realizar un procesamiento especial del archivo A para eliminar todas las líneas procesadas actualmente y volver a establecer el desplazamiento en 0.

Sin duda será más rápido que un programa abra y busque un archivo en lugar de abrirlo y reescribirlo. Esta discusión supone que usted tiene control sobre el programa B, por supuesto. No sé si ese es el caso, pero puede haber otras posibles soluciones si proporciona más información.

paxdiablo
fuente
Creo que el OP está tratando de lograr lo que me hizo encontrar esta pregunta. Tengo 10 archivos CSV con 500k líneas en cada uno. Cada archivo tiene la misma fila de encabezado que la primera línea. Estoy tomando estos archivos en un solo archivo y luego importándolos en una base de datos permitiendo que la base de datos cree nombres de columnas desde la primera línea. Obviamente no quiero que esa línea se repita en el archivo 2-10.
db
1
@db En ese caso, awk FNR-1 *.csvprobablemente sea más rápido.
jinawee
10

Usted puede editar los archivos en su lugar: Sólo uso de Perl -ibandera, de esta manera:

perl -ni -e 'print unless $. == 1' filename.txt

Esto hace que la primera línea desaparezca, como usted pregunta. Perl necesitará leer y copiar todo el archivo, pero organiza que la salida se guarde con el nombre del archivo original.

alexis
fuente
10

Puedes hacer esto fácilmente con:

cat filename | sed 1d > filename_without_first_line

en la línea de comando; o para eliminar la primera línea de un archivo de forma permanente, use el modo in situ de sed con la -ibandera:

sed -i 1d <filename>
Ingo Baab
fuente
9

Como dijo Pax, probablemente no va a llegar más rápido que esto. La razón es que casi no hay sistemas de archivos que admitan el truncamiento desde el principio del archivo, por lo que esta será una noperación O ( ) donde nestá el tamaño del archivo. Sin embargo, lo que puede hacer mucho más rápido es sobrescribir la primera línea con el mismo número de bytes (tal vez con espacios o un comentario) que podría funcionar para usted dependiendo de exactamente lo que está tratando de hacer (¿qué es eso por cierto?).

Robert Gamble
fuente
Re "... casi no hay sistemas de archivos que admitan truncar ..." : eso es interesante; considere incluir una nota entre paréntesis nombrando dicho sistema de archivos.
agc
1
@agc: irrelevante ahora, pero mi primer trabajo en los años 70 fue con Quadex, una pequeña startup (ahora desaparecida y sin relación con las dos compañías que ahora usan ese nombre). Tenían un sistema de archivos que permitía agregar o eliminar al principio o al final de un archivo, utilizado principalmente para implementar la edición en menos de 3 KB colocando archivos en la ventana superior e inferior. No tenía nombre propio, era solo parte de QMOS, el Sistema Operativo Multiusuario Quadex. ('Multi' usualmente era 2-3 en un LSI-11/02 con menos de 64KB de RAM y usualmente algunos disquetes RX01-type de 8 "cada 250KB.) :-)
dave_thompson_085
9

La spongeutilidad evita la necesidad de hacer malabarismos con un archivo temporal:

tail -n +2 "$FILE" | sponge "$FILE"
agc
fuente
spongede hecho es mucho más limpio y robusto que la solución aceptada ( tail -n +2 "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE")
Jealie
1
Debe quedar claro que 'esponja' requiere la instalación del paquete 'moreutils'.
FedFranzoni
Esta es la única solución que funcionó para mí para cambiar un archivo del sistema (en una imagen acoplable de Debian). Otras soluciones fallaron debido al error "Dispositivo o recurso ocupado" al intentar escribir el archivo.
FedFranzoni
Pero, ¿ spongealmacena el archivo completo en la memoria? Eso no funcionará si son cientos de GB.
OrangeDog
@OrangeDog, siempre que el sistema de archivos pueda almacenarlo, spongelo absorberá, ya que usa un archivo / tmp como un paso intermedio, que luego se usa para reemplazar el original después.
agc
8

Si desea modificar el archivo en su lugar, siempre se puede utilizar el original eden lugar de su s sucesor treaming sed:

ed "$FILE" <<<$'1d\nwq\n'

El edcomando era el editor de texto original de UNIX, incluso antes de que hubiera terminales de pantalla completa, y mucho menos estaciones de trabajo gráficas. El exeditor, mejor conocido como lo que estás usando cuando se pulsa en el colon rápido en vi, es un ex versión de tendido ed, por lo que muchos de los mismos comandos de trabajo. Si bien edestá destinado a usarse de manera interactiva, también se puede usar en modo por lotes enviándole una cadena de comandos, que es lo que hace esta solución.

La secuencia <<<$'1d\nwq\n'se aprovecha de apoyo del Golpe de aquí-cuerdas ( <<<) y citas POSIX ( $'... ') a la entrada de alimentación al edcomando que consiste en dos líneas: 1dque d eletes línea 1 , y luego wq, que w ritos la parte posterior archivo a disco y luego q sale de la sesión de edición.

Mark Reed
fuente
Esto es elegante. +1
Armin
Pero debe leer todo el archivo en la memoria, lo que no funcionará si son cientos de GB.
OrangeDog
5

debería mostrar las líneas excepto la primera línea:

cat textfile.txt | tail -n +2
serup
fuente
44
- deberías hacer "tail -n +2 textfile.txt"
niglesias el
55
@niglesiais No estoy de acuerdo con el "uso inútil del gato", ya que deja en claro que esta solución está bien en el contenido canalizado y no solo en los archivos.
Titou
5

Podría usar vim para hacer esto:

vim -u NONE +'1d' +'wq!' /tmp/test.txt

Esto debería ser más rápido, ya que vim no leerá todo el archivo cuando se procese.

Hongbo Liu
fuente
Puede que necesite citar +wq!si su shell es bash. Probablemente no, ya !que no está al principio de una palabra, pero adquirir el hábito de citar cosas probablemente sea bueno en todos lados. (Y si busca una súper eficiencia al no citar innecesariamente, tampoco necesita las comillas 1d).
Mark Reed
vim no necesita leer todo el archivo. De hecho, si el archivo es más grande que la memoria, como se preguntó en esta Q, vim lee todo el archivo y lo escribe (o la mayor parte) en un archivo temporal, y después de editarlo lo vuelve a escribir (en el archivo permanente). No sé cómo crees que podría funcionar sin esto.
dave_thompson_085
4

¿Qué tal usar csplit?

man csplit
csplit -k file 1 '{1}'
Shahbaz
fuente
Esta sintaxis también funcionaría, pero sólo generan dos archivos de salida en lugar de tres: csplit file /^.*$/1. O más simplemente: csplit file //1. O aún más simple: csplit file 2.
Marco Roy
1

Como parece que no puedo acelerar la eliminación, creo que un buen enfoque podría ser procesar el archivo en lotes como este:

While file1 not empty
  file2 = head -n1000 file1
  process file2
  sed -i -e "1000d" file1
end

El inconveniente de esto es que si el programa se mata en el medio (o si hay algún sql malo allí, causando que la parte del "proceso" muera o se bloquee), habrá líneas que se omiten o se procesan dos veces .

(archivo1 contiene líneas de código sql)

Brent
fuente
¿Qué contiene la primera línea? ¿Puedes sobrescribirlo con un comentario sql como sugerí en mi publicación?
Robert Gamble, el
0

Si lo que está buscando hacer es recuperarse después de una falla, puede crear un archivo que tenga lo que ha hecho hasta ahora.

if [[ -f $tmpf ]] ; then
    rm -f $tmpf
fi
cat $srcf |
    while read line ; do
        # process line
        echo "$line" >> $tmpf
    done
Tim
fuente
0

Este liner hará:

echo "$(tail -n +2 "$FILE")" > "$FILE"

Funciona, ya que tailse ejecuta antes echoy luego el archivo se desbloquea, por lo tanto, no es necesario un archivo temporal.

egors
fuente
-1

¿Usaría la cola en las líneas N-1 y la dirigiría a un archivo, luego eliminaría el archivo antiguo y cambiaría el nombre del nuevo archivo al antiguo nombre?

Si estuviera haciendo esto programáticamente, leería el archivo y recordaría el desplazamiento del archivo, después de leer cada línea, para poder volver a esa posición y leer el archivo con una línea menos.

EvilTeach
fuente
La primera solución es esencialmente idéntica a la que Brent está haciendo ahora. No entiendo su enfoque programático, solo la primera línea debe eliminarse, simplemente leería y descartaría la primera línea y copiaría el resto en otro archivo que es nuevamente el mismo que los enfoques sed y tail.
Robert Gamble, el
La segunda solución implica que el archivo no se reduce por la primera línea cada vez. El programa simplemente lo procesa, como si se hubiera reducido, pero comenzando en la siguiente línea cada vez
EvilTeach
Todavía no entiendo cuál es tu segunda solución.
Robert Gamble