여러 파일의 데이터를 단일 CSV 파일로 효율적으로 추출

여러 파일의 데이터를 단일 CSV 파일로 효율적으로 추출

동일한 구조를 가진 XML 파일이 많이 있습니다.

$ cat file_<ID>.xml
... 
 ... 
   ...
      <double>1.2342</double>
      <double>2.3456</double>
      ...
   ...
 ... 
... 

<double>여기서 각 XML 파일의 해당 항목 수는 고정되어 알려져 있습니다(내 특별한 경우에는 168).

csv다음과 같이 모든 XML 파일의 내용을 저장하는 파일을 작성해야 합니다 .

file_0001 1.2342 2.3456 ... 
file_0002 1.2342 2.3456 ... 

등.

이 작업을 어떻게 효율적으로 수행할 수 있습니까?


내가 생각 해낸 최고는 다음과 같습니다.

#!/usr/bin/env zsh

for x in $path_to_xmls/*.xml; do 

    # 1) Get the doubles ignoring everything else
    # 2) Remove line breaks within the same file
    # 3) Add a new line at the end to construct the CSV file
    # 4) Join the columns together

    cat $x | grep -F '<double>' | \ 
    sed -r 's/.*>([0-9]+\.*[0-9]*).*?/\1/' | \
    tr '\n' ' ' | sed -e '$a\'  |  >> table_numbers.csv

    echo ${x:t} >> file_IDs.csv
done
    
paste file_IDs table_numbers.csv > final_table.csv

~10K XML 파일이 포함된 폴더에 위 스크립트를 배치하면 다음과 같은 결과가 나타납니다.

./from_xml_to_csv.sh  100.45s user 94.84s system 239% cpu 1:21.48 total

나쁘지는 않지만 100배 또는 1000배 더 많은 파일을 처리할 수 있기를 바랍니다. 어떻게 하면 이 처리를 보다 효율적으로 만들 수 있습니까?

또한, 위의 솔루션을 사용하면 수백만 개의 파일을 처리하는 등 글로벌 확장이 한계에 도달하는 상황에 직면하게 될까요? (전형적인"too many args"질문).

고쳐 쓰다

이 질문에 관심이 있는 사람은 @mikeserve의 답변을 읽어보세요. 지금까지 가장 빠르고 확장성이 뛰어난 제품입니다.

답변1

이렇게 하면 트릭을 수행할 수 있습니다.

awk -F '[<>]' '
      NR!=1 && FNR==1{printf "\n"} 
      FNR==1{sub(".*/", "", FILENAME); sub(".xml$", "", FILENAME); printf FILENAME} 
      /double/{printf " %s", $3}
      END{printf "\n"}
    ' $path_to_xml/*.xml > final_table.csv

설명하다:

  • awk: 이 프로그램을 사용하여 awkGNU awk 4.0.1에서 테스트했습니다.
  • -F '[<>]'<: 및 >필드 구분 기호로 사용
  • NR!=1 && FNR==1{printf "\n"}: 전체의 첫 번째 줄( )이 아니고 NR!=1파일의 첫 번째 줄( FNR==1)이면 개행 문자를 출력한다.
  • FNR==1{sub(".*/", "", FILENAME); sub(".xml$", "", FILENAME); printf FILENAME}: 파일의 첫 번째 줄인 경우 /파일명()에서 마지막() 이전을 모두 삭제하고 뒤의()를 삭제한 후 결과를 인쇄()합니다.sub(".*/", "", FILENAME)FILENAME.xmlsub(".xml$", "", FILENAME)printf FILENAME
  • /double/{printf " %s", $3}줄에 "double"( /double/)이 포함되어 있으면 공백이 인쇄되고 그 뒤에 세 번째 필드( printf " %s", $3)가 표시됩니다. 숫자가 될 구분 기호로 <및 를 사용합니다 (첫 번째 필드는 첫 번째 필드 앞에 오는 항목이고 두 번째 필드는 입니다 ). 필요한 경우 여기에서 숫자 형식을 지정할 수 있습니다. 예를 들어 임의 의 숫자 대신 를 사용하면 소수점 이하 3자리가 출력되며 전체 길이(점수와 소수점 이하 자릿수 포함)는 최소 8자리가 됩니다.><double%8.3f%s
  • END{printf "\n"}: 마지막 줄 뒤에 추가 줄바꿈을 인쇄합니다(선택 사항일 수 있음).
  • $path_to_xml/*.xml: 파일 목록
  • > final_table.csvfinal_table.csv:결과를 넣으세요.

"인수 목록이 길어짐" 오류가 발생하면 직접 전달하는 대신 findwith 인수를 사용하여 파일 목록을 생성할 수 있습니다.-exec

find $path_to_xml -maxdepth 1 -type f -name '*.xml' -exec awk -F '[<>]' '
      NR!=1 && FNR==1{printf "\n"} 
      FNR==1{sub(".*/", "", FILENAME); sub(".xml$", "", FILENAME); printf FILENAME} 
      /double/{printf " %s", $3}
      END{printf "\n"}
    ' {} + > final_table.csv

설명하다:

  • find $path_to_xml: find파일을 나열 하라고 지시합니다.$path_to_xml
  • -maxdepth 1: 하위 폴더를 입력하지 마세요$path_to_xml
  • -type f:일반 파일만 나열합니다. (이 역시 $path_to_xml자신을 제외합니다.)
  • -name '*.xml': only list files that match the pattern*.xml`, 인용해야 합니다. 그렇지 않으면 쉘이 확장 모드를 시도합니다.
  • -exec COMMAND {} +: COMMAND대신 일치하는 파일을 매개변수로 사용합니다 {}. +여러 파일을 한 번에 전달할 수 있으므로 포크가 줄어듭니다. 각 파일에 대해 개별적으로 명령을 실행하는 대신 사용되는 경우 \;( ;따옴표 필요, 그렇지 않으면 셸에서 해석됨) .+

xargs다음과 함께 사용할 수도 있습니다 find.

find $path_to_xml -maxdepth 1 -type f -name '*.xml' -print0 |
 xargs -0 awk -F '[<>]' '
      NR!=1 && FNR==1{printf "\n"} 
      FNR==1{sub(".*/", "", FILENAME); sub(".xml$", "", FILENAME); printf FILENAME} 
      /double/{printf " %s", $3}
      END{printf "\n"}
    ' > final_table.csv

설명하다

  • -print0: 널 문자로 구분된 파일 목록을 출력합니다.
  • |(파이프): 표준 출력을 find표준 입력으로 리디렉션합니다.xargs
  • xargs: 표준 입력에서 명령을 빌드하고 실행합니다. 즉, 전달된 각 인수(이 경우 파일 이름)에 대해 명령을 실행합니다.
  • -0: xargs인수가 널 문자로 구분된다고 직접 가정합니다.

awk -F '[<>]' '      
      BEGINFILE {sub(".*/", "", FILENAME); sub(".xml$", "", FILENAME); printf FILENAME} 
      /double/{printf " %s", $3}
      ENDFILE {printf "\n"}
    ' $path_to_xml/*.xml > final_table.csv

which BEGINFILEENDFILE파일이 변경될 때 호출됩니다(awk가 지원하는 경우).

답변2

가능한 전역 확장 한도가 초과되었습니다 - 예, 아니오. 쉘이 이미 실행 중이므로 중지되지 않습니다. 그러나 전체 전역 배열을 단일 명령에 대한 인수로 전달하려는 경우에는 가능합니다. 이 문제를 해결하는 이식 가능하고 강력한 방법은 다음과 같습니다 find.

find . \! -name . -prune -name pattern -type f -exec cat {} + | ...

cat... 현재 디렉토리에서 이름이 일치하는 일반 파일 만 찾습니다.patterncat하지만 이를 초과하지 않도록 필요한 만큼만 호출됩니다 ARG_MAX.

하지만 실제로 GNU가 있기 때문에 sed우리는 할 수 있습니다 .거의sed그냥 하나의 스크립트로 모든 작업을 수행하세요 find.

cd /path/to/xmls
find . \! -name . -prune -name \*.xml -type f -exec  \
    sed -sne'1F;$x;/\n*\( \)*<\/*double>/!d' \
        -e  '$s//\1/gp;H' {} + | paste -d\\0 - -

나는 다른 방법을 생각했다. 이것은 ~이 될 것이다매우.빠르지만 파일당 정확히 168개의 일치 항목이 있고 파일 이름에 점이 하나만 있어야 합니다.

(   export LC_ALL=C; set '' - -
    while [ "$#" -lt 168 ]; do set "$@$@"; done
    shift "$((${#}-168))"
    find . \! -name . -prune -name \*.xml -type f      \
              -exec  grep -F '<double>' /dev/null {} + |
    tr \<: '>>' | cut -d\> -f1,4 | paste -d\  "$@"     |
    sed 'h;s|./[^>]*>||g;x;s|\.x.*||;s|..||;G;s|\n| |'
)

요청에 따라 명령 작동 방식에 대한 자세한 설명은 다음과 같습니다.

  1. ( ... )

    • 첫째, 전체 작은 스크립트는 자체 하위 쉘에서 실행됩니다. 왜냐하면 실행 중에 일부 전역 환경 속성을 변경하여 작업이 완료되면 변경한 모든 속성이 원래 값으로 복원되기 때문입니다.
  2. export LC_ALL=C; set '' - -
    • 현재 로케일을 로 설정하면 C필터 작업을 많이 줄일 수 있습니다 . UTF-8 로케일에서 모든 문자는 하나 이상의 바이트로 표시될 수 있으며 발견된 모든 문자는 수천 개의 가능한 문자 그룹에서 선택되어야 합니다. C 언어 환경에서 각 문자는 바이트이며 그 수는 128개뿐입니다. 전반적으로 문자 일치 속도가 빨라집니다.
    • set명령문은 쉘의 위치 매개변수를 변경합니다. 실행 set '' - -설정 은 , 및 $1입니다 .\0$2$3-
  3. while ... set "$@$@"; done; shift ...
    • 기본적으로 이 문의 요점은 168개의 대시 배열을 얻는 것입니다. 나중에 paste168번째 줄바꿈을 유지하면서 연속된 167개 줄바꿈 세트를 공백으로 대체할 것입니다. 가장 간단한 방법은 stdin에 168개의 매개변수 참조를 제공 -하고 이를 모두 함께 붙여넣도록 지시하는 것입니다.
  4. find ... -exec grep -F '<double>' /dev/null' ...
    • find부분은 이전에 논의되었지만 고정 문자열과 일치 grep할 수 있는 행만 인쇄합니다 . 첫 번째 매개변수를 생성하면 다음과 같은 매개변수를 얻을 수 있습니다.-F<double>grep/dev/null안 돼요문자열 일치 - grep각 호출이 항상 2개 이상의 파일 매개변수를 검색하는지 확인합니다. 2개 이상의 명명된 검색 파일로 호출하면 파일 이름은 항상 각 출력 줄의 시작 부분에 grep인쇄됩니다 .file_000.xml:
  5. tr \<: '>>'
    • 여기서는 의 출력에서 ​​또는 문자 grep의 각 발생을 변환합니다 .:<>
    • 이 시점에서 샘플 매치 라인은 다음과 같습니다 ./file_000.xml> >double>0.0000>/double>.
  6. cut -d\> -f1,4
    • cut첫 번째 또는 네 번째 문자별 필드에 없는 입력은 해당 출력에서 ​​제거됩니다 >.
    • 이 시점에서 샘플 매치 라인은 다음과 같습니다 ./file_000.xml>0.0000.
  7. paste -d\ "$@"
    • 이미 논의했지만 여기서는 paste일괄 입력 행으로 168을 사용합니다.
    • 이때 아래와 같이 168개의 일치하는 라인이 동시에 나타납니다../file_000.xml>0.000 .../file_000.xml>0.167
  8. sed 'h;s|./[^>]*>||g;x;s|\.xml.*||;s|..||;G;s|\n| |'
    • 이제 더 빠르고 작은 유틸리티가 대부분의 작업을 수행했습니다. 멀티 코어 시스템에서는 동시에 수행할 수도 있습니다. 그리고 그 유틸리티 -특히 cut그리고 paste. 하지만 나는 이것을 할 수 있다고 상상할 수 있는 만큼 했고 항소해야 한다.sedawksed
    • 먼저 h모든 입력 줄의 복사본을 만든 다음 g패턴 공간에서 모든 패턴 발생을 전역적으로 삭제했습니다 ./[^>]*>. 즉, 모든 파일 이름 발생을 삭제했습니다. 이때의 패턴 공간은 sed다음과 같습니다.0.000 0.0001...0.167
    • 그런 다음 이전 공간과 패턴 공간을 x변경 하고 모든 것을 삭제합니다 . 따라서 저장된 줄 복사본의 첫 번째 파일 이름부터 시작하는 모든 내용이 삭제됩니다. 그런 다음 처음 두 문자를 제거하거나 삭제하면 패턴 공간이 이제 다음과 같이 보입니다 .h\.xml.*./file_000
    • 이제 남은 것은 그것들을 서로 붙이는 것뿐입니다. ewline 문자 뒤의 패턴 공간 에 G이전 공백의 복사본을 추가 한 다음 ewline을 공백으로 바꿉니다 .h\ns///\n
    • 그래서 결국 패턴 공간은 file_000 0.000...0.167. 이는 sed각 파일 쓰기 출력이 find에 전달되는 것입니다 grep.

답변3

향후 유지 관리 프로그래머와 시스템 관리자를 대신하여 XML을 구문 분석하는 데 정규식을 사용하지 마십시오. XML은 정규식 구문 분석에 적합하지 않은 구조화된 데이터 유형입니다. 일반 텍스트인 것처럼 가장하여 "가짜"로 만들 수 있지만 XML에는 다르게 구문 분석되는 의미상 동일한 항목이 많이 있습니다. 예를 들어 줄 바꿈을 포함하고 단항 태그를 가질 수 있습니다.

그래서 - 파서를 사용하여 - XML이 유효하지 않기 때문에 일부 소스 데이터를 모의했습니다. 좀 더 완전한 샘플을 주시면 좀 더 완전한 답변을 드리겠습니다.

기본 수준에서는 double다음과 같이 노드를 추출합니다.

#!/usr/bin/env perl

use strict;
use warnings;
use XML::Twig;

my $twig = XML::Twig -> new;
$twig -> parse ( \*DATA ); 

foreach my $double ( $twig -> get_xpath('//double') ) {
   print $double -> trimmed_text,"\n";
}

__DATA__
<root> 
 <subnode> 
   <another_node>
      <double>1.2342</double>
      <double>2.3456</double>
      <some_other_tag>fish</some_other_tag>
   </another_node>
 </subnode>
</root> 

이것은 다음을 인쇄합니다:

1.2342
2.3456

그럼 확장해보자:

#!/usr/bin/env perl

use strict;
use warnings;
use XML::Twig;
use Text::CSV;

my $twig = XML::Twig->new;
my $csv  = Text::CSV->new;

#open our results file
open( my $output, ">", "results.csv" ) or die $!;
#iterate each XML File. 
foreach my $filename ( glob("/path/to/xml/*.xml") ) {
    #parse it
    $twig->parsefile($filename);
    #extract all the text of all the 'double' elements. 
    my @doubles = map { $_->trimmed_text } $twig->get_xpath('//double');
    #print it as comma separated. 
    $csv->print( $output, [ $filename, @doubles ] );

}
close($output);

나는 이것이 트릭을 수행해야 한다고 생각합니다(샘플 데이터 없이는 확실히 말할 수 없습니다). 하지만 주의하세요. XML 파서를 사용하면 (XML 사양에 따라) 완전히 효율적으로 수행할 수 있는 XML 형식 재지정이 발생하지 않습니다. CSV 파서를 사용하면 쉼표나 줄 바꿈이 포함된 필드에 얽매이지 않습니다.

보다 구체적인 노드를 찾고 있다면 보다 자세한 경로를 지정할 수 있습니다. 실제로 위의 코드는 단지 조회의 인스턴스일 뿐입니다 double. 하지만 다음을 사용할 수 있습니다.

get_xpath("/root/subnode/another_node/double")

답변4

각 파일을 두 번 씁니다. 아마도 이것은 가장 비싼 부분 일 것입니다. 대신에 전체 항목을 메모리(아마도 배열)에 보관하려고 할 것입니다. 그럼 마지막으로 한번 써보세요.

ulimit메모리 제한에 도달하기 시작했는지 확인하세요 . 이 워크로드를 10~100배 늘리면 10~100GB의 메모리가 필요할 수 있습니다. 반복당 수천 번 실행되는 루프에서 일괄 처리할 수 있습니다. 이것이 반복 가능한 프로세스여야 하는지는 잘 모르겠지만 더 빠르고 강력해야 한다면 더 정교해져야 합니다. 그렇지 않으면 배치가 손으로 꿰매어집니다.

또한 각 파일과 각 파이프에 대해 여러 프로세스를 생성합니다. 단일 프로세스를 사용하여 전체 구문 분석/수정(grep/sed/tr)을 수행할 수 있습니다. grep 후에 Zsh를 확장하여 다른 번역을 처리할 수 있습니다(참고자료 참조 man zshexpn). 또는 sed여러 표현식을 사용하여 한 번의 호출로 모든 단일 행 작업을 수행할 수 있습니다. (확장 정규식)을 피하고 탐욕스럽지 않으면 sed더 빠를 수 있습니다 . 여러 파일에서 일치하는 줄을 한 번에 추출하고 중간 임시 파일에 쓸 수 있습니다 -r. grep그러나 병목 현상을 이해하고 해결되지 않은 문제를 해결하지 마십시오.

관련 정보