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