파일을 읽고 배열로 저장합니다. 빈 문자열을 건너뛰지 마세요.

파일을 읽고 배열로 저장합니다. 빈 문자열을 건너뛰지 마세요.

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.perllolperlreftutperldataPerl 변수( "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 =...와 전체 루프는 단 한 줄의 코드로 대체될 수 있습니다.foreachwhile (<>)매우좋은 구문 설탕):

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

노트:데이터::덤프전체 데이터 구조를 예쁘게 인쇄하기 위한 Perl 모듈입니다. 디버깅하고 데이터 구조가 실제로 생각한 것과 같은지 확인하는 데 유용합니다. 그리고 우연히도 출력은 Perl 스크립트에 복사하여 붙여넣고 변수에 직접 할당할 수 있는 형식입니다.

libdata-dump-perl패키지의 데비안 및 관련 배포판 에서 사용할 수 있습니다 . 다른 배포판에서도 이를 패키지화할 수 있습니다. 그렇지 않으면 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키 이름으로 정렬된 레코드를 덤프합니다(이것은 Perl의 해시가 정렬되지 않는다는 사실을 Data::Dump가 처리하는 방법입니다).


기타 제안

이와 같은 데이터 구조에 데이터가 있으면 이를 SQL 데이터베이스에 쉽게 삽입할 수 있습니다.mysql/마리아 데이터베이스또는포스트그레SQL또는SQLite3. Perl에는 데이터베이스 모듈이 있습니다(참조:데이터베이스 인터페이스) 이 모든 것 이상을 위해.

(데비안 등에서는 libdbd-mysql-perl, libdbd-mariadb-perl, libdbd-pg-perl, libdbd-sqlite3-perl, 로 패키지되어 있습니다 libdbi-perl. 다른 배포판에서는 패키지 이름이 다릅니다)

그런데 메인 구문 분석 루프는 다음과 같은 다른 Perl 모듈을 사용하여 구현할 수도 있습니다.텍스트::CSV, CSV 및 유사한 파일 형식(예: 탭으로 구분)을 구문 분석할 수 있습니다. 또는DBD::CSVText::CSVCSV 또는 TSV 파일을 열고 SQL 쿼리를 실행할 수 있습니다 .마치 SQL 데이터베이스인 것처럼.

실제로 이러한 모듈을 사용하여 CSV 또는 TSV 파일을 SQL 데이터베이스로 가져오는 것은 매우 간단한 10-15줄 스크립트이며 대부분은 상용구 설정 항목입니다. 실제 알고리즘은 SELECT를 실행하는 간단한 while 루프입니다. 원본 데이터를 쿼리하고 INSERT 문을 대상 데이터에 삽입합니다.

두 모듈 모두 및 와 같은 데비안용으로 패키지되어 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에서는 $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 쉘에서 나왔지만 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[@]}

여기서 비결은 빈 필드가 없도록 각 필드에 항목(이 경우 끝에 쉼표)을 추가하는 것입니다. 그런 다음 나중에 껍질을 벗기십시오. 비어 있는 필드와 누락된 필드를 구별해야 하는 경우 끝에 추가 문자를 추가하세요.

빈 필드에 더미 값을 채울 수도 있지만 이를 위해서는 더미 값을 할당할 수 있어야 합니다. 두 가지 대체를 수행해야 합니다.

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가지 변수에 대해 읽어 보는 것도 좋습니다.

관련 정보