Когда дело доходит до разделения текстового файла на несколько файлов в Linux, большинство людей используют команду split. В команде split нет ничего плохого, за исключением того, что он использует размер байта или размер строки для разделения файлов.
Это не удобно в ситуациях, когда вам нужно разделить файлы на основе его содержимого, а не на основе размера. Позвольте привести пример.
Мы управляем своими запланированными твитами, используя файлы YAML. Типичный файл твитов содержит несколько твитов, разделенных четырьмя тире:
---- event: repeat: { days: 180 } status: | Я использую команду "sed" ежедневно. А вы? https://andreyex.ru #Shell #Linux #Sed #AndreyEx ---- status: | Печать первого столбца файла данных, разделенного пробелами: awk '{print $1}' data.txt # Распечатать только первый столбец По какой-то неизвестной причине мне легче запомнить, чем: cut -f1 data.txt #Linux #AWK #Cut ---- status: | For the #shell #beginners : [...]
При импорте их в свою систему нам нужно написать каждый твит в свой собственный файл. Мы делаем это, чтобы не регистрировать повторяющиеся твиты.
Но как разбить файл на несколько частей на основе его содержимого? Вероятно, вы можете получить что-то убедительное, используя команду awk :
sh$ awk < tweets.yaml ' > /----/ { OUTPUT="tweet." (N++) ".yaml" } > { print > OUTPUT } > '
Однако, несмотря на относительную простоту, такое решение не очень надежное: например, мы не правильно закрыли различные выходные файлы, поэтому это может очень сильно превысить ограничение на открытые файлы. Или что, если мы забыли разделитель перед самым первым твитом файла? Конечно, все, что можно обработать и зафиксировать в сценарии AWK, за счет усложнения его работы. Но зачем беспокоиться об этом, когда у нас есть инструмент csplit
для выполнения этой задачи?
Инструмента csplit
является кузеном инструмента split
, который можно использовать для разбиения файла на куски фиксированного размера. Но csplit
будет определять границы блоков на основе содержимого файла, а не использовать количество байтов.
В этой статье мы продемонстрируем использование команды csplit, а также объясним вывод этой команды.
Так, например, если мы хотим разбить файл твита на основе разделителя ----
, то могли бы написать:
sh$ csplit tweets.yaml /----/ 0 10846
Возможно, вы догадались, что инструмент csplit
использовал регулярное выражение, предоставленное в командной строке, для идентификации разделителя. И каковы могут быть результаты 0
и 10983
отображаемые на стандартном выходе? Ну, это размер в байтах каждого созданного фрагмента данных.
sh$ ls -l xx0* -rw-r--r-- 1 andreyex andreyex 0 Jul 23 13:45 xx00 -rw-r--r-- 1 andreyex andreyex 10846 Jul 23 13:45 xx01
Подожди минуту! Откуда берутся те имена файлов xx00
и xx01
? И зачем csplit
разбивать файл на две части ? И почему первый кусок данных имеет длину ноль байтов ?
Ответ на первый вопрос прост: xxNN
(или более формально xx%02d
) является стандартным форматом имени файла, используемым csplit
. Но вы можете изменить это, используя параметры --suffix-format
и --prefix
. Например, мы могли бы изменить формат на что-то более значимое для наших нужд:
sh$ csplit tweets.yaml \ > --prefix='tweet.' --suffix-format='%03d.yaml' \ > /----/ 0 10846 sh$ ls -l tweet.* -rw-r--r-- 1 andreyex andreyex 0 Jul 23 13:45 tweet.000.yaml -rw-r--r-- 1 andreyex andreyex 10846 Jul 23 13:45 tweet.001.yaml
Префикс является обычной строкой, но суффикс является строкой формата, как та, которая используется в стандартной библиотеке C функцией printf
. Большинство символов формата будут использоваться дословно, за исключением спецификаций преобразования, которые вводятся знаком процента (%
) и заканчивается спецификатором преобразования (здесь d
). Между ними формат может также содержать различные флаги и опции. В нашем примере %03d
спецификация преобразования означает:
d
),3
),0
).Но это не касается других запросов, которые у нас были выше: так почему у нас есть только два куска, один из которых содержит нулевые байты? Возможно, вы уже нашли ответ на этот последний вопрос самостоятельно: наш файл данных начинается с ----
самой первой строки. Таким образом, csplit
он считается разделителем, и поскольку перед этой строкой не было данных, он создал пустой первый кусок. Мы можем отключить создание файлов с нулевыми байтами с помощью опции --elide-empty-files
:
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 andreyex andreyex 10846 Jul 23 13:45 tweet.000.yaml
Ок: больше нет пустых файлов. Но в некотором смысле это результат худший, так как csplit
разделяет файл всего на один кусок. Мы едва можем назвать это «разделение» файла, не так ли?
Объяснение этого удивительного результата csplit
вовсе не предполагает, что каждый файл должен быть разделен на основе того же разделителя. Фактически, csplit
требуется предоставить каждый используемый разделитель. Даже если это несколько раз то же самое:
sh$ csplit tweets.yaml \ > --prefix='tweet.' --suffix-format='%03d.yaml' \ > --elide-empty-files \ > /----/ /----/ /----/ 170 250 10426
Мы поместили три (одинаковых) разделителя в командной строке. Итак, csplit
определил конец первого фрагмента на основе первого разделителя. Это приводит к тому, что фрагмент длины с нулевым байтом будет удален. Второй фрагмент был разделен следующим совпадением строк./----/
. Превращение в кусок на 170 байт. Наконец, третий фрагмент длиной 250 байтов был идентифицирован на основе третьего разделителя. Остальные данные, 10426 байт, были помещены в последний кусок.
sh$ ls -l tweet.???.yaml -rw-r--r-- 1 andreyex andreyex 170 Jul 23 13:45 tweet.000.yaml -rw-r--r-- 1 andreyex andreyex 250 Jul 23 13:45 tweet.001.yaml -rw-r--r-- 1 andreyex andreyex 10426 Jul 23 13:45 tweet.002.yaml
Очевидно, что это было бы нецелесообразно, если бы нам пришлось предоставить столько разделителей в командной строке, сколько в файле данных кусков. Тем более, что это точное число обычно заранее неизвестно. К счастью, csplit
имеет специальный шаблон, означающий «повторить предыдущий шаблон как можно больше раз». Несмотря на свой синтаксис, напоминающий звездный квантификатор в регулярном выражении, это ближе к концепции Kleene plus, поскольку он используется для повторения разделителя, который уже встречался один раз:
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
И на этот раз, наконец, мы разделили свою коллекцию на отдельные части. Однако есть ли в csplip
какие-то другие «специальные» шаблоны? Ну, мы не знаем, можем ли мы назвать их «особенными».
Мы только что видели в предыдущем разделе, как использовать квантор ‘{*}’ для несвязанных повторений. Однако, заменив звезду на число, вы можете запросить точное количество повторений:
sh$ csplit tweets.yaml \ > --prefix='tweet.' --suffix-format='%03d.yaml' \ > --elide-empty-files \ > /----/ '{6}' 170 250 190 208 140 216 9672
Это приводит к интересному угловому случаю. Что будет добавлено, если количество повторений превысит количество фактических разделителей в файле данных? Давайте посмотрим на примере:
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
Интересно, что csplit
не только сообщила об ошибке, но и удалила все файлы chunk, созданные во время процесса. Обратите особое внимание на нашу формулировку: она удалила их. Это означает, что файлы были созданы, а затем, csplit
столкнувшись с ошибкой, удалила их. Другими словами, если у вас уже есть файл, имя которого выглядит как файл chunk, он будет удален:
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
В приведенном выше примере файл tweet.002.yaml
, который мы создали вручную, был перезаписан, а затем удален csplit
.
Вы можете изменить это поведение с помощью опции --keep-files
. Как следует из названия, оно не будет удалять куски csplit, созданные после обнаружения ошибки:
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
Обратите внимание, что в этом случае, несмотря на ошибку, csplit
не отбрасывает данные:
sh$ diff -s tweets.yaml <(cat tweet.*) Files tweets.yaml and /dev/fd/63 are identical
Но что, если в файле есть какие-то данные, которые мы хотим пропустить? csplit
имеет ограниченную поддержку для этого, используя шаблон %regex%
.
При использовании символа процента ( %
) в качестве разделителя регулярных выражений вместо косой черты ( /
) csplit
будет пропускаться данные до (но не включая) первой строки, соответствующей регулярному выражению. Это может быть полезно при игнорировании некоторых записей, особенно в начале или в конце входного файла:
sh$ # Оставить только первые два твита 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: | Я использую команду "sed" ежедневно. А вы? https://andreyex.ru #Shell #Linux #Sed #AndreyEx ==> tweet.001.yaml <== ---- status: | Печать первого столбца файла данных, разделенного пробелами: awk '{print $1}' data.txt # Распечатать только первый столбец По какой-то неизвестной причине мне легче запомнить, чем: cut -f1 data.txt #Linux #AWK #Cut
sh$ # Пропустить первые два твита 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 : «#AndreyEx : Операционная система LinuxОперационная система Linux»Операционная система Linux#Unix #Linux #AndreyEx ==> tweet.001.yaml <== ---- status: | Хотите знать самый старый файл на диске? find / -type f -printf '%TFT%.8TT %p\n' | sort | less (должен работать на любой единой системе, совместимой со спецификацией UNIX) #UNIX #Linux ==> tweet.002.yaml <== ---- status: | При использовании команды find используйте `- iname` вместо `- name' для поиска без учета регистра #Unix #Linux #Shell #Find
sh$ # Оставить только третий и четвертый твиты 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 : «#AndreyEx : Операционная система LinuxОперационная система Linux»Операционная система Linux#Unix #Linux #AndreyEx ==> tweet.001.yaml <== ---- status: | Хотите знать самый старый файл на диске? find / -type f -printf '%TFT%.8TT %p\n' | sort | less (должен работать на любой единой системе, совместимой со спецификацией UNIX) #UNIX #Linux ==> tweet.002.yaml <== ---- status: | При использовании команды find используйте `- iname` вместо `- name' для поиска без учета регистра #Unix #Linux #Shell #Find
При использовании регулярных выражений ( /…/
или %…%
) вы можете указать положительное (+N
) или отрицательное (-N
) смещение в конце шаблона, так чтобы csplit
разделил файл на N строк после или до соответствующей строки. Помните, что во всех случаях шаблон указывает конец фрагмента:
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 : «#AndreyEx : Операционная система LinuxОперационная система Linux»Операционная система Linux#Unix #Linux #AndreyEx ---- ==> tweet.001.yaml <== status: | Хотите знать самый старый файл на диске? find / -type f -printf '%TFT%.8TT %p\n' | sort | less (должен работать на любой единой системе, совместимой со спецификацией UNIX) #UNIX #Linux ---- ==> tweet.002.yaml <== status: | При использовании команды find используйте `- iname` вместо `- name' для поиска без учета регистра #Unix #Linux #Shell #Find ----
Мы уже видели, как мы можем использовать регулярное выражение для разделения файлов. В этом случае csplit
разделит файл в первой строке, соответствующей этому регулярному выражению. Но вы также можете определить разделительную линию по номеру строки, как мы ее увидим сейчас.
Прежде чем переключиться на YAML, мы использовали для хранения запланированных твитов в плоском файле.
В этом файле твит был сделан из двух строк. Один из них содержит необязательное повторение, а второй содержит текст твита, причем новые строки заменены на \n. Еще раз, что образец файла доступен в Интернете.
С этим форматом «фиксированного размера» тоже можно было использовать, csplit
чтобы поместить каждый отдельный твит в свой собственный файл:
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 } Я использую команду "sed" ежедневно. А вы?\n\nhttps://andreyex.ru\n#Shell #Linux #Sed\n#AndreyEx ==> tweet.002.txt <== {} Печать первого столбца файла данных, разделенного пробелами:\nawk '{print $1}' data.txt # Распечатать только первый столбец\n\nПо какой-то неизвестной причине мне легче запомнить, чем:\ncut -f1 data.txt\n\n#Linux #AWK #Cut
Приведенный выше пример кажется легко понятным, но здесь есть две подводные камни. Во- первых, 2
дано в качестве аргумента csplit
является строка номер, не линия подсчета. Однако при использовании повторения, как мы сделали, после первого совпадения csplit
будет использовать это число в качестве счетчика строк. Если это не ясно, мы позволим вам сравнить выходные данные трех следующих команд:
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
Мы упомянули о второй ловушке, несколько связанной с первой. Возможно, вы заметили пустую строку в самой верхней части файла tweets.txt
? Это приводит к тому фрагменту tweet.000.txt
, который содержит только символ новой строки. К сожалению, это требовалось в этом примере из-за повторения: помните, что мне нужны две строки. Таким образом, 2
это обязательное условие перед повторением. Но это также означает, что первый фрагмент будет разбит, но не включает в себя вторую строку. Другими словами, первый фрагмент содержит одну строку. Все остальные будут содержать 2 строки. Возможно, вы могли бы поделиться своим мнением в разделе комментариев, но, по моему мнению, это был неудачный выбор дизайна.
Вы можете уменьшить эту проблему, перейдя непосредственно к первой непустой строке:
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 } Я использую команду "sed" ежедневно. А вы?\n\nhttps://andreyex.ru\n#Shell #Linux #Sed\n#AndreyEx ==> tweet.001.txt <== {} Печать первого столбца файла данных, разделенного пробелами:\nawk '{print $1}' data.txt # Распечатать только первый столбец\n\nПо какой-то неизвестной причине мне легче запомнить, чем:\ncut -f1 data.txt\n\n#Linux #AWK #Cut ==> tweet.002.txt <== {} For the #shell #beginners :\n«#AndreyEx : Операционная система LinuxОперационная система Linux»\nhttps://andreyex.ru/operacionnaya-sistema-linux/\n\n#Unix #Linux\n#AndreyEx
Конечно, как и большинство инструментов командной строки, csplit
можно считывать входные данные со своего стандартного ввода. В этом случае вы должны указать -
в качестве входного имени файла:
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 } Я использую команду "sed" ежедневно. А вы?\N\Nhttps://andreyex.ru\N#SHELL #LINUX #SED\N#AndreyEx ==> tweet.001.txt <== {} Печать первого столбца файла данных, разделенного пробелами:\NAWK '{PRINT $1}' DATA.TXT # Распечатать только первый столбец\N\NПо какой-то неизвестной причине мне легче запомнить, чем:\NCUT -F1 DATA.TXT\N\N#LINUX #AWK #CUT ==> tweet.002.txt <== {} FOR THE #SHELL #BEGINNERS :\N«#AndreyEx : Операционная система LinuxОперационная система Linux»\Nhttps://andreyex.ru/operacionnaya-sistema-linux/\N\N#UNIX #LINUX\N#AndreyEx ==> tweet.003.txt <== {} Хотите знать самый старый файл на диске?\N\NFIND / -TYPE F -PRINTF '%TFT%.8TT %P\N' | SORT | LESS\N(должен работать на любой единой системе, совместимой со спецификацией UNIX)\N#UNIX #LINUX {} При использовании команды find используйте `- iname` вместо `- name' для поиска без учета регистра\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#AndreyEx sh$ tr [:lower:] [:upper:] < t
И это все, что мы хотели показать вам сегодня. Надеюсь, в будущем вы будете использовать csplit для разделения файлов в Linux. Если вам понравилась эта статья и не забывайте делиться ею в своей любимой социальной сети!