linux - make - ¿Cómo dividir un archivo y mantener la primera línea en cada una de las piezas?
shell script linux (9)
Dado: un gran archivo de datos de texto (por ejemplo, formato CSV) con una primera línea ''especial'' (por ejemplo, nombres de campo).
Se busca: un equivalente del comando coreutils split -l
, pero con el requisito adicional de que la línea del encabezado del archivo original aparezca al comienzo de cada una de las piezas resultantes.
Supongo que algún brebaje de split
y head
hará el truco?
Esta es la secuencia de comandos 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
Eliminé wc
, cut
, ls
y echo
en los lugares donde no son necesarios. Cambié algunos de los nombres de los archivos para hacerlos un poco más significativos. Lo dividí en varias líneas solo para que sea más fácil de leer.
Si quieres ser elegante, puedes usar mktemp
o tempfile
para crear un nombre de archivo temporal en lugar de usar uno con código tempfile
.
Editar
Usando la split
GNU 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_
Descompuesto para la 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 un directorio de variables: > "$FILE/data.dat"
por ejemplo.
Esta es una versión más robusta del guión de Denis Williamson . La secuencia de comandos crea una gran cantidad de archivos temporales, y sería una pena si se dejaran tirados si la ejecución fuera incompleta. Entonces, agreguemos trampas de señal (vea 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 mejor 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
Reemplace ''13'' con el código de retorno que desee. Ah, y probablemente deberías estar usando mktemp de todos modos (como algunos ya lo han sugerido), así que adelante y elimina ''tmp_file'' de la memoria en la línea de trampa. Mira la página de señal para obtener más señales.
Me gustó la versión awk de marco, adoptada a partir de esto, una línea simplificada donde se puede especificar fácilmente la fracción de división 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
Nunca estoy seguro de las reglas para copiar scripts directamente de los sitios de otras personas, pero Geekology tiene una buena secuencia de comandos para hacer lo que quiera, 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.
Puede usar la nueva funcionalidad de filtro en GNU coreutils split> = 8.13 (2011):
tail -n +2 FILE.in |
split -l 50 - --filter=''sh -c "{ head -n1 FILE.in; cat; } > $FILE"''
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 sector. No requiere archivos temporales y se puede poner en una sola línea.
Realmente me gustaron las versiones de Rob y Dennis, tanto que quería 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
- archivo_in es el argumento del archivo que desea dividir manteniendo encabezados
- Use
awk
lugar detail
debido a queawk
tiene un mejor rendimiento - dividido en 100.000 archivos de línea en lugar de 4
- El nombre del archivo dividido será el nombre del archivo de entrada anexado con un guión bajo y números (hasta 99999 - del argumento de división "-d -a 5")
- Use mktemp para manejar de forma segura los archivos temporales
- Use una sola
head | cat
línea dehead | cat
lugar de dos líneas
Soy un novato cuando se trata de Bash-fu, pero pude 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 supone que su archivo de entrada es file.txt
, no está usando el argumento de prefix
para split
, y está trabajando en un directorio que no tiene ningún otro archivo que comience con el formato de salida xa*
predeterminado de split
. Además, reemplace el ''4'' con el tamaño de línea de división que desee.
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 ayudar a hacer eso:
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