GNU/Linux >> Tutoriales Linux >  >> Linux

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

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!


Linux
  1. 5 formas de vaciar o eliminar el contenido de un archivo grande en Linux

  2. Linux – ¿Todo es un archivo?

  3. 9 ejemplos útiles del comando Split en Linux

  4. Introducción a Linux KVM (virtualización basada en kernel) y sus beneficios

  5. ¿Cómo puedo crear un archivo tar de varias partes en Linux?

Cómo editar un archivo sin cambiar sus marcas de tiempo en Linux

Cómo encontrar archivos basados ​​en la marca de tiempo en Linux

¿Por qué Linux es tan malo y Windows 11 mejor en todos los sentidos?

5 comandos para ver el contenido de un archivo en la línea de comandos de Linux

Findmnt - Mejor manera de encontrar sistemas de archivos montados en Linux

Tutorial de Tripwire:Sistema de detección de intrusos basado en host Linux