Csplit: una mejor manera de dividir archivos en Linux según su contenido

¿Cómo estais? aquí Kiko. Os traigo un nuevo post sobre el sistema operativo mas molón. Encantado linuxeros.

Cuando se trata de dividir un archivo de texto en varios archivos en Linux, la mayoría de la gente usa el comando split. No hay nada de malo con el comando split, excepto que depende del tamaño del byte o del tamaño de la línea para dividir los archivos.

Esto no es conveniente en situaciones en las que necesita dividir archivos en función de su contenido en lugar de su tamaño. Dejame darte un ejemplo.

Administro mis tweets planificados usando ЯМЛ archivos. 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 :
[...]

Cuando los importo a mi sistema, tengo que escribir cada tweet en mi propio archivo. Hago esto para evitar registrar tweets duplicados.

Pero, ¿cómo divido 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 
  > '

A pesar de su relativa simplicidad, dicha solución no es muy estable: por ejemplo, no cerré los diversos archivos fuente correctamente, por lo que esto puede alcanzar en gran medida el límite de archivos abiertos. ¿O qué pasa si olvidé el separador antes del primer tweet del archivo? Por supuesto, todo esto se puede procesar y ajustar en el script AWK, a costa de hacerlo más complejo. Pero, ¿por qué lidiar con eso cuando tenemos csplit herramienta para realizar esta tarea?

Use csplit para dividir archivos en Linux

El csplit la herramienta es prima de split herramienta que se puede utilizar para dividir un archivo en piezas de tamaño fijo. Pero csplit identificará los límites de las piezas en función del contenido del archivo, en lugar de utilizar el número de bytes.

En este tutorial, demostraré el uso de los comandos csplit y explicaré el resultado de este comando.

Por ejemplo, si quiero dividir mi archivo de tweets en función de ---- separador, podría escribir:

  sh$ csplit tweets.yaml /----/
  0
  10846

Puede que hayas adivinado csplit La herramienta utiliza la expresión regular proporcionada en la línea de comando para identificar el delimitador. Y que podrian ser estos 0 y 10983 resultado mostrado en la salida estándar? Bueno, tienen el tamaño en bytes de cada dato 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 estos xx00 y xx01 los nombres de archivo provienen? Y por qué csplit dividir el archivo en solo dos partes? ¿Y por qué el primer dato 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 --suffix-format y --prefix ajustes. Por ejemplo, podría cambiar la forma de 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 en un formato como el que usa la biblioteca C estándar. printf función. La mayoría de los caracteres de formulario se usarán literalmente, excepto para las especificaciones de conversión, que se ingresan con el signo de porcentaje (%) y que termina con un especificador de conversión (aquí, d). Entre ellos, el formato también puede contener varias banderas y opciones. En mi ejemplo %03d la especificación de conversión significa:

muestra el número de pista como un entero decimal (d), en un campo de tres caracteres (3), eventualmente alineado a la izquierda con ceros (0).

Pero eso no se aplica a las otras interrogaciones que tuve anteriormente: ¿por qué solo tenemos dos pistas, una de las cuales contiene cero bytes? Es posible que ya haya encontrado la respuesta a la última pregunta: mi archivo de datos comienza con ---- en su primera fila. Entonces, csplit lo miró como un separador, y como no había datos antes de esa línea, creó una primera pieza vacía. 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

Bien: no más archivos vacíos. Pero en cierto sentido, el resultado es el peor ahora, desde entonces. csplit divida el archivo en una sola pieza. Difícilmente podemos llamar a esto «dividir» un archivo, ¿verdad?

La explicación de este sorprendente resultado es csplit no implica en absoluto que cada plato deba separarse sobre la base del mismo separador. De hecho, csplit requiere que proporcione cualquier separador utilizado. Incluso si es el mismo varias veces:

  sh$ csplit tweets.yaml 
  >     --prefix='tweet.' --suffix-format="%03d.yaml" 
  >     --elide-empty-files 
  >     /----/ /----/ /----/
  170
  250
  10426

Puse tres delimitadores (idénticos) en la línea de comando. Entonces, csplit identifica el final de la primera pieza basándose en el primer separador. Esto da como resultado una pieza de cero bytes que se ha eliminado. La segunda pieza se distinguió de la siguiente línea. /----/. Conduce a una pieza de 170 bytes. Finalmente, se identificó una tercera pieza con una longitud de 250 bytes basada en el tercer separador. Los datos restantes, 10 426 bytes, se colocaron en la última pieza.

  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 de línea de comando como partes del archivo de datos. Especialmente porque este número exacto generalmente no se conoce de antemano. Por suerte, csplit hay un modelo especial que significa «repetir el modelo anterior tanto como sea posible». A pesar de la sintaxis que recuerda al cuantificador de estrellas en una expresión regular, está más cerca de Kleene plus concepto, ya que se utiliza para repetir un delimitador que ya se ha combinado 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 dividí mi colección de tweets en partes separadas. Sin embargo, lo hace csplip ¿tienes otros modelos agradables «especiales» como este? Bueno, no sé si podemos llamarlos «especiales», pero definitivamente csplit Entiendo más que modelos.

Más patrones para separar

En la sección anterior vimos cómo usar el cuantificador «*» para iteraciones no relacionadas. Sin embargo, al reemplazar la estrella por un número, puede solicitar el 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

Esto conduce a un caso angular interesante. ¿Qué se agregará si el número de iteraciones excede el número de delimitadores reales en el archivo de datos? Bueno, veamos esto, por 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ó un error, pero también eliminó todos los archivos creados durante el proceso. Presta especial atención a mi redacción: remoto ellos. Esto significa que los archivos se crearon cuando csplit encontró el error, los eliminó. En otras palabras, si ya tiene un archivo cuyo nombre parece un archivo de pieza, 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 reemplazado y luego eliminado de csplit.

Puede cambiar este comportamiento usando --keep-files opción. Como sugiere su nombre, no eliminará las piezas de csplit creadas después de 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 este caso y a pesar del error, csplit no rechazó 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? Okey, csplit hay soporte limitado para este uso %regex% modelo.

Omitir datos en csplit

Cuando se usa un signo de porcentaje (%) como separador de la expresión regular en lugar de una barra (/), csplit omitirá los datos (pero no los incluirá) en la primera línea correspondiente a la expresión regular. Esto puede resultar útil para ignorar algunas entradas, 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] »
      

      #Unix #Linux
      #YesIKnowIT

  ==> tweet.001.yaml <==
  ----
    status: |
      Want to know the oldest file in your disk?

      find / -type f -printf '%TFT%.8TT %pn' | 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] »
      

      #Unix #Linux
      #YesIKnowIT

  ==> tweet.001.yaml <==
  ----
    status: |
      Want to know the oldest file in your disk?

      find / -type f -printf '%TFT%.8TT %pn' | 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

Use compensaciones al dividir archivos con csplit

Cuando se utilizan expresiones regulares (o /…​/ o %…​%) puede especificar positivo (+N) o negativo (-N) compensar al final del modelo para que csplit dividirá el archivo N líneas antes o después de la línea correspondiente. Recuerda que en todos los casos el patrón determina el final de la pieza:

  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] »
      

      #Unix #Linux
      #YesIKnowIT
  ----

  ==> tweet.001.yaml <==
    status: |
      Want to know the oldest file in your disk?

      find / -type f -printf '%TFT%.8TT %pn' | 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
  ----

Dividido por el número de línea

Ya hemos visto cómo podemos usar una expresión regular para dividir archivos. En este caso, csplit dividirá el archivo en la primera línea correspondiente a esta expresión regular. Pero también puedes identificar la línea divisoria por su número, como veremos ahora.

Antes de cambiar a YAML, solía almacenar mis tweets planificados en archivo plano.

Se ha realizado un tweet de dos líneas en este archivo. Uno contiene una iteración opcional y el otro contiene el texto del tweet, con nuevas líneas reemplazadas por n. Una vez más este archivo de muestra está disponible en línea.

También pudo utilizar este formato de «tamaño fijo» 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?nnhttps://www.yesik.it/EP07n#Shell #Linux #Sedn#YesIKnowIT

  ==> tweet.002.txt <==
  
  Print the first column of a space-separated data file:nawk 'print $1' data.txt # Print out just the first columnnnFor some unknown reason, I find that easier to remember than:ncut -f1 data.txtnn#Linux #AWK #Cut

El ejemplo anterior parece fácil de entender, pero aquí hay dos trampas. Primero, 2 dado como argumento para csplit es un número de fila, no un número de fila. Sin embargo, cuando uso la repetición, como hice después del primer juego, csplit usará este número como el número de filas. Si no está claro, le permito comparar el resultado de los siguientes tres comandos:

  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. Es posible que haya notado la línea en blanco en la parte superior de tweets.txt ¿expediente? Esto lleva a esto tweet.000.txt una pieza que contiene solo el carácter de nueva línea. Desafortunadamente, en este ejemplo se requirió debido a la repetición: recuerde que quiero partes de dos líneas. De modo que 2 se requiere antes de la repetición. Pero también significa que la primera pieza se romperá, pero no la segunda fila. En otras palabras, la primera pieza contiene una línea. Todos los demás contendrán 2 líneas. Quizás puedas compartir tu opinión en la sección de comentarios, pero creo que fue una elección de diseño desafortunada.

Puede aliviar este problema yendo directamente a la primera línea no 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?nnhttps://www.yesik.it/EP07n#Shell #Linux #Sedn#YesIKnowIT

  ==> tweet.001.txt <==
  
  Print the first column of a space-separated data file:nawk 'print $1' data.txt # Print out just the first columnnnFor some unknown reason, I find that easier to remember than:ncut -f1 data.txtnn#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/TvW8DiEmTcQnn#Unix #Linuxn#YesIKnowIT

Leyendo de stdin

Por supuesto, como la mayoría de las herramientas de línea de comandos, csplit puede leer la entrada de su entrada estándar. En este 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?NNHTTPS://WWW.YESIK.IT/EP07N#SHELL #LINUX #SEDN#YESIKNOWIT

  ==> tweet.001.txt <==
  
  PRINT THE FIRST COLUMN OF A SPACE-SEPARATED DATA FILE:NAWK 'PRINT $1' DATA.TXT # PRINT OUT JUST THE FIRST COLUMNNNFOR SOME UNKNOWN REASON, I FIND THAT EASIER TO REMEMBER THAN:NCUT -F1 DATA.TXTNN#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/TVW8DIEMTCQNN#UNIX #LINUXN#YESIKNOWIT

  ==> tweet.003.txt <==
  
  WANT TO KNOW THE OLDEST FILE IN YOUR DISK?NNFIND / -TYPE F -PRINTF '%TFT%.8TT %PN' | SORT | LESSN(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 SEARCHN#UNIX #LINUX #SHELL #FIND
  
  FROM A POSIX SHELL `$OLDPWD` HOLDS THE NAME OF THE PREVIOUS WORKING DIRECTORY:NCD /TMPNECHO YOU ARE HERE: $PWDNECHO YOU WERE HERE: $OLDPWDNCD $OLDPWDNN#UNIX #LINUX #SHELL #CD
  
  FROM A POSIX SHELL, "CD" IS A SHORTHAND FOR CD $HOMEN#UNIX #LINUX #SHELL #CD
  
  HOW TO MOVE HUNDREDS OF FILES IN NO TIME?NUSING THE FIND COMMAND!NNHTTPS://YOUTU.BE/ZMEFXJYZAQKN#UNIX #LINUX #MOVE #FILES #FINDN#YESIKNOWIT

Y eso es casi todo lo que quería mostrarte hoy. Espero usar csplit para dividir archivos en Linux en el futuro. ¡Si te gustó este artículo y no olvides compartirlo y dar me gusta en tu red social favorita!

El Blog lo hacemos para colaborar y servir de referencia a la gente Linux. Esperamos que os guste.

Leave a Reply