ファイルを読み取り、配列として保存します。空の文字列をスキップしないでください。

ファイルを読み取り、配列として保存します。空の文字列をスキップしないでください。

File.tsv7つの列があるタブ区切りファイルです。

cat File.tsv
1   A   J               1
2   B   K   N           1
3   C   L   O   P   Q   1

以下は、File.tsv7列のタブで区切られたファイルを読み取り、項目を配列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ファイルの一部のエントリは空ですが、ファイルを読み取るとnull値をスキップします。

ノート

NULL値の前後にタブ文字があるtsvファイル。

必要なソリューション

null 値を読み取り、配列に保存します。

答え1

コメントで述べたように、これはシェルスクリプトの仕事ではありません。 bash(および同様のシェル)は、データを処理するのではなく、他のプログラムの実行を調整するために使用されます。

使用どの他の言語 - awk、Perl、Pythonはすべて良い選択です。作成しやすく、読みやすく保つのは簡単です。たくさん急いで。

以下は、テキストファイルをAoH(ハッシュ配列)に読み込みperl、そのデータをさまざまな印刷ステートメントで使用する方法の例です。

AoHは、名前が示すように、各要素が連想配列(ハッシュとも呼ばれる)の配列であるデータ構造です。

これは、AoA(Array of Arrays)データ構造(List of ListsまたはLoLとも呼ばれます)を使用して行うことができますが、フィールド番号を覚えておらずに名前でフィールドにアクセスするのが便利です。

Perl に付属の Perl データ構造 料理書から Perl データ構造の詳細を読むことができます。実行man perldscまたはperldoc perldsc.Perl変数( "perllolperlreftutperldataPerlには、スカラー、スカラー配列、およびスカラー連想配列(ハッシュと呼ばれる)の3つの組み込みデータ型があります。"."スカラー"は、数値、文字列、または数値などの単一の値です。引用する他の変数として)

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 =...とループ全体を1行のコードに置き換えることができます。foreachwhile (<>)非常に良いフレーズ砂糖):

  @record{@headers} = map { s/^\s+$//, $_ } split /\t/;

メモ:データ::ダンプ全体のデータ構造をきれいに印刷するためのPerlモジュールです。デバッグし、データ構造が実際に考えたものと同じであることを確認するのに役立ちます。そして偶然にも、出力はPerlスクリプトにコピーして貼り付け、変数に直接割り当てることができる形式です。

libdata-dump-perlパッケージのDebianおよび関連ディストリビューションで使用できます。他のディストリビューションでもそれをパッケージ化できます。それ以外の場合はCPANから取得します。または、スクリプトの最後の3行をコメントアウトまたは削除してください。ここでは使用する必要はありません。これは、出力ループに印刷されたデータを印刷する別の方法です。

たとえば、別の名前で保存し、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でソートされたレコードをダンプします(これはPerlのハッシュがソートされていないことをData :: Dumpが処理する方法です)。


その他の提案

このようなデータ構造にデータがある場合は、それをSQLデータベースに簡単に挿入できます。mysql/マリアデータベースまたはPostgreSQLまたはSQLite3。 Perlにはデータベースモジュールがあります(参照:データベースインタフェース)これらすべて以上のため。

(Debianなどではlibdbd-mysql-perl、、、、、でパッケージされていますlibdbd-mariadb-perl。他のディストリビューションでlibdbd-pg-perlはパッケージ名が異なります)libdbd-sqlite3-perllibdbi-perl

ところで、メイン構文解析ループは、次のような他のPerlモジュールを使用して実装することもできます。テキスト::CSV、CSVなどのファイル形式(タブ区切りなど)を解析できます。またはDBD::CSVText::CSVCSVファイルまたはTSVファイルを開き、SQLクエリを実行できます。まるでSQLデータベースのように

実際、これらのモジュールを使用してCSVファイルまたはTSVファイルをSQLデータベースにインポートするのは非常に単純な10〜15行のスクリプトであり、ほとんどは定型句設定項目です。実際のアルゴリズムは、SELECTを実行する単純なwhileループです。ソースデータを照会し、INSERTステートメントをターゲットデータに挿入します。

両方のモジュールと同じ Debian 用にパッケージされていlibtext-csv-perlますlibdbd-csv-perl。他のディストリビューション用にパッケージ化することもできます。そしていつものように、CPANで使用することができます。

答え2

\tとしてスペース文字isspace()(trueまたは一致する文字を返すgrep '[[:space:]]')、これは標準です噴射 POSIX 指定の動作: IFS 空白文字シーケンス ( に現れる空白文字$IFS) は、次のように処理されます。一つ先行、末尾、または空白ではなく、IFS文字の両側の区切り文字と区切り文字は無視されます。

もともとこの「機能」に由来するkshでは、この特殊処理はTAB、NL、および空白文字に制限されていましたが、POSIXはこれをロケールから空白と見なされるすべての文字に変更しました。ただし、実際にはほとんどのシェルはまだTAB / NL / SPC(デフォルトでもあります$IFS)に対してのみこれを行います。私が知る限り、唯一の例外(この点で唯一のPOSIX互換シェル)はyash

Bashでは5.0で動作が変更され、bashは空白文字に特別な処理が適用されるという点でksh93のように動作しますが、1バイトにエンコードされた文字にのみ適用されます。

治療法の比較スペース性格と入力するそれら:

$ 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以降では、TABを特別に処理したくない場合は、bashTABを空白として扱わないロケールを構築する必要があります。それにもかかわらず、これだけでは十分ではありません。空白以外の文字の場合(たとえば、TABの代わりに,、および空の文字列の代わりに)a,b,c,abcabc

より良いアプローチは、IFS分割に関連する空白文字の特別な処理を無効にしたり、適切な分割演算子を使用できるシェルを使用したり、@casが言ったように適切なプログラミング言語を使用することですperl。このために設計されていません。

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

たとえば、3行目${array[2][5]}6番目のフィールドが表示されます


1 IFS分割はもともとBourneシェルから来ましたが、Bourneシェルでは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[@]}

ここでの秘密は、空のフィールドがないように各フィールドに項目(この場合は最後にコンマ)を追加することです。その後、後で皮をむきます。空のフィールドと不足しているフィールドを区別する必要がある場合は、最後に追加の文字を追加してください。

空のフィールドにダミー値を入力することもできますが、これにはダミー値を割り当てることができる必要があります。 2つの代替を実行する必要があります。

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

列数を変更する予定がない場合は、7つの変数について読むことをお勧めします。

関連情報