Последнее несчастье тяжелее всех (Т. Фуллер).

csplit: лучший способ разделения файла в Linux на основе его содержимого

13 мин для чтения
FavoriteLoadingДобавить в избранное
1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (1 оценок, среднее: 5,00 из 5)
Загрузка...
23 августа 2018
csplit - лучший способ разделения файла в Linux на основе его содержимого
Когда дело доходит до разделения текстового файла на несколько файлов в 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 для разделения файлов в Linux

csplit - лучший способ разделения файла в Linux на основе его содержимого

Инструмента 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 какие-то другие «специальные» шаблоны? Ну, мы не знаем, можем ли мы назвать их «особенными».

Больше шаблонов csplit

Мы только что видели в предыдущем разделе, как использовать квантор ‘{*}’ для несвязанных повторений. Однако, заменив звезду на число, вы можете запросить точное количество повторений:

  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

При использовании символа процента ( %) в качестве разделителя регулярных выражений вместо косой черты ( /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

 

Использование смещений при расщеплении файлов с помощью csplit

При использовании регулярных выражений ( /…​/ или %…​%) вы можете указать положительное (+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

 

Чтение из stdin

Конечно, как и большинство инструментов командной строки, 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. Если вам понравилась эта статья и не забывайте делиться ею в своей любимой социальной сети!

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Просмотров: 48

Если статья понравилась, то поделитесь ей в социальных сетях:

Добавить комментарий

Войти с помощью: 

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам:

Заполните форму и наш менеджер перезвонит Вам в самое ближайшее время!

badge
Обратный звонок 1
Отправить
galka

Спасибо! Ваша заявка принята

close
galka

Спасибо! Ваша заявка принята

close