GNU/Linux >> Linux Esercitazione >  >> Linux

csplit:un modo migliore per dividere il file in Linux in base al suo contenuto

Quando si tratta di dividere un file di testo in più file in Linux, la maggior parte delle persone usa il comando split. Non c'è niente di sbagliato nel comando split, tranne che si basa sulla dimensione del byte o sulla dimensione della riga per dividere i file.

Questo non è conveniente nelle situazioni in cui è necessario dividere i file in base al contenuto, anziché alle dimensioni. Ti faccio un esempio.

Gestisco i miei tweet programmati utilizzando i file YAML. Un tipico file tweet contiene diversi tweet, separati da quattro trattini:

  ----
    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 :
[...]

Quando li importo nel mio sistema, devo scrivere ogni tweet nel proprio file. Lo faccio per evitare di registrare tweet duplicati.

Ma come dividere un file in più parti in base al suo contenuto? Bene, probabilmente puoi ottenere qualcosa di convincente usando i comandi awk:

  sh$ awk < tweets.yaml '
  >     /----/ { OUTPUT="tweet." (N++) ".yaml" }
  >     { print > OUTPUT }
  > '

Tuttavia, nonostante una relativa semplicità, una soluzione del genere non è molto robusta:ad esempio, non ho chiuso correttamente i vari file di output, quindi questo potrebbe benissimo raggiungere il limite di file aperti. O se ho dimenticato il separatore prima del primo tweet del file? Naturalmente, tutto ciò può essere gestito e corretto nello script AWK, a scapito di renderlo più complesso. Ma perché preoccuparsene quando abbiamo il csplit strumento per svolgere tale compito?

Utilizzo di csplit per dividere i file in Linux

Il csplit strumento è un cugino di split strumento che può essere utilizzato per dividere un file in dimensioni fisse pezzi. Ma csplit identificherà i limiti del blocco in base al contenuto del file, anziché utilizzare il conteggio dei byte.

In questo tutorial, dimostrerò l'utilizzo del comando csplit e spiegherò anche l'output di questo comando.

Quindi, ad esempio, se voglio dividere il mio file tweet in base al ---- delimitatore, potrei scrivere:

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

Potresti aver indovinato il csplit strumento ha utilizzato l'espressione regolare fornita sulla riga di comando per identificare il separatore. E quali potrebbero essere quei 0 e 10983 risultato visualizzato sullo standard output? Bene, sono la dimensione in byte di ogni blocco di dati creato.

  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

Apetta un minuto! Dove quei xx00 e xx01 da cui provengono i nomi dei file? E perché csplit dividere il file in solo due blocchi ? E perché il primo blocco di dati ha una lunghezza di zero byte ?

La risposta alla prima domanda è semplice:xxNN (o più formalmente xx%02d ) è il formato del nome file predefinito utilizzato da csplit . Ma puoi cambiarlo usando il --suffix-format e --prefix opzioni. Ad esempio, potrei cambiare il formato in qualcosa di più significativo per le mie esigenze:

  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

Il prefisso è una semplice stringa, ma il suffisso è una stringa di formato come quella usata dalla libreria C standard printf funzione. La maggior parte dei caratteri del formato verrà utilizzata alla lettera, ad eccezione delle specifiche di conversione introdotte dal segno di percentuale (% ) e che termina con un identificatore di conversione (qui, d ). Nel mezzo, il formato può anche contenere vari flag e opzioni. Nel mio esempio, il %03d specifica di conversione significa:

  • visualizza il numero del blocco come numero intero decimale (d ),
  • in un campo di tre caratteri (3 ),
  • alla fine riempito a sinistra con zeri (0 ).

Ma questo non riguarda gli altri interrogatori che ho avuto sopra:perché ne abbiamo solo due blocchi, uno dei quali contenente zero byte? Forse hai già trovato la risposta a quest'ultima domanda da solo:il mio file di dati inizia con ---- sulla sua prima riga. Quindi, csplit lo considerava un delimitatore e poiché non c'erano dati prima di quella riga, ha creato un primo blocco vuoto. Possiamo disabilitare la creazione di file di lunghezza zero byte usando il --elide-empty-files opzione:

  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:niente più file vuoti. Ma in un certo senso, il risultato è il peggiore ora, da csplit dividere il file in solo uno pezzo. Riusciamo a malapena a chiamare questo "suddivisione" di un file, vero?

La spiegazione di quel risultato sorprendente è csplit non supponiamo che ogni mandrino debba essere diviso in base allo stesso separatore. In realtà, csplit richiede di fornire ogni separatore utilizzato. Anche se è più volte uguale:

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

Ho inserito tre separatori (identici) sulla riga di comando. Quindi, csplit identificato la fine del primo blocco in base al primo separatore. Porta a un blocco di lunghezza zero byte che è stato eliminato. Il secondo blocco è stato delimitato dalla riga successiva che corrisponde a /----/ . Porta a un blocco di 170 byte. Infine, è stato identificato un terzo blocco di 250 byte basato sul terzo separatore. I dati rimanenti, 10426 byte, sono stati inseriti nell'ultimo blocco.

  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

Ovviamente, non sarebbe pratico se dovessimo fornire tanti separatori sulla riga di comando quanti sono i blocchi nel file di dati. Soprattutto perché quel numero esatto di solito non è noto in anticipo. Fortunatamente, csplit ha un pattern speciale che significa "ripetere il pattern precedente il più possibile." Nonostante la sua sintassi ricorda il quantificatore di stelle in un'espressione regolare, questo è più vicino al concetto di Kleene plus poiché viene utilizzato per ripetere un separatore che ha già stato abbinato una volta:

  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

E questa volta, finalmente, ho diviso la mia raccolta di tweet in singole parti. Tuttavia, csplip hai altri bei modelli "speciali" come quello? Bene, non so se possiamo chiamarli "speciali", ma sicuramente, csplit capire di più sui modelli.

Altri pattern csplit

Abbiamo appena visto nella sezione precedente come utilizzare il quantificatore '{*}' per le ripetizioni non legate. Tuttavia, sostituendo la stella con un numero, puoi richiedere un numero esatto di ripetizioni:

  sh$ csplit tweets.yaml \
  >     --prefix='tweet.' --suffix-format='%03d.yaml' \
  >     --elide-empty-files \
  >     /----/ '{6}'
  170
  250
  190
  208
  140
  216
  9672

Questo porta a un interessante caso d'angolo. Cosa verrebbe aggiunto se il numero di ripetizioni superasse il numero di delimitatori effettivi nel file di dati? Bene, vediamolo su un esempio:

  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

È interessante notare che non solo csplit ha segnalato un errore, ma ha anche rimosso tutto i file chunk creati durante il processo. Presta particolare attenzione alla mia formulazione:è rimossa loro. Ciò significa che i file sono stati creati, quindi, quando csplit riscontrato l'errore, li ha cancellati. In altre parole, se hai già un file il cui nome sembra un file chunk, verrà rimosso:

  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

Nell'esempio sopra, il tweet.002.yaml il file che abbiamo creato manualmente è stato sovrascritto, quindi rimosso da csplit .

Puoi cambiare quel comportamento usando il --keep-files opzione. Come suggerisce il nome, non rimuoverà i blocchi creati da csplit dopo aver riscontrato un errore:

  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

Nota in tal caso, e nonostante l'errore, csplit non ha scartato alcun dato:

  sh$ diff -s tweets.yaml <(cat tweet.*)
  Files tweets.yaml and /dev/fd/63 are identical

Ma cosa succede se ci sono dei dati nel file che voglio scartare? Bene, csplit ha un supporto limitato per l'utilizzo di un %regex% modello.

Salto dei dati in csplit

Quando si utilizza un segno di percentuale (% ) come delimitatore regex invece di una barra (/ ), csplit salta dati fino a (ma esclusa) la prima riga che corrisponde all'espressione regolare. Questo può essere utile per ignorare alcuni record, specialmente all'inizio o alla fine del file di input:

  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

Utilizzo degli offset durante la divisione dei file con csplit

Quando si utilizzano espressioni regolari (o /…​/ o %…​% ) puoi specificare un positivo (+N ) o negativo (-N ) offset alla fine del pattern, quindi csplit dividerà il file N righe dopo o prima della riga corrispondente. Ricorda, in tutti i casi, il modello specifica la fine del pezzo:

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

Dividi per numero di riga

Abbiamo già visto come possiamo usare un'espressione regolare per dividere i file. In tal caso, csplit dividerà il file alla prima riga corrispondente quella regex. Ma puoi anche identificare la linea divisa in base al suo numero di linea come la vedremo ora.

Prima di passare a YAML, memorizzavo i miei tweet programmati in un file flat.

In quel file, un tweet era composto da due righe. Uno contenente una ripetizione facoltativa e il secondo contenente il testo del tweet, con nuove righe sostituite da \n. Ancora una volta quel file di esempio è disponibile online.

Con quel formato a "dimensione fissa" è stato anche possibile utilizzare csplit per inserire ogni singolo tweet nel proprio file:

  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

L'esempio sopra sembra facile da capire, ma ci sono due insidie ​​qui. Innanzitutto, il 2 dato come argomento a csplit è una riga numero , non una riga conta . Tuttavia, quando utilizzo una ripetizione come ho fatto, dopo la prima corrispondenza, csplit utilizzerà quel numero come riga count . Se non è chiaro, ti lascio confrontare l'output dei tre seguenti comandi:

  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

Ho accennato a una seconda trappola, in qualche modo collegata alla prima. Forse hai notato la riga vuota nella parte superiore del tweets.txt file? Porta a quel tweet.000.txt pezzo che contiene solo il carattere di nuova riga. Sfortunatamente, in quell'esempio era richiesto a causa della ripetizione:ricorda che voglio due righe pezzi. Quindi il 2 è obbligatorio prima della ripetizione. Ma questo significa anche il primo chunk si interromperà, ma non include , la riga due. In altre parole, il primo pezzo contiene una riga. Tutti gli altri conterranno 2 righe. Forse potresti condividere la tua opinione nella sezione commenti, ma per quanto mi riguarda penso che questa sia stata una scelta di design sfortunata.

Puoi mitigare questo problema saltando direttamente alla prima riga non vuota:

  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

Lettura da stdin

Ovviamente, come la maggior parte degli strumenti a riga di comando, csplit può leggere i dati di input dal suo input standard. In tal caso, devi specificare - come nome del file di input:

  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

Ed è praticamente tutto ciò che volevo mostrarti oggi. Spero che in futuro utilizzerai csplit per dividere i file in Linux. Se ti è piaciuto questo articolo e non dimenticare di condividerlo e metterlo mi piace sul tuo social network preferito!


Linux
  1. Linux:tutto è un file?

  2. 9 esempi utili del comando Split in Linux

  3. Come dividere iso o file usando il comando "split" in Linux

  4. il modo più veloce per convertire file delimitati da tabulazioni in csv in linux

  5. Il modo più efficiente per copiare un file in Linux

Come modificare un file senza modificarne i timestamp in Linux

Come trovare file basati su timestamp in Linux

Perché Linux è così cattivo e Windows 11 è migliore in ogni singolo modo?

5 comandi per visualizzare il contenuto di un file nella riga di comando di Linux

Findmnt - Un modo migliore per trovare filesystem montati su Linux

Tutorial Tripwire:Sistema di rilevamento delle intrusioni basato su host Linux