awk:字段上的精确字符串匹配不使用 NUL 作为记录分隔符

awk:字段上的精确字符串匹配不使用 NUL 作为记录分隔符

给定一个在字段中包含换行符的文件(由双引号嵌入),我尝试使用 NUL 作为记录分隔符,然后选择所需的记录。为此,我用 NUL 替换了行尾,然后纠正了由换行符分割的字段(使用 完成sed)。然而,将 (GNU) 中的第一个字段awk与字符串精确匹配会失败。有趣的是,第一个字段上的字符串模式匹配失败,这使我认为RS="\x00"应用正确。

为什么会失败呢?为什么模式匹配有效?

示例文件input.txt

head1,head2,head3
a,b,c
b,no a in first field,c
a,"with quotes",c
a,"with ,",c
b,a,1
a,"with
 newline",c
b,1,a

awk在介绍 NUL 作品之前,通过精确的字符串记录选择:

$awk 'BEGIN {FS=OFS=","} {if ($1=="a") print}' input.txt

结果:

a,b,c
a,"with quotes",c
a,"with ,",c
a,"with

引入 NUL 并纠正“newline-splits”有效(注意"with\n newline"条目):

$sed -e 's/$/\x00/' -e 's/\(,"[^,"]*\)\x00/\1/' input.txt | cat -A

head1,head2,head3^@$
a,b,c^@$
b,no a in first field,c^@$
a,"with quotes",c^@$
a,"with ,",c^@$
b,a,1^@$
a,"with$
 newline",c^@$
b,1,a^@$

对 in 字段 1 使用模式匹配是有效的(请注意"a"in 其他字段如何失败,但"head1"匹配):

$sed -e 's/$/\x00/' -e 's/\(,"[^,"]*\)\x00/\1/' input.txt |
awk 'BEGIN {RS=ORS="\x00" ; FS=OFS=","}
     { if ($1~"a") print}' |
cat -A

head1,head2,head3^@$
a,b,c^@$
a,"with quotes",c^@$
a,"with ,",c^@$
a,"with$
 newline",c^@

然而:字段 1 中的精确匹配"a"失败:

sed -e 's/$/\x00/' -e 's/\(,"[^,"]*\)\x00/\1/' input.txt |
awk 'BEGIN {RS=ORS="\x00" ; FS=OFS=","} { if ($1=="a") print}' 

##<no output>##

我哪里错了?为什么在使用 NUL as 之前就可以工作RS

答案1

您的 sed 命令不会将换行符 ( \n) 更改为 NUL ( \0) ,而是更改为 NUL + 换行符 ( \0\n) (如图cat -A所示)。

当使用 GNU awk 并将 RS 设置为 时\0,后续记录(及其第一个字段)的第一个字符将为\n,这将破坏您的精确匹配。

换行符's/\(,"[^,"]*\)\x00/\1/'分割修正根本不会改变这一点——它只是将newline",c记录附加到前一个记录上。


一个快速而肮脏的“解决方案”是设置RS\0\n而不是仅仅设置\0。但是这种处理 csv 文件以便 awk 解析它们的方法并不可靠,所以你真的应该找到更好的东西。

用你的最后一个例子:

sed -e 's/$/\x00/' -e 's/\(,"[^,"]*\)\x00/\1/' input.txt |
gawk 'BEGIN {RS=ORS="\x00\n" ; FS=OFS=","} { if ($1=="a") print}' | cat -A
a,b,c^@$
a,"with quotes",c^@$
a,"with ,",c^@$
a,"with$
 newline",c^@$
sed -e 's/$/\x00/' -e 's/\(,"[^,"]*\)\x00/\1/' input.txt |
gawk 'BEGIN {RS="\x00\n" ; FS=OFS=","} { if ($1=="a") print}'
a,b,c
a,"with quotes",c
a,"with ,",c
a,"with
 newline",c

答案2

您的文件可能包含带有 CRLF 行结尾的 LF 中场,例如,如果它是从 MS-Excel 导出的。在这种情况下,你所需要的 gawk 就是:

awk 'BEGIN{RS=ORS="\r\n"; FPAT="[^,]*|(\"[^\"]*\")+"} $1=="a"' file

例如(cat -v仅使用以使 CR 显示为^Ms):

$ cat -v file
head1,head2,head3^M
a,b,c^M
b,no a in first field,c^M
a,"with quotes",c^M
a,"with ,",c^M
b,a,1^M
a,"with
 newline",c^M
b,1,a^M

$ awk 'BEGIN{RS=ORS="\r\n"; FPAT="[^,]*|(\"[^\"]*\")+"} $1=="a"' file | cat -v
a,b,c^M
a,"with quotes",c^M
a,"with ,",c^M
a,"with
 newline",c^M

如果有任何原因导致上述内容对您不起作用,请参阅https://stackoverflow.com/questions/45420535/whats-the-most-robust-way-to-efficiently-parse-csv-using-awk或在 gawkextlib 中下载/使用 gawks CSV 解析器扩展。

答案3

混合 sed awk 方法:

$ < file \
sed -e '
  s/$/\x00/
  s/\(,"[^,"]*\)\x00/\1/
  H;1h;$!d;g
  s/\x00\n/\x00/g
' |
awk '/^a,/' RS="\x00" -

评论: sed+awk 的混合我已经采用了你的代码并稍微调整了它以获得所需的结果。主要思想是去掉 sed 总是放置的换行符。因此,我们在处理每条记录后阻止 sed 打印。然后在 eof 处,我们去掉换行符,并将 NUL 分隔数据传递给 awk,并使用 NUL 作为记录分隔符。然后我们只需查找以 a 开头的记录,

输出:

a,b,c
a,"with quotes",c
a,"with ,",c
a,"with
 newline",c

下面给出了仅 awk 和仅 sed 的方法。他们依靠引用字段内的引用来加倍。

纯 sed 方法:

$ sed -Ee ':a
    /^(([^"]*"){2})*[^"]*$/!{
      $d;N;ba
    }
    /^a,/!d
' file

纯awk方法

$ awk -F\" '
   !(NF%2){
      t = $0; n = NF
      while (getline a > 0) {
        t = t ORS a
        n = n + split(a, _x, FS)
        if (!(nf%2)) break 
      }
      $0 = t
   }/^a,/
' file

相关内容