Contents

使用标题行分割文件

1. 概述

正如我们所知,*split *命令可以帮助我们将一个大文件按给定的行数拆分成多个小文件。

但是,如果输入文件包含标题行,我们有时希望将标题行复制到每个拆分文件中。默认情况下,split命令无法做到这一点。

在本教程中,我们将讨论如何解决这个问题。

2. 问题介绍

一个具体的例子可以帮助我们快速理解问题。

首先,让我们看一下我们的输入示例。tokyo_medal.tsv文件保存了东京奥运会奖牌表前 10 名的数据:

$ cat tokyo_medal.tsv
Rank	Country	Gold	Silver	Bronze	Total
1	United States	39	41	33	113
2	China	38	32	18	88
3	Japan	27	14	17	58
4	Great Britain	22	21	22	65
5	ROC	20	28	23	71
6	Australia	17	7	22	46
7	Netherlands	10	12	14	36
8	France	10	12	11	33
9	Germany	10	11	16	37
10	Italy	10	10	20	40

正如我们在上面的输出中看到的,该文件是一个TSV 文件 。此外,该文件包含一个标题行来说明每列中值的含义。TSV 或 CSV 文件包含标题行是很常见的。

现在,我们的目标是将tokyo_medal.tsv文件拆分成多个部分。假设我们希望每件作品都有三个记录。此外,每件作品还必须有一个标题行。

在本教程中,我们将介绍三种不同的方法来解决问题:

  • 使用版本 >= 8.13 的split 命令
  • 编写一个简单的 shell 脚本
  • 使用 awk 命令

接下来,让我们看看它们的实际效果。

3. 使用较新的split命令

split命令是GNU Coreutils 包的成员。

从 8.13 版本开始,split实用程序引入了一个新的*–filter=COMMAND* 选项。

我们将使用此选项解决问题。首先,我们将看一下完成这项工作的命令。然后,我们就会明白为什么它会起作用。

3.1. 解决方案

**–filter =COMMAND 选项允许我们将split结果写入 shell 命令。**换句话说,我们可以使用filter命令对拆分后的部分进行后处理。

接下来,让我们看看这个选项如何帮助我们解决问题:

$ tail -n +2 tokyo_medal.tsv | split -d -l 3 - --filter='sh -c "{ head -n1 tokyo_medal.tsv; cat; } > $FILE"' part_
$ ls -1 part*
part_00
part_01
part_02
part_03

正如我们在上面的输出中看到的,执行命令后创建了四个文件。现在,让我们检查文件的内容:

$ head part*
==> part_00 <==
Rank	Country	Gold	Silver	Bronze	Total
1	United States	39	41	33	113
2	China	38	32	18	88
3	Japan	27	14	17	58
==> part_01 <==
Rank	Country	Gold	Silver	Bronze	Total
4	Great Britain	22	21	22	65
5	ROC	20	28	23	71
6	Australia	17	7	22	46
==> part_02 <==
Rank	Country	Gold	Silver	Bronze	Total
7	Netherlands	10	12	14	36
8	France	10	12	11	33
9	Germany	10	11	16	37
==> part_03 <==
Rank	Country	Gold	Silver	Bronze	Total
10	Italy	10	10	20	40

所以,我们得到了预期的结果。这样,我们就解决了这个问题。

3.2. 命令如何工作

现在,让我们浏览命令的每个部分并了解其工作原理:

tail -n +2 tokyo_medal.tsv | split -d -l 3 - --filter='sh -c "{ head -n1 tokyo_medal.tsv; cat; } > $FILE"' part_
  • tail -n +2 tokyo_medal.tsvtail 命令从输入文件中剪切标题行,然后我们将所有数据记录通过管道传递给下一个命令
  • …… | split -d -l 3 – –filter=’…’ part_ – 让我们先跳过*–filter=’…‘部分。*split命令从stdin ( - )读取数据记录,并每三行 ( -l 3 ) 将它们拆分。-d选项告诉split在生成的文件名中使用数字后缀
  • –filter=‘sh -c “{ head -n1 tokyo_medal.tsv; 猫; } > $FILE”’–filter选项中的命令对拆分数据进行后处理。**我们用headcat命令声明了一个命令组。 它从输入文件中读取标题行,然后加入拆分记录。最后,我们将带有标题行的记录重定向到*$FILE*,即part_x文件

但是,如果我们系统上的Coreutils包的版本低于8.13,我们需要通过不同的方式来解决问题。因此,我们现在将注意力转向其他一些方法。

4. 编写一个简单的 Shell 脚本

尽管较旧的split命令无法自行解决问题,但我们可以用 shell 脚本将其包装起来以处理标题行。

4.1. 解决问题

简单来说,我们可以分两步解决问题:

  • 第 1 步:拆分没有标题行的输入文件
  • 第 2 步:为每个拆分文件添加标题行

按照这个思路,我们可以构建一个脚本:

#!/bin/bash
INPUT=tokyo_medal.tsv
# Step 1: split the input file without the header line
tail -n +2 "$INPUT" | split -d -l 3 - sh_part_
# Step 2: add the header line to each split file
for file in sh_part_*
do
    head -n 1 "$INPUT" > with_header_tmp
    cat "$file" >> with_header_tmp
    mv -f with_header_tmp "$file"
done

如脚本所示,当我们实现步骤 2 时,我们创建了一个带有_header_tmp 的临时文件来保存标题行,然后附加split结果。

请注意,此示例脚本中跳过了参数处理。例如,输入文件和拆分选项在脚本中是硬编码的。

那是因为本教程的重点是文件拆分实现。但是,如果我们想让我们的脚本可重用,我们应该在现实世界中添加参数处理。

4.2. 测试脚本

现在,让我们将脚本命名为split_with_header.sh并测试它是否按预期工作:

$ ./split_with_header.sh
$ head sh_part_*
==> sh_part_00 <==
Rank	Country	Gold	Silver	Bronze	Total
1	United States	39	41	33	113
2	China	38	32	18	88
3	Japan	27	14	17	58
==> sh_part_01 <==
Rank	Country	Gold	Silver	Bronze	Total
4	Great Britain	22	21	22	65
5	ROC	20	28	23	71
6	Australia	17	7	22	46
==> sh_part_02 <==
Rank	Country	Gold	Silver	Bronze	Total
7	Netherlands	10	12	14	36
8	France	10	12	11	33
9	Germany	10	11	16	37
==> sh_part_03 <==
Rank	Country	Gold	Silver	Bronze	Total
10	Italy	10	10	20	40

伟大的!我们的脚本有效。 通常,当我们遇到文件拆分问题时,split命令会首先出现。但是,实际上,其他 Linux 命令也可以执行这种文件拆分任务。

接下来,让我们使用awk命令解决问题。

5. 使用awk命令

awk是文本处理的有力武器。此外,awk还定义了自己的类 C 脚本语言。它可以在不使用任何外部命令的情况下解决这个问题。

5.1. awk解决方案

首先,让我们看看awk是如何解决问题的:

$ awk -v lines="3" -v pre="awk_part_" '
        NR==1 { header=$0; next}
        (NR-1) % lines ==1 { fname=pre c++; print header > fname}
        {print > fname}' tokyo_medal.tsv
$ head awk_part_*
==> awk_part_0 <==
Rank	Country	Gold	Silver	Bronze	Total
1	United States	39	41	33	113
2	China	38	32	18	88
3	Japan	27	14	17	58
==> awk_part_1 <==
Rank	Country	Gold	Silver	Bronze	Total
4	Great Britain	22	21	22	65
5	ROC	20	28	23	71
6	Australia	17	7	22	46
==> awk_part_2 <==
Rank	Country	Gold	Silver	Bronze	Total
7	Netherlands	10	12	14	36
8	France	10	12	11	33
9	Germany	10	11	16	37
==> awk_part_3 <==
Rank	Country	Gold	Silver	Bronze	Total
10	Italy	10	10	20	40

如上面的输出所示,输入文件已按我们预期的方式与标题行分开。

5.2. awk命令的工作原理

现在,让我们通过awk命令并了解它是如何工作的:

  • awk -v lines=”3″ -v pre=”awk_part_” – 首先,我们声明了两个 awk变量来定义每个拆分文件中有多少条记录以及文件名的前缀
  • NR==1 { header=$0; next} – 当 awk读取第一行时,它将该行存储在 标题变量中并停止进一步处理
  • (NR-1) % lines ==1 { fname=pre c++; print header > fname} – 当当前行是主干的第一条记录时,我们需要通过递增计数器 ( c ) 来更新文件名 ( fname ) 。此外,由于这将是一个新文件,我们将标头变量的值作为第一行添加到文件中
  • {print > fname}’ tokyo_medal.tsv – 然后,我们可以将每个记录行重定向到当前的fname文件

这样,awk只读取一次输入文件就解决了问题。