如何使用awk使用非贪婪正则表达式在嵌套分隔符中提取数据



这个问题以许多不同的多字符分隔符的形式反复出现,所以IMHO值得一个规范的答案。

给定如下输入文件:

<foo> .. 1 <foo> .. a<2 .. </foo> .. </foo> <foo> .. @{<>}@ <foo> .. 4 .. </foo> .. </foo> <foo> .. 5 .. </foo>

如何使用非贪婪匹配awk来提取嵌套的开始(<foo>)和结束(</foo>)分隔符之间的文本?

期望输出(任意顺序)为:

<foo> .. a<2 .. </foo>
<foo> .. 1  .. </foo>
<foo> .. 4 .. </foo>
<foo> .. @{<>}@  .. </foo>
<foo> .. 5 .. </foo>

请注意,开始或结束可以是任何多字符字符串,它们之间的文本可以是除了这些字符串之外的任何内容,包括这些字符串的一部分的字符,例如本例中的<>字符。

主要的挑战是,由于awk只支持贪婪匹配,因此您不能编写任何将在行中第一个</foo>而不是最后一个</foo>处停止的<foo>.*</foo>变体。解决方案是将每个开始和结束字符串转换为不能出现在输入中的单个字符,因此您可以编写x[^xy]*y,其中x和y是那些开始/结束字符,但您如何选择不能出现在输入中的字符?你不-你做一个:

$ cat nonGreedy.awk
{
    $0 = encode($0)
    while ( match($0,/({[^{}]*})/) ) {
        print decode(substr($0,RSTART,RLENGTH))
        $0 = substr($0,1,RSTART-1) substr($0,RSTART+RLENGTH)
    }
}
function encode(str) {
    gsub(/@/,"@A",str)
    gsub(/{/,"@B",str); gsub(/}/,"@C",str)
    gsub(/<foo>/,"{",str); gsub(/</foo>/,"}",str)
    return str
}
function decode(str) {
    gsub(/}/,"</foo>",str); gsub(/{/,"<foo>",str)
    gsub(/@C/,"}",str); gsub(/@B/,"{",str)
    gsub(/@A/,"@",str)
    return str
}
$ awk -f nonGreedy.awk file
<foo> .. a<2 .. </foo>
<foo> .. 1  .. </foo>
<foo> .. 4 .. </foo>
<foo> .. @{<>}@  .. </foo>
<foo> .. 5 .. </foo>

上面的工作是通过你选择任何不能出现在START/END字符串中的字符(注意它不一定是一个不能出现在输入中的字符,只是不在那些字符串中),在这种情况下,我选择@,并在输入中每次出现后附加A。此时,每次出现@A都代表一个@字符,并且保证在输入的任何地方都不会出现@B@后面跟着任何其他字符。

现在我们可以选择另外两个字符,我们想用它来表示开始/结束字符串,在这种情况下,我选择{},并将它们转换为一些@前缀的字符串,如@B@C,在这一点上,@B的每一次出现都代表一个{字符,@C代表一个}字符,没有{ s或} s在输入的任何地方。

现在要找到我们想要提取的字符串,剩下要做的就是将每个开始字符串<foo>转换为我们选择的开始字符{,并将每个结束字符串</foo>转换为结束字符},然后我们可以使用{[^{}]*}的简单regexp来表示<foo>.*</foo>的非贪婪版本。

当我们找到每个字符串时,我们只是以相反的顺序展开我们上面所做的转换(注意,你必须以与整个记录完全相反的顺序展开每个匹配字符串的替换),因此{返回到<foo>, @B返回到{, @A返回到@,等等,我们得到了该字符串的原始文本。

上面的代码可以在任何awk中工作。如果你的开始/结束字符串包含正则元字符,那么你必须转义这些字符或使用while(index(substr()))循环而不是gsub()来替换它们。

请注意,如果您使用gawk并且标签没有嵌套,那么您可以完全保留上述两个函数,并将脚本的其余部分更改为:

BEGIN { FPAT="{[^{}]*}" }
{
    $0 = encode($0)
    for (i=1; i<=NF; i++) {
        print decode($i)
    }
}

显然,你不需要把编码/解码功能放在单独的函数中,我只是把它分离出来,使该功能显式,并与使用它的循环分开,以便清晰。

关于何时/如何应用上述方法的另一个示例,请参见https://stackoverflow.com/a/40540160/1745001。

我的(当前版本)解决方案从前面处理问题,因此输出不完全相同:

<foo> .. 1                   # second
  <foo> .. a<2 .. </foo> ..  # first in my approach
</foo> 
<foo> .. @{<>}@              # fourth
  <foo> .. 4 .. </foo> ..    # third
</foo> 
<foo> .. 5 .. </foo>         # fifth

如果程序向后遍历数组arrseps,输出将是相同的(可能),但是我只是暂时没有时间了。

在Gnu awk中(用于使用split和四个参数来解析数据)。

EDIT为了与Gnu awk以外的其他函数兼容,我添加了gsplit()函数,这是一个粗糙的Gnu awk split替代品。

$ cat program.awk
{ data=data $0 }                         # append all records to one var
END {
    n=gsplit(data, arr, "</?foo>", seps) # split by every tag
    for(i=1;i<=n;i++) {                  # atm iterate arrays from front to back
        if(seps[i]=="<foo>")             # if element opening tag
            stack[++j]=seps[i] arr[i+1]  # store tag ang wait for closing tag
        else {
            stack[j]=stack[j] (seps[i]==prev ? arr[i] : "")
            print stack[j--] seps[i] 
        } 
        prev = seps[i]
    }
}
# elementary gnu awk split compatible replacement
function gsplit(str, arr, pat, seps,    i) {
    delete arr; delete seps; i=0
    while(match(str, pat)) {
        arr[++i]=substr(str,1,(RSTART-1))
        seps[i]=substr(str,RSTART,RLENGTH)
        str=substr(str,(RSTART+RLENGTH))
    }
    arr[++i]=substr(str,1)
    return i
}
运行:

$ awk -f program.awk file
<foo> .. a<2 .. </foo>
<foo> .. 1  .. </foo>
<foo> .. 4 .. </foo>
<foo> .. @{<>}@  .. </foo>
<foo> .. 5 .. </foo>

最新更新