如何从不可预测的格式化文件中提取单行/多行正则表达式匹配项,并将每一个放在一行中的输出文件中?

时间:2015-06-15 06:22:01

标签: linux shell unix replace grep

我有一个非常庞大的文件,如下所示:

<a>text</a>text
blah


<b>data1</b>abc<b>data2</b>    
  <b>data3</b>blahblah
    <c>text</c>
  <d>text</d>
<x>blahblah<b>data4   
   data5


        data6</b>  
    <b>data7
</x>

也就是说,它的格式是不可预测的。我需要提取每个<b>...</b>项(它可能包含多行文本!)并将它们中的每一个放在一个单独的行中。同时,我需要用一个空格替换换行符和空格。

期望的输出:

<b>data1</b>
<b>data2</b>
<b>data3</b>
<b>data4 data5 data6</b>

我发现的只有两步之遥:

gawk '{if ($0 != "") { printf "%s", gensub(/\s+/, " ", "g", gensub(/\s+$/, "", "g", $0)) } }' path/to/input.txt > path/to/single-line.txt  

然后

grep -Pzo '(?s)<b>.*?</b>' path/to/single-line.txt > path/to/output.txt

但我不喜欢它!必须将多GB文本文件转换为单行...似乎不太好。是否有可能在一次通过中“即时”解决这样的问题?

2 个答案:

答案 0 :(得分:1)

假设您的文档格式正确,即<b>开始标记始终与</b>结束标记匹配,那么这可能就是您所需要的:

sed 's@<[/]\?b>@\n&\n@g' path/to/input.txt | 
awk 'BEGIN {buf=""}
   /<b>/ {Y=1; buf=""}
   /<\/b>/ {Y=0; print buf"</b>"}
   Y {buf = buf$0}
' | tr -s ' '

输出:

<b>data1</b>
<b>data2</b>
<b>data3</b>
<b>data4 data5 data6</b>

说明:

我们首先使用sed 's@<[/]\?b>@\n&\n@g'<b></b>移到自己的行中。

然后我们用awk实现一个简单的解析器:

  • BEGIN {buf=""}:初始化缓冲区
  • /<b>/ {Y=1; buf=""}:找到<b>时,启用捕获(Y = 1)并清空缓冲区
  • /<\/b>/ {Y=0; print buf"</b>"}:找到</b>时,禁用捕获并打印缓冲内容以及结束标记
  • Y {buf = buf$0}:如果捕获标志为true,则将当前行追加到缓冲区

最后,我们通过tr -s ' '传递输出以将多个空格挤压到单个空格中。

如果你想要一行:

sed 's@<[/]\?b>@\n&\n@g' in.txt | awk 'BEGIN{B=""} /<b>/{Y=1;B=""} /<\/b>/{Y=0;print B"</b>"} Y{B=B$0}' | tr -s ' '

或将其另存为shell脚本(extract_b.sh):

#!/usr/bin/sh
sed 's@<[/]\?b>@\n&\n@g' "$1" | awk 'BEGIN{B=""} /<b>/{Y=1;B=""} /<\/b>/{Y=0;print B"</b>"} Y{B=B$0}' | tr -s ' '

并像这样使用它:

extract_b.sh path/to/input.txt > /path/to/output.txt

还使用mawk进行了测试,速度更快(在我的测试中为27 Mb / s与17Mb / s),您可能更喜欢将它用于多GB文件。

答案 1 :(得分:0)

我将假设你的源文件是XML - 看起来像它。 如果是,那么基本上,基于正则表达式的解析不能很好地工作 - 语义相同的XML将匹配不同的模式。

所以我建议这里的工作工具是一个XML解析器。我会这样做:

#!/usr/bin/perl
use strict;
use warnings;

use XML::Twig;

XML::Twig->new(
    'twig_handlers' => {
        'b' => sub { print $_ ->text_only =~ s/\s+/ /grs, "\n"; }
    }
)->parse( <> );

这将贯穿您的源数据,并随时打印b元素。

但是内存占用问题有点大。 XML需要大约10倍的内存,这真是坏消息。幸运的是,对于XML::Twig,您可以使用purge方法处理该情况:

    '_default_' => sub { $_[0] -> purge; }

这会在每个元素上设置一个处理程序(不是b),并且将清除内存数据到目前为止。

E.g。

#!/usr/bin/perl
use strict;
use warnings;

use XML::Twig;

XML::Twig->new(
    'twig_handlers' => {
        'b' => sub { print $_ ->text_only =~ s/\s+/ /grs, "\n"; }
         '_default_' => sub { $_[0] -> purge; }
    }
)->parse( <> );

使用./myscript.pl <yourfile>运行它。

如果您愿意,可以将其展开以便清晰:

#!/usr/bin/perl
use strict;
use warnings;

use XML::Twig;

sub handle_b {
    my ( $twig, $b_element ) = @_; 
    my $b_text = $b_element -> text_only;
    $b_text =~ s/\s+/ /gs; #replace multiline space with single space. 
    print $b_text,"\n"; 
}

sub purge_as_we_go {
    my ( $twig, $element ) = @_; 
    $twig -> purge; 
}

XML::Twig->new(
    'twig_handlers' => {
        'b' => \&handle_b,
        '_default_' => \&purge_as_we_go,
    }
)->parse( <> );

我们使用在perl中具有特殊含义的钻石运算符<> - 它是{em> 数据在STDIN上传输(因此我们可以cat XMLFILE | ./myscript.pl )或者它打开在命令行上提供的文件并读取它们(./myscript.pl some_xml_file

这类似于大多数unix工具的工作方式,但显然如果您愿意,可以parse ( <STDIN> );parsefile ( $ARGV[0] );

编辑:只是发现你也在标记之后 - 在这种情况下,您可以使用$element -> sprint来提供元素/属性等而不是text_only。 请注意 - 这可能不适用于嵌套元素。