如何使用awk使用非贪婪的regexp在嵌套分隔符中提取数据

时间:2016-11-09 17:14:42

标签: awk

这个问题以多种形式反复出现,有许多不同的多字符分隔符,所以恕我直言是值得一个规范的答案。

给定输入文件,如:

<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>

请注意,start或end可以是任何多字符字符串,它们之间的文本可以是除字符串之外的任何字符串,包括属于这些字符串的字符,例如<>个字符在这个例子中。

2 个答案:

答案 0 :(得分:1)

主要挑战是,由于awk仅支持贪婪匹配,因此您无法写出<foo>.*</foo>的任何变体,这些变体将停留在行的第一个</foo>而不是最后{{1} }}。解决方案是将每个开始和结束字符串转换为单个字符,这些字符不能出现在输入中,因此您可以编写</foo>,其中x和y是那些开始/结束字符,但是如何选择可以&#39的字符; t出现在输入中?你没有 - 你做了一个:

x[^xy]*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> ,并在输入中每次出现@后附加A。此时,@A的每次出现都代表@个字符,并且保证不会出现@B@,后面跟输入中的任何其他内容。

现在我们可以选择其他2个我们想要用来表示开始/结束字符串的字符,在这种情况下,我选择{},并将它们转换为某些{ {1}} - @@B等前缀字符串,此时每次出现的@C代表@B个字符,{代表{{1} }}字符,输入中的任何位置都没有@C}

现在我们要做的就是找到我们想要提取的字符串,将每个起始字符串{转换为我们选择的起始字符},以及每个字符串将字符串<foo>结束到结束字符{,然后我们可以使用</foo>的简单正则表达式来表示}的非贪婪版本。

当我们找到每个字符串时,我们只是以相反的顺序展开我们在上面所做的转换(请注意,您必须完全按照将它们应用于整个记录的相反顺序展开每个匹配字符串的替换),因此{[^{}]*}去返回<foo>.*</foo>{返回<foo>@B返回{等,我们有该字符串的原始文本。< / p>

以上内容适用于任何awk。如果您的开始/结束字符串包含RE元字符,那么您必须转义它们或使用@A循环而不是@来替换它们。

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

while(index(substr()))

显然,您并不需要将编码/解码功能放在单独的函数中,我只是将其分离出来以使该功能明确,并与使用它的循环分开。

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

答案 1 :(得分:1)

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

<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来解析数据)。

编辑为了与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>