如何通过正则表达式从文件中获取多行?
我经常想通过正则表达式获取多行/修改多行。示例案例:
我正在尝试读取 XML/SGML 文件的一部分(它们不一定格式良好或采用可预测的语法,因此正则表达式比正确的解析器更安全。此外,我希望能够完全做到这一点shell 脚本(在 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>
在这个例子中?
答案1
如果你安装了 GNU grep,你可以通过传入-P
(perl-regex) 标志并激活来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
答案2
#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>$
您可以随时使用l
ook 打印出您的图案空间。然后您可以对\n
字符进行寻址。
sed l <file
将向您展示每行在被调用的sed
阶段处理它l
。
\backslash
所以我刚刚测试了它,在第一行之后还需要一个,comma
,但否则按原样工作。在这里,我将其放在 a 中_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
为 an ,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
ook 并更改第一行以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
脚本的方式。
答案3
如果您的文件像您的示例一样简单,@jamespfinn 的答案将非常有效。如果您遇到更复杂的情况,<tag1>
可能跨越 2 行以上,则需要稍微复杂的技巧。例如:
$ 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;}
:如果找到开始标记 ( ),则将变量$a
设置为。1
<tag1>
if($a==1){push @l,$_}
:对于每一行,如果$a
是1
,则将该行添加到数组中@l
。if(/<\/tag1>/)
: 如果当前行与结束标记匹配:if(grep {/foo/} @l){print "@l"}
:如果数组中保存的任何行(这些是和@l
之间的行)与字符串匹配,则打印 的内容。<tag1>
</tag1>
foo
@l
$a=0; @l=()
:清空列表 (@l=()
) 并设置$a
回 0。
答案4
我认为你可以使用 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 上测试它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>