csplit:一种在 Linux 中根据内容拆分文件的更好方法

在 Linux 中,当谈到将一个文本文件拆分为多个文件时,大多数人使用 split 命令。 split 命令没有任何问题,只是它依赖于字节大小或行大小来拆分文件。

在需要根据内容而不是大小拆分文件的情况下,这并不方便。 让我给你一个 example.

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

将它们导入我的系统时,我需要将每条推文写入自己的文件。 我这样做是为了避免注册重复的推文。

但是怎样根据文件内容将文件拆分成几个部分呢? 好吧,也许您可​​以使用 awk 命令获得一些令人信服的信息:

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

然而,尽管相对简单,但这样的解决方案并不是很健壮:对于 example,我没有正确 close 各种输出文件,所以这很可能达到打开文件的限制。 或者如果我在文件的第一条推文之前忘记了分隔符怎么办? 当然,所有这些都可以在 AWK 脚本中处理和修复,代价是让它变得更加复杂。 但是,当我们拥有 csplit 工具来完成这项任务?

在 Linux 中使用 csplit 拆分文件

csplit 工具是 split 可用于将文件拆分为固定大小块的工具。 但 csplit 将根据文件内容识别块边界,而不是使用字节数。

在本教程中,我将演示 csplit 命令的用法,并将解释此命令的输出。

因此对于 example, 如果我想根据 ---- 分隔符,我可以写:

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

你可能已经猜到 csplit 工具使用命令行上提供的正则表达式来识别分隔符。 那些可能是什么 010983 结果显示在标准输出上? 好吧,它们是每个创建的数据块的大小(以字节为单位)。

  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

等一下! 那些 xx00xx01 文件名来自? 为什么 csplit 仅将文件拆分为两个块? 为什么第一个数据块的长度为零字节?

第一个问题的答案很简单: xxNN (或更正式的 xx%02d) 是使用的默认文件名格式 csplit. 但是您可以使用 --suffix-format--prefix 选项。 为了 example,我可以将格式更改为对我的需求更有意义的格式:

  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

前缀是一个普通字符串,但后缀是一个格式字符串,类似于标准 C 库使用的格式字符串 printf 功能。 格式的大多数字符将逐字使用,除了由百分号 (%) 并以转换说明符结尾(此处, d)。 在两者之间,格式还可能包含各种标志和选项。 在我的 example, 这 %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 sylvain sylvain 10846 Jun  6 11:30 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 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

显然,如果我们必须在命令行上提供与数据文件中的块一样多的分隔符,那将是不切实际的。 尤其是因为这个确切的数字通常是事先不知道的。 幸运的是, csplit 有一个特殊的模式,意思是“尽可能多地重复之前的模式”。 尽管它的语法提醒正则表达式中的星号量词,但这更接近于 克莱恩加 概念,因为它用于重复已经匹配过一次的分隔符:

  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 了解更多模式。

更多 csplit 模式

我们刚刚在上一节中看到了怎样使用 ‘{*}’ 量词进行未绑定的重复。 但是,通过用数字替换星号,您可以请求准确的重复次数:

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

这导致了一个有趣的角落案例。 如果重复次数超过数据文件中实际分隔符的数量,会附加什么? 好吧,让我们看看 example:

  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 报告了一个错误,但它也删除了在此过程中创建的所有块文件。 特别注意我的措辞:它 移除 他们。 这意味着文件是创建的,然后,当 csplit 遇到错误,它删除了它们。 换句话说,如果您已经有一个名称看起来像块文件的文件,它将被删除:

  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

在上面 example, 这 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$ # 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] »
      

      #Unix #Linux
      #YesIKnowIT

  ==> tweet.001.yaml <==
  ----
    status: |
      Want to know the oldest file in your disk?

      find / -type f -printf '%TFT%.8TT %pn' | 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] »
      

      #Unix #Linux
      #YesIKnowIT

  ==> tweet.001.yaml <==
  ----
    status: |
      Want to know the oldest file in your disk?

      find / -type f -printf '%TFT%.8TT %pn' | 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

使用 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 :
      « #GlobPatterns : how to move hundreds of files in not time [1/3] »
      

      #Unix #Linux
      #YesIKnowIT
  ----

  ==> tweet.001.yaml <==
    status: |
      Want to know the oldest file in your disk?

      find / -type f -printf '%TFT%.8TT %pn' | 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
  ----

按行号拆分

我们已经看到了怎样使用正则表达式来拆分文件。 在这种情况下, 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 }
  I think I use the `sed` command daily. And you?nnhttps://www.yesik.it/EP07n#Shell #Linux #Sedn#YesIKnowIT

  ==> tweet.002.txt <==
  {}
  Print the first column of a space-separated data file:nawk '{print $1}' data.txt # Print out just the first columnnnFor some unknown reason, I find that easier to remember than:ncut -f1 data.txtnn#Linux #AWK #Cut

这 example 上面看起来很容易理解,但这里有两个陷阱。 首先, 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 只包含换行符的块。 不幸的是,这是必需的 example 因为重复:记住我想要两行块。 所以 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 }
  I think I use the `sed` command daily. And you?nnhttps://www.yesik.it/EP07n#Shell #Linux #Sedn#YesIKnowIT

  ==> tweet.001.txt <==
  {}
  Print the first column of a space-separated data file:nawk '{print $1}' data.txt # Print out just the first columnnnFor some unknown reason, I find that easier to remember than:ncut -f1 data.txtnn#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/TvW8DiEmTcQnn#Unix #Linuxn#YesIKnowIT

从标准输入读取

当然,像大多数命令行工具一样, 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 }
  I THINK I USE THE `SED` COMMAND DAILY. AND YOU?NNHTTPS://WWW.YESIK.IT/EP07N#SHELL #LINUX #SEDN#YESIKNOWIT

  ==> tweet.001.txt <==
  {}
  PRINT THE FIRST COLUMN OF A SPACE-SEPARATED DATA FILE:NAWK '{PRINT $1}' DATA.TXT # PRINT OUT JUST THE FIRST COLUMNNNFOR SOME UNKNOWN REASON, I FIND THAT EASIER TO REMEMBER THAN:NCUT -F1 DATA.TXTNN#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/TVW8DIEMTCQNN#UNIX #LINUXN#YESIKNOWIT

  ==> tweet.003.txt <==
  {}
  WANT TO KNOW THE OLDEST FILE IN YOUR DISK?NNFIND / -TYPE F -PRINTF '%TFT%.8TT %PN' | SORT | LESSN(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 SEARCHN#UNIX #LINUX #SHELL #FIND
  {}
  FROM A POSIX SHELL `$OLDPWD` HOLDS THE NAME OF THE PREVIOUS WORKING DIRECTORY:NCD /TMPNECHO YOU ARE HERE: $PWDNECHO YOU WERE HERE: $OLDPWDNCD $OLDPWDNN#UNIX #LINUX #SHELL #CD
  {}
  FROM A POSIX SHELL, "CD" IS A SHORTHAND FOR CD $HOMEN#UNIX #LINUX #SHELL #CD
  {}
  HOW TO MOVE HUNDREDS OF FILES IN NO TIME?NUSING THE FIND COMMAND!NNHTTPS://YOUTU.BE/ZMEFXJYZAQKN#UNIX #LINUX #MOVE #FILES #FINDN#YESIKNOWIT

这就是我今天想向您展示的全部内容。 我希望将来你会在 Linux 中使用 csplit 来分割文件。 如果您喜欢这篇文章并且不要忘记在您最喜欢的社交网络上分享和喜欢它!

相关阅读:

Posted in: Linux