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

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

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

我管理我的预定推文使用 YAML 文件。 一个典型的推文文件包含几条推文,用四个破折号分隔:

  ----
    event:
      repeat: { days: 180 }
    status: |
      I think I use the `sed` command daily. And you?

      
      #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 报错了,但同时也删除了过程中创建的所有chunk文件。 特别注意我的措辞:它 移除 他们。 这意味着文件被创建,然后,当 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?

      
      #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 来分割文件。 如果你喜欢这篇文章,别忘了在你最喜欢的社交网络上分享和喜欢它!