GNU awk、FPAT以及使用正则表达式匹配负字符串和特殊字符的正则表达式。

5

TL(请参见问题结尾的 TL;DR)

我使用管道符号作为字段分隔符(|),并使用反斜杠加引号对(\")来引用数据中包含分隔符的字段,例如:

1|\"2\"|\"3.1|3.2\"|4  # basically 1, 2, 3.1|3.2, 4

那就是(在awk中):

$1==1
$2==\"2\"
$3==\"3.1|3.2\"
$4==4

我决定尝试使用GNU awk的FPAT来解决字段问题,因为编写一个否定匹配正则表达式到\"似乎并不那么糟糕。
我找到了这个回答Regular expression to match a line that doesn't contain a word,其中包含一个链接到(外部链接)给定输入短语的负正则表达式在线生成器
由于该生成器目前仅支持字母数字和空格字符,因此将\"(反斜线引号)替换为bq,生成器提供了正则表达式:
^([^b]|b+[^bq])*b*$ 

|被替换为p,上面的数据被替换为:

1pbq2bqpbq3.1p3.2bqp4
1|\"2\"|\"3.1|3.2\"|4  # original for comparision

样例来自GNU Awk文档的FPAT (FPAT="([^,]*)|(\"[^\"]+\")"),被用来生成一个FPAT

FPAT="([^p]*)|(bq([^b]|b+[^bq])*b*bq)"

一项试验已经完成:

$ gawk 'BEGIN {
    FPAT="([^p]*)|(bq([^b]|b+[^bq])*b*bq)"
    OFS=ORS
}
{
    print $1,$2,$3,$4
}' data

输出结果为:

1
bq2bq
bq3.1p3.2bq
4

这是正确的。在程序中用|"替换pq后得到:

$ gawk 'BEGIN {
    FPAT="([^|]*)|(b\"([^b]|b+[^b\"])*b*b\")"
    OFS=ORS
}
{
    print $1,$2,$3,$4
}' data

输出:
1
b"2b"
b"3.1|3.2b"
4

这段代码原本是正确的,但是当用\替换b并添加转义字符后,结果如下:

(简短总结如何修复以下脚本中的转义问题)

$ gawk 'BEGIN {
    FPAT="([^|]*)|(\\\"([^\\]|\\+[^\\\"])*\\*\\\")"
    OFS=ORS
} 
{
    print $1,$2,$3,$4
}' data

并且输出失败或与之前不同:

1
\"2\"
\"3.1
3.2\"

我猜测我的\\有问题,但经过多次尝试和错误,我的脑海里充满了反斜杠,所有的想法几乎都逃脱了(双关语)。由于社区是关于分享的,我想与你们分享我的头痛。

编辑:显然这与引号中的反斜杠有关,因为如果我使用GNU awk的强类型类型FPAT=@/.../来定义,而不是定义FPAT="...",我会得到正确的输出:

$ gawk 'BEGIN {
    FPAT=@/([^|]*)|(\\\"([^\\]|\\+[^\\\"])*\\*\\\")/
    OFS=ORS
} 
{
    print $1,$2,$3,$4
}' data

现在输出:

1
\"2\"
\"3.1|3.2\"
4

关于 编辑:显然这与引号中的反斜杠有关 - 我不知道你在转义方面是否还有其他问题,但这不是你遇到的问题,正如我在答案中所说的那样,[^\\\"]并不意味着not \"。我尝试在您最后的代码段中使用FPAT,但是得到了awk:tst.awk:2:警告:regexp转义序列`\"'不是已知的regexp运算符,所以我不知道您想发布什么。 - Ed Morton
有趣。最近在使用 sub(/\"/...) 时,我也收到了同样的警告,但上面的代码段都没有出现过这种情况。感觉好像突然就开始出现了,如果有任何意义的话。 - James Brown
1
当将正则表达式存储在字符串中(FPAT="...\\...")而不是正则表达式中(FPAT=@/...\\.../)时,预计会有不同的转义次数结果,因为它在使用时会被评估两次,一次在转换为正则表达式时,另一次在作为该正则表达式时使用。这就是为什么 printf 'a\\b\n' | awk '{sub("\\\\","-")}1'(或 printf 'a\\b\n' | awk '{x="\\\\"; sub(x,"-")}1')需要两次转义,而 printf 'a\\b\n' | awk '{sub(/\\/,"-")}1' 只需要一次转义的原因。 - Ed Morton
1
啊,当然。谢谢@EdMorton。我很抱歉假期推迟了我的回复 - 新年快乐! - James Brown
显示剩余5条评论
1个回答

1

你似乎尝试使用 [^\\\"] 表示不是字符串 \",但它并不意味着这个,它的意思是既不是字符 \ 也不是字符 "。在 FPAT 正则表达式的这一部分中,你需要有一个要否定的单个字符,因此方法是将输入中的每个 \" 转换为一个不能出现在输入中的单个字符(我使用 \n,因为通常 RS 就是这样,但你可以使用任何不能出现在记录中的字符),然后将记录拆分成字段,再在使用每个单独的字段之前恢复 \"

$ cat tst.awk
BEGIN { FPAT="([^|]*)|(\n[^\n]+\n)" }
{
    gsub(/\\"/,"\n")              # Replace each\" with \n in the record
    $0 = $0                       # Re-split the record into fields
    for (i=1; i<=NF; i++) {
        gsub("\n","\\\"",$i)      # Replace each \n with \" in the field
        print "$"i"=" $i
    }
}

$ awk -f tst.awk file
$1=1
$2=\"2\"
$3=\"3.1|3.2\"
$4=4

If there is no specific char that can't be present in your input then it's easy to manipulate your input such that whatever character you like cannot be present during field splitting (I'm using \n again here but this time it'd work even if your input was multi-line records containing \ns, assuming you set RS appropriately to allow reading of multi-line records):

$ cat tst.awk
BEGIN { FPAT="([^|]*)|(\n[^\n]+\n)" }
{
    gsub(/@/,"@A")
    gsub(/\n/,"@B")
    gsub(/\\"/,"\n")
    $0 = $0
    for (i=1; i<=NF; i++) {
        gsub("\n","\\\"",$i)
        gsub("@B","\n",$i)
        gsub("@A","@",$i)
        print "$"i"=" $i
    }
}

$ awk -f tst.awk file
$1=1
$2=\"2\"
$3=\"3.1|3.2\"
$4=4


首先,非常感谢您的迅速回答。我试图使用“[^\"]”来表示“不是字符串\”,但它并不意味着这个。好吧,那只是生成的正则表达式的一部分,在整个(带有bpq)中似乎可以工作,但我无法让它与正确的字符一起工作。再说一遍,除了问题中的一个数据行之外,我还没有进一步测试它,所以我真的不知道我面临的陷阱。 - James Brown
1
它似乎只能与“bpq”一起使用,但实际上不可能正常工作。在该正则表达式中看到“[^bq]”是解决问题的重要线索。我怀疑您用于生成正则表达式的任何内容都认为“bq”是一个包含字符的变量,而不是意图成为2个字符字符串,但我不确定。 - Ed Morton

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接