读取文件并存储为数组,不跳过空字符串

读取文件并存储为数组,不跳过空字符串

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 perldscperldoc 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 有一些foreachwhile (<>)非常很好的语法糖):

  @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::CSVText::CSV允许您打开 CSV 或 TSV 文件并对其运行 SQL 查询就好像它是一个 SQL 数据库一样

事实上,使用这些模块将 CSV 或 TSV 文件导入 SQL 数据库是一个相当简单的 10-15 行脚本,其中大部分是样板设置内容……实际的算法是一个简单的 while 循环来运行对源数据进行 SELECT 查询并将 INSERT 语句插入目标数据。

这两个模块都针对 debian 等进行了打包,如libtext-csv-perllibdbd-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>

因此,在bash5.0+ 版本中,为了bash不再对 TAB 给予特殊待遇,您需要构建一个不将 TAB 视为空格的区域设置。即便如此,这也不够,因为即使对于非空白字符,例如,代替 TAB,a,b,c,被拆分为a, b, 和c代替a, b,c和空字符串

更好的方法是使用一个 shell,其中可以禁用与 IFS 分割有关的空白字符的特殊处理,或者有一个适当的分割运算符,或者更好的是如 @cas 所说,使用适当的编程语言,例如perl代替 shell这不是为此设计的。

在 ksh93 或 zsh 中,可以通过将 中的相应字符加倍来删除特殊处理$IFSzsh的分词也不会丢弃尾随的空元素。所以在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"))

https://raku.org/
https://docs.raku.org/

答案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]%,}")

如果您不打算改变列数,我还建议阅读七个不同的变量。

相关内容