给定一个在字段中包含换行符的文件(由双引号嵌入),我尝试使用 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 显示为^M
s):
$ 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