Pokud jde o rozdělení textového souboru do více souborů v Linuxu, většina lidí používá příkaz split. Na příkazu split není nic špatného, kromě toho, že se při rozdělování souborů spoléhá na velikost bajtu nebo velikost řádku.
To není vhodné v situacích, kdy potřebujete rozdělit soubory na základě jejich obsahu, nikoli velikosti. Dovolte mi uvést příklad.
Své naplánované tweety spravuji pomocí souborů YAML. Typický soubor tweetu obsahuje několik tweetů oddělených čtyřmi pomlčkami:
----
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 :
[...]
Když je importuji do svého systému, musím zapsat každý tweet do vlastního souboru. Dělám to, abych se vyhnul registraci duplicitních tweetů.
Jak ale rozdělit soubor na několik částí na základě jeho obsahu? No, pravděpodobně můžete získat něco přesvědčivého pomocí příkazů awk:
sh$ awk < tweets.yaml '
> /----/ { OUTPUT="tweet." (N++) ".yaml" }
> { print > OUTPUT }
> '
Navzdory relativní jednoduchosti však takové řešení není příliš robustní:například jsem správně neuzavřel různé výstupní soubory, takže to může velmi dobře dosáhnout limitu otevřených souborů. Nebo co když jsem zapomněl oddělovač před úplně prvním tweetem souboru? Samozřejmě, že to vše lze zvládnout a opravit ve skriptu AWK, na úkor jeho komplexnosti. Ale proč se s tím obtěžovat, když máme csplit
nástroj k provedení tohoto úkolu?
Použití csplit k rozdělení souborů v Linuxu
csplit
nástroj je bratrancem split
nástroj, který lze použít k rozdělení souboru na pevnou velikost kousky. Ale csplit
bude identifikovat hranice bloků na základě obsahu souboru, spíše než pomocí počtu bajtů.
V tomto tutoriálu předvedu použití příkazu csplit a také vysvětlím výstup tohoto příkazu.
Pokud tedy například chci rozdělit svůj tweetový soubor na základě ----
oddělovač, mohl bych napsat:
sh$ csplit tweets.yaml /----/
0
10846
Možná jste uhodli csplit
nástroj použil k identifikaci oddělovače regulární výraz uvedený na příkazovém řádku. A co by mohlo být těch 0
a 10983
výsledek zobrazen na standardním výstupu? Mají velikost v bajtech každého vytvořeného kusu dat.
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
Počkej chvíli! Kde jsou tyto xx00
a xx01
názvy souborů pocházejí? A proč csplit
rozdělit soubor na pouze dva bloky ? A proč má první datový blok délku nula bajtů ?
Odpověď na první otázku je jednoduchá:xxNN
(nebo formálněji xx%02d
) je výchozí formát souboru používaný csplit
. Ale můžete to změnit pomocí --suffix-format
a --prefix
možnosti. Mohl bych například změnit formát na něco smysluplnějšího pro mé potřeby:
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
Předpona je prostý řetězec, ale přípona je formátovací řetězec, jaký používá standardní knihovna C printf
funkce. Většina znaků ve formátu bude použita doslovně, s výjimkou specifikací převodu, které jsou uvedeny znakem procenta (%
) a který končí specifikátorem konverze (zde d
). Mezi tím může formát obsahovat také různé příznaky a možnosti. V mém příkladu %03d
specifikace konverze znamená:
- zobrazit číslo bloku jako celé desítkové číslo (
d
), - v poli o šířce tří znaků (
3
), - nakonec doplněno vlevo nulami (
0
).
Ale to se netýká ostatních výslechů, které jsem měl výše:proč máme jen dva chunks, jeden z nich obsahuje nulu bajtů? Možná jste již sami našli odpověď na tuto otázku:můj datový soubor začíná ----
na svém prvním řádku. Takže csplit
považoval to za oddělovač, a protože před tímto řádkem nebyla žádná data, vytvořil prázdný první blok. Vytváření souborů s nulovou délkou bajtů můžeme zakázat pomocí --elide-empty-files
možnost:
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:žádné další prázdné soubory. Ale v jistém smyslu je teď výsledek nejhorší, protože csplit
rozdělit soubor pouze na jeden kus. Tomu sotva můžeme říkat „rozdělení“ souboru, že?
Vysvětlení tohoto překvapivého výsledku je csplit
ne vůbec předpokládejme, že každý sklíčidlo by mělo být rozděleno na základě stejného oddělovač. Vlastně csplit
vyžaduje, abyste poskytli každý použitý oddělovač. I když je to několikrát stejné:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/ /----/ /----/
170
250
10426
Na příkazový řádek jsem vložil tři (identické) oddělovače. Takže csplit
identifikoval konec prvního bloku na základě prvního oddělovače. Vede k bloku délky nula bajtů, který byl elidován. Druhý blok byl oddělen dalším řádkem odpovídajícím /----/
. To vede k bloku o velikosti 170 bajtů. Nakonec byl na základě třetího oddělovače identifikován třetí blok o délce 250 bajtů. Zbývající data, 10426 bajtů, byla vložena do posledního bloku.
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
Je zřejmé, že by nebylo praktické, kdybychom museli na příkazovém řádku poskytnout tolik oddělovačů, kolik je kusů v datovém souboru. Zejména proto, že přesné číslo obvykle není předem známo. Naštěstí csplit
má speciální vzor s významem „co nejvíce opakujte předchozí vzor.“ Navzdory své syntaxi připomínající hvězdný kvantifikátor v regulárním výrazu se tento koncept blíží konceptu Kleene plus, protože se používá k opakování oddělovače, který již se shoduje jednou:
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
A tentokrát jsem konečně svou sbírku tweetů rozdělil na jednotlivé části. Nicméně csplip
máte nějaké další pěkné "speciální" vzory, jako je tento? No, nevím, jestli je můžeme nazvat „speciálními“, ale rozhodně csplit
pochopit více vzorců.
Další vzory csplit
Právě jsme viděli v předchozí části, jak použít kvantifikátor „{*}“ pro nevázaná opakování. Nahrazením hvězdičky číslem si však můžete vyžádat přesný počet opakování:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/ '{6}'
170
250
190
208
140
216
9672
To vede k zajímavému rohovému případu. Co by se přidalo, kdyby počet opakování přesáhl počet skutečných oddělovačů v datovém souboru? Podívejme se na to na příkladu:
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
Zajímavé je, že nejen csplit
nahlásil chybu, ale také odstranil vše soubory bloků vytvořené během procesu. Věnujte zvláštní pozornost mé formulaci:odstraněno jim. To znamená, že soubory byly vytvořeny při csplit
došlo k chybě, smazal je. Jinými slovy, pokud již máte soubor, jehož název vypadá jako blokový soubor, bude odstraněn:
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
Ve výše uvedeném příkladu tweet.002.yaml
soubor, který jsme ručně vytvořili, byl přepsán a poté odstraněn pomocí csplit
.
Toto chování můžete změnit pomocí --keep-files
volba. Jak již název napovídá, neodstraní části csplit vytvořené po zjištění chyby:
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
Všimněte si v tom případě a navzdory chybě csplit
nezahodila žádná data:
sh$ diff -s tweets.yaml <(cat tweet.*)
Files tweets.yaml and /dev/fd/63 are identical
Ale co když jsou v souboru nějaká data, která chci zrušit? No, csplit
má pro to určitou omezenou podporu pomocí %regex%
vzor.
Přeskočení dat v csplit
Při použití znaku procenta (%
) jako oddělovač regulárního výrazu namísto lomítka (/
), csplit
přeskočí data až do (ale ne včetně) prvního řádku odpovídající regulárnímu výrazu. To může být užitečné pro ignorování některých záznamů, zejména na začátku nebo na konci vstupního souboru:
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
Použití offsetů při rozdělování souborů pomocí csplit
Při použití regulárních výrazů (buď /…/
nebo %…%
) můžete zadat kladné číslo (+N
) nebo záporné (-N
) offset na konci vzoru, takže csplit
rozdělí soubor N řádky za nebo před odpovídající řádkou. Pamatujte, že ve všech případech vzor určuje konec z části:
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
----
Rozdělit podle čísla řádku
Již jsme viděli, jak můžeme použít regulární výraz k rozdělení souborů. V takovém případě csplit
rozdělí soubor na prvním řádku odpovídající ten regulární výraz. Ale můžete také identifikovat rozdělenou čáru podle jejího čísla řádku, jak to nyní uvidíme.
Před přechodem na YAML jsem si své naplánované tweety ukládal do plochého souboru.
V tomto souboru byl tweet vytvořen ze dvou řádků. Jeden obsahuje volitelné opakování a druhý obsahuje text tweetu s novými řádky nahrazenými \n. Tento ukázkový soubor je opět dostupný online.
S tímto formátem „pevné velikosti“ bylo také možné použít csplit
pro vložení každého jednotlivého tweetu do vlastního souboru:
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
Výše uvedený příklad se zdá být snadno pochopitelný, ale jsou zde dvě úskalí. Nejprve 2
zadaný jako argument pro csplit
je číslo řádku , nikoli počet řádků . Když však používám opakování jako já, po první shodě csplit
použije toto číslo jako počet řádku . Pokud to není jasné, nechám vás porovnat výstup tří následujících příkazů:
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
Zmínil jsem druhé úskalí, trochu související s tím prvním. Možná jste si všimli prázdného řádku úplně nahoře v tweets.txt
soubor? Vede k tomuto tweet.000.txt
blok, který obsahuje pouze znak nového řádku. Bohužel to bylo v tomto příkladu vyžadováno kvůli opakování:nezapomeňte, že chci dva řádky kousky. Takže 2
je povinné před opakováním. To ale také znamená první kus se zlomí při, ale bez , řádek dva. Jinými slovy, první blok obsahuje jeden řádek. Všechny ostatní budou obsahovat 2 řádky. Možná byste se mohli podělit o svůj názor v sekci komentářů, ale podle sebe si myslím, že to byla nešťastná volba designu.
Tento problém můžete zmírnit přeskočením přímo na první neprázdný řádek:
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
Čtení z stdin
Samozřejmě, jako většina nástrojů příkazového řádku, csplit
může číst vstupní data ze svého standardního vstupu. V takovém případě musíte zadat -
jako vstupní název souboru:
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
A to je tak všechno, co jsem vám dnes chtěl ukázat. Doufám, že v budoucnu budete používat csplit k rozdělení souborů v Linuxu. Pokud se vám tento článek líbil a nezapomeňte jej sdílet a lajkovat na své oblíbené sociální síti!