Cuando se trata de dividir un archivo de texto en varios archivos en Linux, la mayoría de las personas usan el comando dividir. No hay nada malo con el comando dividir, excepto que se basa en el tamaño de bytes o el tamaño de línea para dividir los archivos.
Esto no es conveniente en situaciones en las que necesita dividir archivos según su contenido, en lugar de su tamaño. Déjame darte un ejemplo.
Administro mis tweets programados usando archivos YAML. Un archivo de tweet típico contiene varios tweets, separados por cuatro guiones:
----
event:
repeat: { days: 180 }
status: |
I think I use the `sed` command daily. And you?
https://www.yesik.it/EP07
#Shell #Linux #Sed #YesIKnowIT
----
status: |
Print the first column of a space-separated data file:
awk '{print $1}' data.txt # Print out just the first column
For some unknown reason, I find that easier to remember than:
cut -f1 data.txt
#Linux #AWK #Cut
----
status: |
For the #shell #beginners :
[...]
Al importarlos a mi sistema, necesito escribir cada tweet en su propio archivo. Lo hago para evitar registrar tweets duplicados.
Pero, ¿cómo dividir un archivo en varias partes en función de su contenido? Bueno, probablemente puedas obtener algo convincente usando los comandos awk:
sh$ awk < tweets.yaml '
> /----/ { OUTPUT="tweet." (N++) ".yaml" }
> { print > OUTPUT }
> '
Sin embargo, a pesar de una relativa simplicidad, dicha solución no es muy sólida:por ejemplo, no cerré correctamente los diversos archivos de salida, por lo que es muy posible que alcance el límite de archivos abiertos. ¿O qué pasa si olvidé el separador antes del primer tweet del archivo? Por supuesto, todo eso se puede manejar y arreglar en el script AWK, a costa de hacerlo más complejo. Pero, ¿por qué molestarse con eso cuando tenemos el csplit
? herramienta para realizar esa tarea?
Uso de csplit para dividir archivos en Linux
El csplit
la herramienta es prima de la split
herramienta que se puede utilizar para dividir un archivo en tamaño fijo trozos Pero csplit
identificará los límites de los fragmentos en función del contenido del archivo, en lugar de utilizar el recuento de bytes.
En este tutorial, demostraré el uso del comando csplit y también explicaré el resultado de este comando.
Entonces, por ejemplo, si quiero dividir mi archivo de tweet según el ----
delimitador, podría escribir:
sh$ csplit tweets.yaml /----/
0
10846
Puede que hayas adivinado el csplit
La herramienta usó la expresión regular proporcionada en la línea de comando para identificar el separador. ¿Y cuáles podrían ser esos 0
y 10983
resultado mostrado en la salida estándar? Bueno, son del tamaño en bytes de cada fragmento de datos creado.
sh$ ls -l xx0*
-rw-r--r-- 1 sylvain sylvain 0 Jun 6 11:30 xx00
-rw-r--r-- 1 sylvain sylvain 10846 Jun 6 11:30 xx01
¡Espera un minuto! Donde esos xx00
y xx01
¿De dónde provienen los nombres de archivo? ¿Y por qué csplit
dividir el archivo en solo dos fragmentos ? ¿Y por qué el primer fragmento de datos tiene una longitud de cero bytes? ?
La respuesta a la primera pregunta es simple:xxNN
(o más formalmente xx%02d
) es el formato de nombre de archivo predeterminado utilizado por csplit
. Pero puedes cambiar eso usando el --suffix-format
y --prefix
opciones Por ejemplo, podría cambiar el formato a algo más significativo para mis necesidades:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> /----/
0
10846
sh$ ls -l tweet.*
-rw-r--r-- 1 sylvain sylvain 0 Jun 6 11:30 tweet.000.yaml
-rw-r--r-- 1 sylvain sylvain 10846 Jun 6 11:30 tweet.001.yaml
El prefijo es una cadena simple, pero el sufijo es una cadena de formato como la que usa la biblioteca C estándar printf
función. La mayoría de los caracteres del formato se usarán textualmente, excepto las especificaciones de conversión que se introducen con el signo de porcentaje (%
) y que termina con un especificador de conversión (aquí, d
). En el medio, el formato también puede contener varias banderas y opciones. En mi ejemplo, el %03d
especificación de conversión significa:
- muestra el número de fragmento como un entero decimal (
d
), - en un campo de tres caracteres de ancho (
3
), - eventualmente rellenado a la izquierda con ceros (
0
).
Pero eso no aborda los otros interrogantes que tenía anteriormente:¿por qué tenemos solo dos trozos, uno de ellos contiene cero bytes? Tal vez ya hayas encontrado la respuesta a esa última pregunta por ti mismo:mi archivo de datos comienza con ----
en su primera línea. Entonces, csplit
lo consideró como un delimitador, y dado que no había datos antes de esa línea, creó un primer fragmento vacío. Podemos deshabilitar la creación de archivos de longitud de cero bytes usando --elide-empty-files
opción:
sh$ rm tweet.*
rm: cannot remove 'tweet.*': No such file or directory
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/
10846
sh$ ls -l tweet.*
-rw-r--r-- 1 sylvain sylvain 10846 Jun 6 11:30 tweet.000.yaml
Ok:no más archivos vacíos. Pero en cierto sentido, el resultado es peor ahora, ya que csplit
dividir el archivo en solo uno pedazo. Apenas podemos llamar a eso "dividir" un archivo, ¿no?
La explicación de ese sorprendente resultado es csplit
no asuma que cada mandril debe dividirse en función del mismo separador. En realidad, csplit
requiere que proporcione cada separador utilizado. Aunque sea varias veces lo mismo:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/ /----/ /----/
170
250
10426
He puesto tres separadores (idénticos) en la línea de comando. Entonces, csplit
identificó el final del primer fragmento en función del primer separador. Conduce a un fragmento de longitud de cero bytes que se elidió. El segundo fragmento estaba delimitado por la siguiente línea que coincidía con /----/
. Conduce a un fragmento de 170 bytes. Finalmente, se identificó un tercer fragmento de 250 bytes de longitud en función del tercer separador. Los datos restantes, 10426 bytes, se colocaron en el último fragmento.
sh$ ls -l tweet.???.yaml
-rw-r--r-- 1 sylvain sylvain 170 Jun 6 11:30 tweet.000.yaml
-rw-r--r-- 1 sylvain sylvain 250 Jun 6 11:30 tweet.001.yaml
-rw-r--r-- 1 sylvain sylvain 10426 Jun 6 11:30 tweet.002.yaml
Obviamente, no sería práctico si tuviéramos que proporcionar tantos separadores en la línea de comando como fragmentos hay en el archivo de datos. Sobre todo porque ese número exacto generalmente no se conoce de antemano. Afortunadamente, csplit
tiene un patrón especial que significa “repetir el patrón anterior tanto como sea posible”. A pesar de que su sintaxis recuerda al cuantificador estrella en una expresión regular, se acerca más al concepto de Kleene plus ya que se usa para repetir un separador que ya se ha emparejado una vez:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/ '{*}'
170
250
190
208
140
[...]
247
285
194
214
185
131
316
221
Y esta vez, finalmente, he dividido mi colección de tweets en partes individuales. Sin embargo, csplip
¿Tienes otros bonitos patrones "especiales" como ese? Bueno, no sé si podemos llamarlos "especiales", pero definitivamente, csplit
entender más de los patrones.
Más patrones csplit
Acabamos de ver en la sección anterior cómo usar el cuantificador '{*}' para repeticiones no enlazadas. Sin embargo, al reemplazar la estrella con un número, puede solicitar un número exacto de repeticiones:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/ '{6}'
170
250
190
208
140
216
9672
Eso lleva a un caso de esquina interesante. ¿Qué agregaría si el número de repeticiones excediera el número de delimitadores reales en el archivo de datos? Bueno, veamos eso en un ejemplo:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/ '{999}'
csplit: ‘/----/’: match not found on repetition 62
170
250
190
208
[...]
91
247
285
194
214
185
131
316
221
sh$ ls tweet.*
ls: cannot access 'tweet.*': No such file or directory
Curiosamente, no solo csplit
informó de un error, pero también eliminó todos los archivos de fragmentos creados durante el proceso. Preste especial atención a mi redacción:se eliminó a ellos. Eso significa que los archivos se crearon cuando csplit
encontró el error, los borró. En otras palabras, si ya tiene un archivo cuyo nombre parece un archivo fragmentado, se eliminará:
sh$ touch tweet.002.yaml
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/ '{999}'
csplit: ‘/----/’: match not found on repetition 62
170
250
190
[...]
87
91
247
285
194
214
185
131
316
221
sh$ ls tweet.*
ls: cannot access 'tweet.*': No such file or directory
En el ejemplo anterior, tweet.002.yaml
El archivo que creamos manualmente fue sobrescrito y luego eliminado por csplit
.
Puede cambiar ese comportamiento usando --keep-files
opción. Como su nombre lo indica, no eliminará los fragmentos creados por csplit después de encontrar un error:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> --keep-files \
> /----/ '{999}'
csplit: ‘/----/’: match not found on repetition 62
170
250
190
[...]
316
221
sh$ ls tweet.*
tweet.000.yaml
tweet.001.yaml
tweet.002.yaml
tweet.003.yaml
[...]
tweet.058.yaml
tweet.059.yaml
tweet.060.yaml
tweet.061.yaml
Observe en ese caso, y a pesar del error, csplit
no descartó ningún dato:
sh$ diff -s tweets.yaml <(cat tweet.*)
Files tweets.yaml and /dev/fd/63 are identical
Pero, ¿qué pasa si hay algunos datos en el archivo que quiero descartar? Bueno, csplit
tiene un soporte limitado para eso usando un %regex%
patrón.
Omitir datos en csplit
Cuando se usa un signo de porcentaje (%
) como delimitador de expresiones regulares en lugar de una barra inclinada (/
), csplit
se saltará datos hasta (pero sin incluir) la primera línea que coincida con la expresión regular. Esto puede ser útil para ignorar algunos registros, especialmente al principio o al final del archivo de entrada:
sh$ # Keep only the first two tweets
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> --keep-files \
> /----/ '{2}' %----% '{*}'
170
250
sh$ head tweet.00[012].yaml
==> tweet.000.yaml <==
----
event:
repeat: { days: 180 }
status: |
I think I use the `sed` command daily. And you?
https://www.yesik.it/EP07
#Shell #Linux #Sed #YesIKnowIT
==> tweet.001.yaml <==
----
status: |
Print the first column of a space-separated data file:
awk '{print $1}' data.txt # Print out just the first column
For some unknown reason, I find that easier to remember than:
cut -f1 data.txt
#Linux #AWK #Cut
sh$ # Skip the first two tweets
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> --keep-files \
> %----% '{2}' /----/ '{2}'
190
208
140
9888
sh$ head tweet.00[012].yaml
==> tweet.000.yaml <==
----
status: |
For the #shell #beginners :
« #GlobPatterns : how to move hundreds of files in not time [1/3] »
https://youtu.be/TvW8DiEmTcQ
#Unix #Linux
#YesIKnowIT
==> tweet.001.yaml <==
----
status: |
Want to know the oldest file in your disk?
find / -type f -printf '%TFT%.8TT %p\n' | sort | less
(should work on any Single UNIX Specification compliant system)
#UNIX #Linux
==> tweet.002.yaml <==
----
status: |
When using the find command, use `-iname` instead of `-name` for case-insensitive search
#Unix #Linux #Shell #Find
sh$ # Keep only the third and fourth tweets
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> --keep-files \
> %----% '{2}' /----/ '{2}' %----% '{*}'
190
208
140
sh$ head tweet.00[012].yaml
==> tweet.000.yaml <==
----
status: |
For the #shell #beginners :
« #GlobPatterns : how to move hundreds of files in not time [1/3] »
https://youtu.be/TvW8DiEmTcQ
#Unix #Linux
#YesIKnowIT
==> tweet.001.yaml <==
----
status: |
Want to know the oldest file in your disk?
find / -type f -printf '%TFT%.8TT %p\n' | sort | less
(should work on any Single UNIX Specification compliant system)
#UNIX #Linux
==> tweet.002.yaml <==
----
status: |
When using the find command, use `-iname` instead of `-name` for case-insensitive search
#Unix #Linux #Shell #Find
Uso de compensaciones al dividir archivos con csplit
Al usar expresiones regulares (ya sea /…/
o %…%
) puede especificar un positivo (+N
) o negativo (-N
) compensado al final del patrón por lo que csplit
dividirá el archivo N líneas después o antes de la línea coincidente. Recuerde, en todos los casos, el patrón especifica el final del trozo:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> --keep-files \
> %----%+1 '{2}' /----/+1 '{2}' %----% '{*}'
190
208
140
sh$ head tweet.00[012].yaml
==> tweet.000.yaml <==
status: |
For the #shell #beginners :
« #GlobPatterns : how to move hundreds of files in not time [1/3] »
https://youtu.be/TvW8DiEmTcQ
#Unix #Linux
#YesIKnowIT
----
==> tweet.001.yaml <==
status: |
Want to know the oldest file in your disk?
find / -type f -printf '%TFT%.8TT %p\n' | sort | less
(should work on any Single UNIX Specification compliant system)
#UNIX #Linux
----
==> tweet.002.yaml <==
status: |
When using the find command, use `-iname` instead of `-name` for case-insensitive search
#Unix #Linux #Shell #Find
----
Dividir por número de línea
Ya hemos visto cómo podemos usar una expresión regular para dividir archivos. En ese caso, csplit
dividirá el archivo en la primera línea coincidencia esa expresión regular. Pero también puede identificar la línea dividida por su número de línea como lo veremos ahora.
Antes de cambiar a YAML, solía almacenar mis tweets programados en un archivo plano.
En ese archivo se hizo un tuit de dos líneas. Uno que contiene una repetición opcional y el segundo que contiene el texto del tweet, con saltos de línea reemplazados por \n. Una vez más, ese archivo de muestra está disponible en línea.
Con ese formato de "tamaño fijo" también pude usar csplit
para poner cada tweet individual en su propio archivo:
sh$ csplit tweets.txt \
> --prefix='tweet.' --suffix-format='%03d.txt' \
> --elide-empty-files \
> --keep-files \
> 2 '{*}'
csplit: ‘2’: line number out of range on repetition 62
1
123
222
161
182
119
184
81
148
128
142
101
107
[...]
sh$ diff -s tweets.txt <(cat tweet.*.txt)
Files tweets.txt and /dev/fd/63 are identical
sh$ head tweet.00[012].txt
==> tweet.000.txt <==
==> tweet.001.txt <==
{ days:180 }
I think I use the `sed` command daily. And you?\n\nhttps://www.yesik.it/EP07\n#Shell #Linux #Sed\n#YesIKnowIT
==> tweet.002.txt <==
{}
Print the first column of a space-separated data file:\nawk '{print $1}' data.txt # Print out just the first column\n\nFor some unknown reason, I find that easier to remember than:\ncut -f1 data.txt\n\n#Linux #AWK #Cut
El ejemplo anterior parece fácil de entender, pero hay dos trampas aquí. Primero, el 2
dado como argumento para csplit
es una línea número , no una línea recuento . Sin embargo, al usar una repetición como hice yo, después de la primera coincidencia, csplit
utilizará ese número como una línea recuento . Si no está claro, te dejo comparar el resultado de los tres comandos siguientes:
sh$ csplit tweets.txt --keep-files 2 2 2 2 2
csplit: warning: line number ‘2’ is the same as preceding line number
csplit: warning: line number ‘2’ is the same as preceding line number
csplit: warning: line number ‘2’ is the same as preceding line number
csplit: warning: line number ‘2’ is the same as preceding line number
1
0
0
0
0
9030
sh$ csplit tweets.txt --keep-files 2 4 6 8 10
1
123
222
161
182
8342
sh$ csplit tweets.txt --keep-files 2 '{4}'
1
123
222
161
182
8342
Mencioné una segunda trampa, algo relacionada con la primera. Tal vez notó la línea vacía en la parte superior de tweets.txt
¿expediente? Conduce a ese tweet.000.txt
fragmento que contiene solo el carácter de nueva línea. Desafortunadamente, se requería en ese ejemplo debido a la repetición:recuerda que quiero dos líneas trozos Entonces el 2
es obligatorio antes de la repetición. Pero eso también significa el primero el fragmento se romperá, pero sin incluir , la línea dos. En otras palabras, el primer fragmento contiene una línea. Todos los demás contendrán 2 líneas. Tal vez podría compartir su opinión en la sección de comentarios, pero creo que esta fue una elección de diseño desafortunada.
Puede mitigar ese problema saltando directamente a la primera línea que no esté vacía:
sh$ csplit tweets.txt \
> --prefix='tweet.' --suffix-format='%03d.txt' \
> --elide-empty-files \
> --keep-files \
> %.% 2 '{*}'
csplit: ‘2’: line number out of range on repetition 62
123
222
161
[...]
sh$ head tweet.00[012].txt
==> tweet.000.txt <==
{ days:180 }
I think I use the `sed` command daily. And you?\n\nhttps://www.yesik.it/EP07\n#Shell #Linux #Sed\n#YesIKnowIT
==> tweet.001.txt <==
{}
Print the first column of a space-separated data file:\nawk '{print $1}' data.txt # Print out just the first column\n\nFor some unknown reason, I find that easier to remember than:\ncut -f1 data.txt\n\n#Linux #AWK #Cut
==> tweet.002.txt <==
{}
For the #shell #beginners :\n« #GlobPatterns : how to move hundreds of files in not time [1/3] »\nhttps://youtu.be/TvW8DiEmTcQ\n\n#Unix #Linux\n#YesIKnowIT
Lectura desde stdin
Por supuesto, como la mayoría de las herramientas de línea de comandos, csplit
puede leer los datos de entrada desde su entrada estándar. En ese caso, debe especificar -
como nombre de archivo de entrada:
sh$ tr [:lower:] [:upper:] < tweets.txt | csplit - \
> --prefix='tweet.' --suffix-format='%03d.txt' \
> --elide-empty-files \
> --keep-files \
> %.% 2 '{3}'
123
222
161
8524
sh$ head tweet.???.txt
==> tweet.000.txt <==
{ DAYS:180 }
I THINK I USE THE `SED` COMMAND DAILY. AND YOU?\N\NHTTPS://WWW.YESIK.IT/EP07\N#SHELL #LINUX #SED\N#YESIKNOWIT
==> tweet.001.txt <==
{}
PRINT THE FIRST COLUMN OF A SPACE-SEPARATED DATA FILE:\NAWK '{PRINT $1}' DATA.TXT # PRINT OUT JUST THE FIRST COLUMN\N\NFOR SOME UNKNOWN REASON, I FIND THAT EASIER TO REMEMBER THAN:\NCUT -F1 DATA.TXT\N\N#LINUX #AWK #CUT
==> tweet.002.txt <==
{}
FOR THE #SHELL #BEGINNERS :\N« #GLOBPATTERNS : HOW TO MOVE HUNDREDS OF FILES IN NOT TIME [1/3] »\NHTTPS://YOUTU.BE/TVW8DIEMTCQ\N\N#UNIX #LINUX\N#YESIKNOWIT
==> tweet.003.txt <==
{}
WANT TO KNOW THE OLDEST FILE IN YOUR DISK?\N\NFIND / -TYPE F -PRINTF '%TFT%.8TT %P\N' | SORT | LESS\N(SHOULD WORK ON ANY SINGLE UNIX SPECIFICATION COMPLIANT SYSTEM)\N#UNIX #LINUX
{}
WHEN USING THE FIND COMMAND, USE `-INAME` INSTEAD OF `-NAME` FOR CASE-INSENSITIVE SEARCH\N#UNIX #LINUX #SHELL #FIND
{}
FROM A POSIX SHELL `$OLDPWD` HOLDS THE NAME OF THE PREVIOUS WORKING DIRECTORY:\NCD /TMP\NECHO YOU ARE HERE: $PWD\NECHO YOU WERE HERE: $OLDPWD\NCD $OLDPWD\N\N#UNIX #LINUX #SHELL #CD
{}
FROM A POSIX SHELL, "CD" IS A SHORTHAND FOR CD $HOME\N#UNIX #LINUX #SHELL #CD
{}
HOW TO MOVE HUNDREDS OF FILES IN NO TIME?\NUSING THE FIND COMMAND!\N\NHTTPS://YOUTU.BE/ZMEFXJYZAQK\N#UNIX #LINUX #MOVE #FILES #FIND\N#YESIKNOWIT
Y eso es todo lo que quería mostrarles hoy. Espero que en el futuro use csplit para dividir archivos en Linux. Si te ha gustado este artículo, ¡no olvides compartirlo y darle me gusta en tu red social favorita!