从多个文件高效提取数据到单个 CSV 文件

Ame*_*ina 3 zsh xml text-processing csv

我有大量具有相同结构的 XML 文件:

$ cat file_<ID>.xml
... 
 ... 
   ...
      <double>1.2342</double>
      <double>2.3456</double>
      ...
   ...
 ... 
... 
Run Code Online (Sandbox Code Playgroud)

其中<double>每个 XML 文件中此类条目的数量是固定且已知的(在我的特定情况下为 168)。

我需要构建一个csv包含所有这些 XML 文件内容的文件,如下所示:

file_0001 1.2342 2.3456 ... 
file_0002 1.2342 2.3456 ... 
Run Code Online (Sandbox Code Playgroud)

等等。

我怎样才能有效地做到这一点?


我想出的最好的是:

$ cat file_<ID>.xml
... 
 ... 
   ...
      <double>1.2342</double>
      <double>2.3456</double>
      ...
   ...
 ... 
... 
Run Code Online (Sandbox Code Playgroud)

当我在一个包含 ~10K XML 文件的文件夹中计时上述脚本时,我得到:

./from_xml_to_csv.sh  100.45s user 94.84s system 239% cpu 1:21.48 total
Run Code Online (Sandbox Code Playgroud)

并不可怕,但我希望能处理 100 倍或 1000 倍以上的文件。我怎样才能使这个处理更有效率?

另外,使用我上面的解决方案,我是否会遇到全局扩展达到限制的情况,例如在处理数百万个文件时?(典型"too many args"问题)。

更新

对于对这个问题的一个很好的解决方案感兴趣的人,请阅读@mikeserve 的回答。到目前为止,它是最快的,也是扩展最好的。

Ada*_*hon 5

这应该可以解决问题:

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
Run Code Online (Sandbox Code Playgroud)

解释:

  • 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)。使用<>作为分隔符,这将是数字(第一个字段是第一个字段之前的任何内容<,第二个字段是double)。如果需要,您可以在此处格式化数字。例如,使用%8.3f代替%s任何数字将打印 3 个小数位,并且总长度(包括点和小数位)至少为 8。
  • END{printf "\n"}: 在最后一行之后打印一个额外的换行符(这可以是可选的)
  • $path_to_xml/*.xml: 文件列表
  • > final_table.csv:final_table.csv通过重定向输出将结果放入

在“argument list to long”错误的情况下,可以使用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
Run Code Online (Sandbox Code Playgroud)

解释:

  • 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`,这需要引用,否则 shell 将尝试扩展模式
  • -exec COMMAND {} +:COMMAND使用匹配的文件作为参数运行命令代替{}. +表示可以一次传递多个文件,从而减少分叉。如果您使用\;(;需要被引用,否则它由 shell 解释) 而不是+单独为每个文件运行命令。

您还可以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
Run Code Online (Sandbox Code Playgroud)

解释

  • -print0: 输出以空字符分隔的文件列表
  • |(pipe): 将标准输出重定向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
Run Code Online (Sandbox Code Playgroud)

where BEGINFILE,ENDFILE在更改文件时调用(如果您的 awk 支持它)。

  • 谢谢——恐怕我现在得到 `"argument list too long: awk"` 。 (2认同)