正则表达式如何从文件中取出多行?


10

正则表达式如何从文件中取出多行?

我经常想通过正则表达式获得多行/修改多行。一个例子:

我正在尝试读取XML / SGML文件的一部分(它们不一定格式正确或语法可预测,因此正则表达式比适当的解析器更安全。此外,我希望能够完全做到这一点在外壳脚本(在Solaris和Linux上运行)中仅知道一些关键词的非结构化文件。

XML示例:

<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>

从这里,我想阅读<tag1>如果其中包含foo某处。

像一个正则表达式(<tag1>.*?foo.*?</tag1>)应该给予正确的部分,但是等工具grep,并sed只为我的单线条的工作。我怎样才能得到

<tag1>
 <tag2>foo</tag2>
</tag1>

在这个例子中?



@evilsoup没错,但是我的问题不是专门关于XML / SGML文件,而是关于任何文本文件。
2014年

Answers:


7

如果您安装了GNU的grep你可以做多通过传递搜索-P(Perl的正则表达式)标志和激活PCRE_DOTALL(?s)

grep -oP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt
<tag1>
<tag2>foo</tag2>
</tag1>

如果以上内容在您的平台上不起作用,请尝试-z另外传递该标志,这会强制grep将NUL视为行分隔符,从而导致整个文件看起来像一行。

grep -ozP '(?s)<tag1>(?:(?!tag1).)*?foo(?:(?!tag1).)*?</tag1>' file.txt

在OP的示例文件上运行时,这在我的系统上没有任何输出。
terdon

为我工作。+1。感谢(?s)小费
内森·华莱士

@terdon,您正在运行什么版本的GNU grep?
iruvar

@ 1_CR (GNU grep) 2.14在Debian上。我按原样复制了OPs示例(仅添加了最后的换行符),然后继续运行您的示例,grep但没有结果。
terdon

1
@slm,我在pcre 6.6上,在RHEL上是GNU grep 2.5.1。您介意尝试grep -ozP而不是grep -oP在平台上尝试吗?
iruvar

3
#begin command block
#append all lines between two addresses to hold space 
    sed -n -f - <<\SCRIPT file.xml
        \|<tag1>|,\|</tag1>|{ H 
#at last line of search block exchange hold and pattern space 
            \|</tag1>|{ x
#if not conditional ;  clear buffer ; branch to script end
                \|<tag2>[^<]*foo[^\n]*</tag2>|!{s/.*//;h;b}
#do work ; print result; clear buffer ; close blocks
    s?*?*?;p;s/.*//;h;b}}
SCRIPT

如果执行上述操作,并根据显示的数据在该行的最后一个清理行之前进行操作,则应使用如下所示的sed模式空间:

 ^\n<tag1>\n<tag2>foo</tag2>\n</tag1>$

您可以随时使用look 打印出模式空间。然后,您可以处理\n字符。

sed l <file

将向您显示每一行在被调用sed的阶段对其进行处理l

因此,我刚刚对其进行了测试,并且在第一行中的\backslash后面还需要一个,comma,但是其他方法仍然可以正常运行。在这里,我将其放在一个中,_sed_function以便在整个答案中都可以出于演示目的方便地将其称为:(包含注释,但为简洁起见,此处将其删除)

_sed_function() { sed -n -f /dev/fd/3 
} 3<<\SCRIPT <<\FILE 
    \|<tag1>|,\|</tag1>|{ H
        \|</tag1>|{ x
            \|<tag2>[^<]*foo[^\n]*</tag2>|!{s/.*//;h;b}
    s?*?*?;p;s/.*//;h;b}}
#END
SCRIPT
<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>
FILE


_sed_function
#OUTPUT#
<tag1>
 <tag2>foo</tag2>
</tag1>

现在,我们将切换p为,l以便在开发脚本时看到正在使用的内容,并删除非操作演示,s?因此我们的最后一行sed 3<<\SCRIPT如下所示:

l;s/.*//;h;b}}

然后,我将再次运行它:

_sed_function
#OUTPUT#
\n<tag1>\n <tag2>foo</tag2>\n</tag1>$

好!所以我是对的-感觉很好。现在,让我们随机播放l一下,看看它插入但删除的行。我们将删除当前流,l并在其中添加一个,!{block}使其类似于:

!{l;s/.*//;h;b}

_sed_function
#OUTPUT#
\n<tag1>\n <tag2>bar</tag2>\n</tag1>$

这就是我们将其清除之前的样子。

我要向您展示的最后一件事是H我们建立旧空间。我希望可以演示几个关键概念。因此,我l再次删除了最后一个挂钩,并更改了第一行,以在最后添加一个窥视H旧空间的信息:

{ H ; x ; l ; x

_sed_function
#OUTPUT#
\n<tag1>$
\n<tag1>\n <tag2>bar</tag2>$
\n<tag1>\n <tag2>bar</tag2>\n</tag1>$
\n<tag1>$
\n<tag1>\n <tag2>foo</tag2>$
\n<tag1>\n <tag2>foo</tag2>\n</tag1>$

H旧空间可以承受线周期的影响-因此得名。所以人们经常绊倒-好吧,经常绊倒-是它在使用后需要删除。在这种情况下,我只x更改了一次,因此保持空间变成了模式空间,反之亦然,并且这种变化还可以承受线周期。

结果是我需要删除以前用作模式空间的保留空间。我首先使用以下命令清除当前模式空间:

s/.*//

只需选择每个字符并将其删除。我无法使用,d因为这将结束当前的行周期,并且下一条命令将无法完成,这将严重破坏我的脚本。

h

这的工作方式H与之类似,但是它会覆盖保留空间,因此,我只是将空白模式空间复制到了保留空间的顶部,有效地删除了它。现在我可以:

b

出来。

这就是我编写sed脚本的方式。


谢谢@slm!你真是个好人,你知道吗?
mikeserv

谢谢,干得好,非常快地上升到3k,接下来上升5k 8
slm

我不知道,@ slm。我开始发现自己在这里学习的越来越少-也许我已经超过了它的用处。我得考虑一下。香港专业教育学院甚至在过去几周才来到该网站。
mikeserv

至少达到10k。值得解锁的一切都在该级别上。继续努力,现在5k很快就会到来。
slm

1
好吧,@ slm-无论如何您都是稀有品种。我确实同意多个答案。这就是为什么当某些qs关闭时它会困扰我的原因。但这实际上很少发生。再次感谢您,slm。
mikeserv

2

如果您的文件像您的示例一样简单,@ jamespfinn的答案将非常有效。如果情况更复杂,<tag1>可能跨越两行,那么您将需要一个稍微复杂的技巧。例如:

$ cat foo.xml
<tag1>
 <tag2>bar</tag2>
 <tag3>baz</tag3>
</tag1>
<tag1>

 <tag2>foo</tag2>
</tag1>

<tag1>
 <tag2>bar</tag2>

 <tag2>foo</tag2>
 <tag3>baz</tag3>
</tag1>
$ perl -ne 'if(/<tag1>/){$a=1;} 
            if($a==1){push @l,$_}
            if(/<\/tag1>/){
              if(grep {/foo/} @l){print "@l";}
               $a=0; @l=()
            }' foo.xml
<tag1>

  <tag2>foo</tag2>
 </tag1>
<tag1>
  <tag2>bar</tag2>

  <tag2>foo</tag2>
  <tag3>baz</tag3>
 </tag1>

perl脚本将处理输入文件的每一行,

  • if(/<tag1>/){$a=1;}:如果找到了开始标记()$a1则将变量设置为<tag1>

  • if($a==1){push @l,$_}:对于每行,如果$a1,则将该行添加到array中@l

  • if(/<\/tag1>/) :如果当前行与结束标记匹配:

    • if(grep {/foo/} @l){print "@l"}:如果数组中保存的任何行@l<tag1>和之间的行</tag1>)与字符串匹配foo,则输出的内容@l
    • $a=0; @l=():清空列表(@l=())并$a重新设置为0。

除非有多个包含“ foo”的<tag1>,否则此方法效果很好。在这种情况下,它打印从年初的每一件事情的第一个<标记1>最后</标记1> ...结束
书斋

@den我用答案中显示的示例测试了它,其中包含3个<tag1>with foo,并且工作正常。什么时候对你失败?
terdon

使用regex解析xml感觉太错误了:)
Braiam 2014年

1

这是一个sed替代方案:

sed -n '/<tag1/{:x N;/<\/tag1/!b x};/foo/p' your_file

说明

  • -n 表示除非另有指示,否则请勿打印行。
  • /<tag1/ 首先匹配开始标签
  • :x 是可以稍后跳转到这一点的标签
  • N 将下一行添加到模式空间(活动缓冲区)。
  • /<\/tag1/!b x表示如果当前模式空间不包含任何结束标记,则跳转到x之前创建的标签。因此,我们一直在模式空间添加行,直到找到结束标记。
  • /foo/p表示如果当前模式空间与匹配foo,则应将其打印出来。

1

我认为您可以使用GNU awk来做到这一点,方法是将结束标签视为记录分隔符,例如用于已知的结束标签</tag1>

gawk -vRS="\n</tag1>\n" '/foo/ {printf "%s%s", $0, RT}'

或更一般的情况(带有正则表达式的结束标签)

gawk -vRS="\n</[^>]*>\n" '/foo/ {printf "%s%s", $0, RT}'

在@ terdon's上进行测试foo.xml

$ gawk -vRS="\n</[^>]*>\n" '/foo/ {printf "%s%s", $0, RT}' foo.xml
<tag1>

 <tag2>foo</tag2>
</tag1>

<tag1>
 <tag2>bar</tag2>

 <tag2>foo</tag2>
 <tag3>baz</tag3>
</tag1>

0

如果文件的结构与上面显示的完全一样,则可以将-A(后面的行)和-B(前面的行)标志用于grep ...,例如:

$ cat yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
<tag1>
 <tag2>foo</tag2>
</tag1>
$ grep -A1 -B1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>
$ grep -A1 -B1 foo yourFile.txt 
<tag1>
 <tag2>foo</tag2>
</tag1>

如果您的grep支持版本,则还可以使用更简单的-C(针对上下文)选项来打印周围的N行:

$ grep -C 1 bar yourFile.txt 
<tag1>
 <tag2>bar</tag2>
</tag1>

谢谢,但是不。这只是一个例子,真实的东西看起来很难预测;-)

1
那不是在其中找到带有foo的标签,而是在发现foo并显示上下文行
Nathan Wallace13年

@NathanWallace是的,这正是OP所要的,在问题中给出的情况下,此答案非常有效。
terdon

@terdon根本不是问题要问的。Quote:“如果<tag1>中包含foo,我想阅读它。” 这种解决方案就像“我想读取'foo'和1行上下文,无论'foo'出现在哪里”。按照您的逻辑,对这个问题的回答同样有效tail -3 input_file.xml。是的,它适用于此特定示例,但对这个问题不是有用的答案。
内森·华莱士

@NathanWallace我的观点是,OP特别声明这不是有效的XML格式,在这种情况下,在OP搜索的字符串周围打印N行就足够了。有了可用的信息,这个答案就足够了。
terdon
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.