¿Cómo dividir un archivo y mantener la primera línea en cada una de las piezas?


Dado: Un archivo de datos de texto grande (por ejemplo, formato CSV) con una primera línea 'especial' (por ejemplo, nombres de campos).

Se busca: Un equivalente del comando coreutils split -l, pero con el requisito adicional de que la línea de encabezado del archivo original aparezca al principio de cada una de las piezas resultantes.

Supongo que algún brebaje de split y head hará el truco?

Author: Arkady, 2009-09-11

9 answers

Este es el script de robhruska limpiado un poco:

tail -n +2 file.txt | split -l 4 - split_
for file in split_*
do
    head -n 1 file.txt > tmp_file
    cat $file >> tmp_file
    mv -f tmp_file $file
done

He quitado wc, cut, ls y echo en los lugares donde son innecesarios. Cambié algunos de los nombres de archivo para hacerlos un poco más significativos. Lo dividí en varias líneas solo para que fuera más fácil de leer.

Si quieres ponerte elegante, puedes usar mktemp o tempfile para crear un nombre de archivo temporal en lugar de usar uno codificado.

Editar

Usando GNU split es posible hacer esto:

split_filter () { { head -n 1 file.txt; cat; } > "$FILE"; }; export -f split_filter; tail -n +2 file.txt | split --lines=4 --filter=split_filter - split_

Desglosado por legibilidad:

split_filter () { { head -n 1 file.txt; cat; } > "$FILE"; }
export -f split_filter
tail -n +2 file.txt | split --lines=4 --filter=split_filter - split_

Cuando se especifica --filter, split ejecuta el comando (una función en este caso, que debe exportarse) para cada archivo de salida y establece la variable FILE, en el entorno del comando, en el nombre del archivo.

Un script o función de filtro podría hacer cualquier manipulación que quisiera al contenido de salida o incluso al nombre del archivo. Un ejemplo de esto último podría ser la salida a un nombre de archivo fijo en una variable directorio: > "$FILE/data.dat", por ejemplo.

 36
Author: Dennis Williamson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-11-20 17:41:42

Podría usar la nueva funcionalidad filter filter en GNU coreutils split >= 8.13 (2011):

tail -n +2 FILE.in |
split -l 50 - --filter='sh -c "{ head -n1 FILE.in; cat; } > $FILE"'
 10
Author: pixelbeat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-08 00:09:05

Puedes usar [mg]awk:

awk 'NR==1{
        header=$0; 
        count=1; 
        print header > "x_" count; 
        next 
     } 

     !( (NR-1) % 100){
        count++; 
        print header > "x_" count;
     } 
     {
        print $0 > "x_" count
     }' file

100 es el número de líneas de cada corte. No requiere archivos temporales y se puede poner en una sola línea.

 8
Author: marco,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-09-12 15:25:48

Soy un novato cuando se trata de Bash-fu, pero fui capaz de inventar esta monstruosidad de dos comandos. Estoy seguro de que hay soluciones más elegantes.

$> tail -n +2 file.txt | split -l 4
$> for file in `ls xa*`; do echo "`head -1 file.txt`" > tmp; cat $file >> tmp; mv -f tmp $file; done

Esto es asumiendo que su archivo de entrada es file.txt, no está utilizando el argumento prefix para split, y está trabajando en un directorio que no tiene ningún otro archivo que comience con el formato de salida predeterminado split xa*. Además, reemplace el ' 4 ' con el tamaño de línea dividida deseado.

 4
Author: Rob Hruska,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-09-11 16:22:53

Esta es una versión más robusta del script de Denis Williamson. El script crea una gran cantidad de archivos temporales, y sería una pena si se dejaran por ahí si la ejecución estaba incompleta. Por lo tanto, vamos a añadir la captura de señal (ver http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html y luego http://tldp.org/LDP/abs/html/debugging.html ) y eliminar nuestros archivos temporales; esta es una buena práctica de todos modos.

trap 'rm split_* tmp_file ; exit 13' SIGINT SIGTERM SIGQUIT 
tail -n +2 file.txt | split -l 4 - split_
for file in split_*
do
    head -n 1 file.txt > tmp_file
    cat $file >> tmp_file
    mv -f tmp_file $file
done

Sustitúyase " 13 " por cualquier código de retorno tú quieres. Oh, y probablemente deberías usar mktemp de todos modos (como algunos ya han sugerido), así que adelante y elimina 'tmp_file" de la rm en la línea de trampa. Vea la página man de signal para ver más señales que capturar.

 2
Author: Sam Bisbee,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-09-11 20:04:39

Nunca estoy seguro de las reglas de copiar scripts directamente desde los sitios de otras personas, pero Geekology tiene un buen script para hacer lo que quieras, con algunos comentarios que confirman que funciona. Asegúrese de hacer tail -n +2 como se indica en un comentario cerca de la parte inferior.

 1
Author: Mark Rushakoff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-09-11 16:13:58

Me gustó la versión awk de marco, adoptó a partir de esto un solo liner simplificado donde puede especificar fácilmente la fracción dividida tan granular como desee:

awk 'NR==1{print $0 > FILENAME ".split1";  print $0 > FILENAME ".split2";} NR>1{if (NR % 10 > 5) print $0 >> FILENAME ".split1"; else print $0 >> FILENAME ".split2"}' file
 1
Author: DreamFlasher,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-21 17:43:21

Me gustaron mucho las versiones de Rob y Dennis, tanto que quise mejorarlas.

Aquí está mi versión:

in_file=$1
awk '{if (NR!=1) {print}}' $in_file | split -d -a 5 -l 100000 - $in_file"_" # Get all lines except the first, split into 100,000 line chunks
for file in $in_file"_"*
do
    tmp_file=$(mktemp $in_file.XXXXXX) # Create a safer temp file
    head -n 1 $in_file | cat - $file > $tmp_file # Get header from main file, cat that header with split file contents to temp file
    mv -f $tmp_file $file # Overwrite non-header containing file with header-containing file
done

Diferencias:

  1. in_file es el argumento de archivo que desea dividir manteniendo las cabeceras
  2. Use awk en lugar de tail debido a que awk tiene un mejor rendimiento
  3. dividir en 100.000 archivos de línea en lugar de 4
  4. El nombre del archivo dividido será el nombre del archivo de entrada agregado con un guion bajo y números (hasta 99999-desde " - d-a 5" argumento dividido)
  5. Use mktemp para manejar archivos temporales de forma segura
  6. Utilice una sola línea head | cat en lugar de dos líneas
 1
Author: Garren S,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-29 21:42:58

Use GNU Parallel:

parallel -a bigfile.csv --header : --pipepart 'cat > {#}'

Si necesita ejecutar un comando en cada una de las partes, GNU Parallel también puede ayudarlo a hacerlo:

parallel -a bigfile.csv --header : --pipepart my_program_reading_from_stdin
parallel -a bigfile.csv --header : --pipepart --fifo my_program_reading_from_fifo {}
parallel -a bigfile.csv --header : --pipepart --cat my_program_reading_from_a_file {}

Si desea dividir en 2 partes por núcleo de CPU (por ejemplo, 24 núcleos = 48 partes de igual tamaño):

parallel --block -2 -a bigfile.csv --header : --pipepart my_program_reading_from_stdin

Si desea dividir en bloques de 10 MB:

parallel --block 10M -a bigfile.csv --header : --pipepart my_program_reading_from_stdin
 0
Author: Ole Tange,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-22 13:42:15