File.tsv
是一个制表符分隔的文件,有 7 列:
cat File.tsv
1 A J 1
2 B K N 1
3 C L O P Q 1
以下读取File.tsv
具有 7 列的制表符分隔文件,并将条目存储在数组 A 中。
while IFS=$'\t' read -r -a D; do
A=("${A[@]}" "${D[i]}" "${D[$((i + 1))]}" "${D[$((i + 2))]}" "${D[$((i + 3))]}" "${D[$((i + 4))]}" "${D[$((i + 5))]}" "${D[$((i + 6))]}")
done < File.tsv
nA=${#A[@]}
for ((i = 0; i < nA; i = i + 7)); do
SlNo="${A[i]}"
Artist="${A[$((i + 1))]}"
VideoTitle="${A[$((i + 2))]}"
VideoId="${A[$((i + 3))]}"
TimeStart="${A[$((i + 4))]}"
TimeEnd="${A[$((i + 5))]}"
VideoSpeed="${A[$((i + 6))]}"
done
问题
tsv 文件中的某些条目为空,但在读取文件时会跳过空值。
笔记
是 tsv 文件,空值前后各有一个制表符。
所需的解决方案
读取空值并将其存储在数组中。
答案1
正如我在评论中所说,这不是 shell 脚本的工作。 bash(和类似的 shell)用于协调其他程序的执行,而不是处理数据。
使用任何其他语言 - awk、perl 和 python 都是不错的选择。它将更容易编写、更容易阅读和维护,并且很多快点。
下面是如何将文本文件读入哈希数组 (AoH) 中的示例perl
,然后在各种打印语句中使用该数据。
AoH 是一种数据结构,正如其名称所示,它是一个数组,其中每个元素都是关联数组(也称为哈希)。
顺便说一句,这也可以使用数组数组 (AoA) 数据结构(也称为列表列表或 LoL)来完成,但通过字段名称访问字段很方便,而不必记住字段编号。
您可以在 Perl 附带的 Perl Data Structures Cookbook 中阅读有关 Perl 数据结构的更多信息。运行man perldsc
或perldoc perldsc
.您可能也想perllol
阅读perlreftut
。如果perldata
您不熟悉 perl 变量 ("Perl 具有三种内置数据类型:标量、标量数组和标量关联数组(称为哈希)“。“标量”是任何单个值,例如数字、字符串或参考到另一个变量)
Perl 附带了一个很多文档和教程 - 运行man perl
以获得概述。包含的 perl 文档大约有 14MB,因此它通常位于一个单独的包中,以防您不想安装它。在 Debian 上:apt install perl-doc
.此外,每个库模块都有自己的文档。
#!/usr/bin/perl -l
use strict;
# Array to hold the hashes for each record
my @data;
# Array of field header names. This is used to insert the
# data into the %record hash with the right key AND to
# ensure that we can access/print each record in the right
# order (perl hashes are inherently unordered so it's useful
# and convenient to use an indexed array to order it)
my @headers=qw(SlNo Artist VideoTitle VideoId TimeStart TimeEnd VideoSpeed);
# main loop, read in each line, split it by single tabs, build into
# a hash, and then push the hash onto the @data array.
while (<>) {
chomp;
my %record = ();
my @line = split /\t/;
# iterate over the indices of the @line array so we can use
# the same index number to look up the field header name
foreach my $i (0..$#line) {
# insert each field into the hash with the header as key.
# if a field contains only whitespace, then make it empty
($record{$headers[$i]} = $line[$i]) =~ s/^\s+$//;
}
push @data, \%record ;
}
# show how to access the AoH elements in a loop:
print "\nprint \@data in a loop:";
foreach my $i (0 .. $#data) {
foreach my $h (@headers) {
printf "\$data[%i]->{%s} = %s\n", $i, $h, $data[$i]->{$h};
}
print;
}
# show how to access individual elements
print "\nprint some individual elements:";
print $data[0]->{'SlNo'};
print $data[0]->{'Artist'};
# show how the data is structured (requires Data::Dump
# module, comment out if not installed)
print "\nDump the data:";
use Data::Dump qw(dd);
dd \@data;
仅供参考,正如 @Sobrique 在评论中指出的那样,主循环内的my @line =...
和整个循环可以只用一行代码替换(perl 有一些foreach
while (<>)
非常很好的语法糖):
@record{@headers} = map { s/^\s+$//, $_ } split /\t/;
笔记:数据::转储是一个用于漂亮打印整个数据结构的 Perl 模块。对于调试很有用,并确保数据结构实际上是您所认为的那样。而且,并非巧合的是,输出的形式可以复制粘贴到 Perl 脚本中并直接分配给变量。
它可用于libdata-dump-perl
软件包中的 debian 和相关发行版。其他发行版可能也打包了它。否则从 CPAN 获取。或者只是注释掉或删除脚本的最后三行——这里没有必要使用它,它只是打印输出循环中已打印数据的另一种方式。
将其另存为,例如read-tsv.pl
,使其可执行chmod +x read-tsv.pl
并运行它:
$ ./read-tsv.pl file.tsv
print @data in a loop:
$data[0]->{SlNo} = 1
$data[0]->{Artist} = A
$data[0]->{VideoTitle} = J
$data[0]->{VideoId} =
$data[0]->{TimeStart} =
$data[0]->{TimeEnd} =
$data[0]->{VideoSpeed} = 1
$data[1]->{SlNo} = 2
$data[1]->{Artist} = B
$data[1]->{VideoTitle} = K
$data[1]->{VideoId} = N
$data[1]->{TimeStart} =
$data[1]->{TimeEnd} =
$data[1]->{VideoSpeed} = 1
$data[2]->{SlNo} = 3
$data[2]->{Artist} = C
$data[2]->{VideoTitle} = L
$data[2]->{VideoId} = O
$data[2]->{TimeStart} = P
$data[2]->{TimeEnd} = Q
$data[2]->{VideoSpeed} = 1
print some individual elements:
1
A
Dump the data:
[
{
Artist => "A",
SlNo => 1,
TimeEnd => "",
TimeStart => "",
VideoId => "",
VideoSpeed => 1,
VideoTitle => "J",
},
{
Artist => "B",
SlNo => 2,
TimeEnd => "",
TimeStart => "",
VideoId => "N",
VideoSpeed => 1,
VideoTitle => "K",
},
{
Artist => "C",
SlNo => 3,
TimeEnd => "Q",
TimeStart => "P",
VideoId => "O",
VideoSpeed => 1,
VideoTitle => "L",
},
]
注意嵌套的 for 循环如何按照我们想要的确切顺序打印数据结构(因为我们迭代了@headers
数组),同时只是使用dd
输出函数转储Data::Dump
按键名排序的记录(这就是 Data::Dump 处理 perl 中的哈希值未排序这一事实的方式)。
其他的建议
一旦您将数据放入这样的数据结构中,就可以轻松地将其插入到 SQL 数据库中,例如mysql/玛丽亚数据库或者PostgreSQL或者sqlite3。 Perl 有数据库模块(参见数据库接口)对于所有这些以及更多。
(在 debian 等中,它们被打包为libdbd-mysql-perl
, libdbd-mariadb-perl
, libdbd-pg-perl
, libdbd-sqlite3-perl
, 和libdbi-perl
。其他发行版将有不同的包名称)
顺便说一句,主解析循环也可以使用另一个名为的 perl 模块来实现文本::CSV,它可以解析 CSV 和类似的文件格式(如制表符分隔)。或者与DBD::CSV它Text::CSV
允许您打开 CSV 或 TSV 文件并对其运行 SQL 查询就好像它是一个 SQL 数据库一样。
事实上,使用这些模块将 CSV 或 TSV 文件导入 SQL 数据库是一个相当简单的 10-15 行脚本,其中大部分是样板设置内容……实际的算法是一个简单的 while 循环来运行对源数据进行 SELECT 查询并将 INSERT 语句插入目标数据。
这两个模块都针对 debian 等进行了打包,如libtext-csv-perl
和libdbd-csv-perl
。可能也为其他发行版打包。并且一如既往地可在 CPAN 上使用。
答案2
正如\t
一个空白字符isspace()
(返回 true 或匹配的字符grep '[[:space:]]'
),这是标准分词 POSIX 指定的行为:IFS 空白字符序列(出现在 中的空白字符$IFS
)被视为一分隔符和前导、尾随或非空白 IFS 字符两侧的分隔符将被忽略。
最初,在该“功能”来自的 ksh 中,这种特殊处理仅限于 TAB、NL 和空格字符,但 POSIX 将其更改为在语言环境中被视为空格的任何字符。但实际上,大多数 shell 仍然只对 TAB/NL/SPC 执行此操作(这也恰好是默认值$IFS
)。据我所知,唯一的例外(在这方面唯一符合 POSIX 标准的 shell)是yash
.
在 bash 中,行为在 5.0 中发生了变化,现在 bash 的行为类似于 ksh93,因为特殊处理应用于空白字符,但仅限于那些在一个字节上编码的字符。
比较治疗方法空间性格和回车那些:
$ s=$'\u2000' a="a${s}${s}b" bash -c 'IFS=$s; [[ $s = [[:space:]] ]] && echo yes; printf "<%s>\n" $a'
yes
<a>
<>
<b>
$ s=$'\r' a="a${s}${s}b" bash -c 'IFS=$s; [[ $s = [[:space:]] ]] && echo yes; printf "<%s>\n" $a'
yes
<a>
<b>
因此,在bash
5.0+ 版本中,为了bash
不再对 TAB 给予特殊待遇,您需要构建一个不将 TAB 视为空格的区域设置。即便如此,这也不够,因为即使对于非空白字符,例如,
代替 TAB,a,b,c,
被拆分为a
, b
, 和c
代替a
, b
,c
和空字符串。
更好的方法是使用一个 shell,其中可以禁用与 IFS 分割有关的空白字符的特殊处理,或者有一个适当的分割运算符,或者更好的是如 @cas 所说,使用适当的编程语言,例如perl
代替 shell这不是为此设计的。
在 ksh93 或 zsh 中,可以通过将 中的相应字符加倍来删除特殊处理$IFS
。zsh
的分词也不会丢弃尾随的空元素。所以在zsh
,
IFS=$'\t\t' read -rA array
实际上会起作用。在那里,您还可以执行以下操作:
IFS= read -r line && array=( "${(@ps[\t])line}" )
读取一行,然后使用s
参数扩展标志将其分割。
在 ksh93 中,您可以使用以下命令进行拆分:
set -o noglob
IFS=$'\t\t'
IFS= read -r line && array=( $line'' )
(与上述方法仍有区别zsh
,空行会产生一个包含一个空元素的数组,而不是一个没有元素的数组)。
ksh93 确实支持多维数组,看起来可能对您有帮助:
i=0
IFS=$'\t\t'; set -o noglob
typeset -a array=()
while IFS= read -r line; do
array[i++]=( $line'' )
done < file.tsv
例如,然后${array[2][5]}
会给你第三行的第六个字段。
1 IFS 分割最初来自 Bourne shell,但在 Bourne shell 中,所有角色都接受了这种处理,而不仅仅是 TAB/NL/SPC
答案3
使用 Raku(以前称为 Perl_6)
下面的所有代码都在 bash 命令行执行:
raku -e 'my @a = lines>>.split("\t"); .raku.put for @a;' test_tsv.tsv
返回:
$(("1", "A", "J", "", "", "", "1").Seq)
$(("2", "B", "K", "N", "", "", "1").Seq)
$(("3", "C", "L", "O", "P", "Q", "1").Seq)
上面的每行都在选项卡上分开\t
。该.raku
方法用于可视化空字符串。超级>>
运算符是映射的缩写,如 中所示.map(*.split("\t"))
。上面的代码返回一个包含 3 个.Seq
元素的对象,但是这很容易被强制转换为一个列表:
raku -e 'my @a = lines>>.split("\t"); .list.raku.put for @a;' test_tsv.tsv
("1", "A", "J", "", "", "", "1")
("2", "B", "K", "N", "", "", "1")
("3", "C", "L", "O", "P", "Q", "1")
该elems
方法在 Raku 中用于获取元素计数,可以是整个数组对象的元素,也可以是每个“行”的元素:
raku -e 'my @a = lines>>.split("\t"); @a.elems.put;' test_tsv.tsv
3
raku -e 'my @a = lines>>.split("\t"); @a>>.elems.put;' test_tsv.tsv
7 7 7
一旦您对获得所需的结构感到满意,就可以轻松简化代码,或添加/删除“Perlish/Rakuish”习惯用法,例如for
(以获得所需的可视化输出或进一步操作):
raku -e 'my @a = lines>>.split("\t"); .put for @a;' test_tsv.tsv
1 A J 1
2 B K N 1
3 C L O P Q 1
raku -e 'my @a = lines>>.split("\t"); @a>>.list.raku.put;' test_tsv.tsv
(("1", "A", "J", "", "", "", "1"), ("2", "B", "K", "N", "", "", "1"), ("3", "C", "L", "O", "P", "Q", "1"))
答案4
bash
如果您确实想使用(并且我同意其他语言更好)进行操作,请尝试以下操作:
sed -e 's/\t/,\t/g' File.tsv | \
while IFS=$'\t' read -r -a D; do
A=("${A[@]}" "${D[i]%,}" "${D[$((i + 1))]%,}" "${D[$((i + 2))]%,}" "${D[$((i + 3))]%,}" "${D[$((i + 4))]%,}" "${D[$((i + 5))]%,}" "${D[$((i + 6))]%,}")
done
nA=${#A[@]}
这里的技巧是向每个字段添加一些内容(在本例中是在末尾添加一个逗号),以便没有字段为空。然后稍后再剥掉。如果您需要区分空字段和缺失字段,请在末尾添加额外的字符。
也可以为空字段填写虚拟值,但这需要您能够分配虚拟值。请注意,您需要进行两次替换:
sed -e 's/\t\t/\tdummy\t/g;s/\t\t/\tdummy\t/g' File.tsv | \
另外,您应该通过删除i
, 并使用 来简化+=
。因此:
A+=("${D[0]%,}" "${D[1]%,}" "${D[2]%,}" "${D[3]%,}" "${D[4]%,}" "${D[5]%,}" "${D[6]%,}")
如果您不打算改变列数,我还建议阅读七个不同的变量。