在 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
工具使用命令行上提供的正则表达式来识别分隔符。 那些可能是什么 0
和 10983
结果显示在标准输出上? 好吧,它们是每个创建的数据块的大小(以字节为单位)。
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
等一下! 那些 xx00
和 xx01
文件名来自哪里? 以及为什么 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 来分割文件。 如果你喜欢这篇文章,别忘了在你最喜欢的社交网络上分享和喜欢它!