grep 特定 csv 列中的 2 个单词并计算它们

grep 特定 csv 列中的 2 个单词并计算它们

我正在尝试找到一种更好的方法来完成以下 grep/awk 查询。以下是该问题的一个简单示例。

我已经用我的正则表达式达到了这一点:

grep -Po ^(?:[^,]+,\s?){7}(Want|Need) | awk -F ',' 'NR>=2{print $8}' | sort | uniq -c

我的 CSV 文件如下所示:

1896,Ranger,2021,State,Postcode,Surname,Industry,Want,Turbo,Good
1896,Ranger,2021,State,Postcode,Surname,Industry,Selling,Turbo,Good
1896,Ranger,2021,State,Postcode,Surname,Industry,Need,Turbo,Good

上面的操作使用 grep 打印整行:

1896,Ranger,2021,State,Postcode,Surname,Industry,Want
1896,Ranger,2021,State,Postcode,Surname,Industry,Need

然后我可以计算第 8 列中的值。我的问题是如何编写 grep/regex 查询以仅返回我使用 regex 选择的组。

例如:

Want
Need

写这篇文章的原因纯粹是为了理解这里使用正则表达式的更好方法。我知道还有其他方法可以做到这一点。

答案1

听起来您正在寻找 PCRE\K断言。从佩尔雷:

这种构造有一种特殊形式,称为 \K(自 Perl 5.10.0 起可用),它使正则表达式引擎“保留”在 \K 之前匹配的所有内容,而不将其包含在 $& 中。

所以

$ grep -Po '^(?:[^,]+,\s?){7}\K(Want|Need)' file.csv
Want
Need

更一般地,这种事情是用向后看断言 - 然而 Perl 不支持可变长度后向查找,grep -P 也不支持:

$ grep -Po '^(?<=(?:[^,]+,\s?){7})(Want|Need)' file.csv
grep: lookbehind assertion is not fixed length

也可以看看前向和后向零长度断言

答案2

请注意,这-P是 GNU 实现的非标准(可选且长期被认为是实验性的)选项grep,它使用 libpcre(perl 正则表达式的独立实现)来进行匹配

libpcre确实带有自己的grep命令作为示例代码 ( pcregrep),尽管它现在已经发展成为成熟的grep实现,例如可以在一些 GNU/Linux 发行版上的自己的包中找到。

pcregrep扩展了 GNUgrep-o非标准选项,以采用可选的数字参数来输出相应的捕获组:

所以在这里:

pcregrep -o1 '^(?:[^,]+,\s?){7}(Want|Need)'

或者你可以使用真实的东西,它也有一个优点,即使在既没有 GNU 的系统上grep(或者在grep没有 PCRE 支持的情况下构建 GNU 的系统)也可以工作pcregrep

perl -lne 'print $1 if /^(?:[^,]+,\s?){7}(Want|Need)/'

但请注意perl,默认情况下,不会像 GNU 那样根据语言环境的文本编码对输入进行解码grep。在这种特定情况下,您匹配的文本仅使用可移植字符集中的字符,这可能是相当有利的,因为即使输入的编码与区域设置不同,它仍然可以工作。

如果您想perl根据区域设置的编码对输入上的文本进行解码(并在输出上进行编码),您可以添加-Mopen=locale.


但就您而言,没有太多值得使用 perl 正则表达式的地方。您在那里使用的所有 Perl 运算符都有标准的 ERE 运算符等效项(甚至是 BRE,除了交替之外)。

  • (?:...):它只是 perl/ERE(...)或 BRE \(...\),没有捕获。
  • +:ERE 中相同,\{1,\}BRE 中相同
  • ?:与 ERE 相同,\{0,1\}在 ERE 中
  • {7}:ERE 中相同,\{7\}BRE 中相同
  • (Want|Need):与 ERE 相同(尽管在选择交替方向时行为略有不同)。
  • \s[[:space:]]在 BRE 和 ERE 中
  • ^, [^,]: BRE 或 ERE 中相同

sed是提取模式中匹配部分的工具(而grep,以 的ed命令命名g/re/p是打印与常规表达式p匹配的行)。标准使用 BRE,但大多数实现支持切换到 ERE(这将添加到标准的下一版本中)。resedsed-E

因此,在这里,作为perl上述命令的等效命令,您也可以进行可移植的操作:

LC_ALL=C sed -nE 's/^([^,]+,[[:space:]]?){7}(Want|Need).*$/\2/p'

或者没有-E

LC_ALL=C sed -n 's/^\([^,]\{1,\},[[:space:]]\{0,1\}\)\{7\}\(Want\).*$/\2/p; t
                 s/^\([^,]\{1,\},[[:space:]]\{0,1\}\)\{7\}\(Need\).*$/\2/p'

或者用其他东西替换它们WantNeed

LC_ALL=C sed -E 's/^(([^,]+,[[:space:]]?){7})(Want|Need)/\1Desire/'
LC_ALL=C sed 's/^\(\([^,]\{1,\},[[:space:]]\{0,1\}\)\{7\}\)Want/\1Desire/; t
              s/^\(\([^,]\{1,\},[[:space:]]\{0,1\}\)\{7\}\)Need/\1Desire/

1 从那时起,其他实现添加了自己的-P选项来使用perl类似正则表达式,而不总是像 ast-open 那样使用 libpcre grep(它确实支持环视断言,但不支持\K

答案3

您已经在使用 awk,所以这里不需要grep。你不需要sort,也不uniq -c需要。例如:

$ awk -v search=Want -F, '$8 ~ search { count[$8]++ };
    END { for (f in count) { printf "%5i\t%s\n", count[f], f}}' input.csv 
    1   Want

$ awk -v search='Want|Need' -F, '$8 ~ search { count[$8]++ };
    END { for (f in count) { printf "%5i\t%s\n", count[f], f}}' input.csv 
    1   Want
    1   Need

或者,如果您希望它也打印匹配的行:

$ awk -v search='Want|Need' -F, '$8 ~ search { count[$8]++ ; print };
    END { for (f in count) { printf "%5i\t%s\n", count[f], f}}' input.csv 
1896,Ranger,2021,State,Postcode,Surname,Industry,Want,Turbo,Good
1896,Ranger,2021,State,Postcode,Surname,Industry,Need,Turbo,Good
    1   Want
    1   Need

-v IGNORECASE=1您可以通过添加到命令行来使用 GNU awk 添加不区分大小写的功能,如果需要,甚至可以添加精确匹配等奇特功能:

$ awk -v search='want' -v exact=1 -v IGNORECASE=1 -F, '
    BEGIN {if (exact == 1) search = "^(" search ")$"};
    $8 ~ search { count[$8]++ ; print };
    END { for (f in count) { printf "%5i\t%s\n", count[f], f}}' input.csv 
1896,Ranger,2021,State,Postcode,Surname,Industry,Want,Turbo,Good
    1   Want

以下不会产生任何输出,因为 while antis in Want,它与整个字段 8 不完全匹配:

$ awk -v search='ant' -v exact=1 -v IGNORECASE=1 -F, '
    BEGIN {if (exact == 1) search = "^(" search ")$"};
    $8 ~ search { count[$8]++ ; print };
    END { for (f in count) { printf "%5i\t%s\n", count[f], f}}' input.csv 

注意:显然有更好的方法来进行命令行选项处理(例如使用获取选择函数或通过编写 shell 脚本包装器来使用 sh/bash 内置函数getopt),但是使用 awk 的-v选项从脚本外部在 awk 中设置变量对于像这样的简单任务来说是简单方便的。

顺便说一句,awk 还允许在不使用的情况下分配变量,-v方法是将它们添加到脚本本身之后的命令行中(awk 会将形式的任何参数解释x=y为将变量 x 设置为值 y。不幸的是,这使得很难使用带有=在他们中 - 也许不可能,我不记得除了“那么不要这样做”之外我是否找到了解决方案)。

但与使用时不同的-v是,这些变量是不是可以在BEGIN {}声明中找到。例如,ant即使我们正在设置,以下内容也会匹配exact=1

$ awk -F, 'BEGIN {if (exact == 1) search = "^(" search ")$"};
           $8 ~ search { count[$8]++ ; print };
           END { for (f in count) { printf "%5i\t%s\n", count[f], f}}' \
    search=ant IGNORECASE=1 exact=1 input.csv 
1896,Ranger,2021,State,Postcode,Surname,Industry,Want,Turbo,Good
    1   Want

来自 GNU awk 的手册页:

如果命令行上的文件名具有以下形式,var=val 则将其视为变量赋值。该变量var 将被赋予值val。 (有时候是这样的BEGIN已运行任何 规则。)

命令行变量分配对于动态分配值到变量 AWK 用来控制如何将输入分解为字段和记录最有用。如果需要对单个数据文件进行多次传递,它对于控制状态也很有用。

IMO,最好将其视为与旧 awk 脚本兼容的遗留功能并仅使用-v.

-v var=val
--assign var=val

在程序开始执行之前,将值赋给val变量。var这样的变量值可用于BEGINAWK 程序的规则。

(上面引号中的“after”和“are”是我添加的粗体强调)

答案4

假设如果您正在搜索的字符串之一没有出现在您希望看到它打印的计数0而不是根本不打印的输入中,那么健壮、可移植、高效、简洁的方法就是简单地执行此操作:

$ awk -F',' -v tgts='Want,Need' '
    { cnt[$8]++ }
    END { split(tgts,t); for (i in t) print t[i], cnt[t[i]]+0 }
' file
Want 1
Need 1

所以很难弄清楚正则表达式会在这里发挥作用。也许是以下几点:

$ awk -F',' -v tgts='Want|Need' '
    $8 ~ ("^"tgts"$") { cnt[$8]++ }
    END { split(tgts,t,/[|]/); for (i in t) print t[i], cnt[t[i]]+0 }
' file
Want 1
Need 1

或者:

$ awk -F',' -v tgts='Want|Need' '
    $0 ~ ("([^,]*,){7}"tgts"(,|$)") { cnt[$8]++ }
    END { split(tgts,t,/[|]/); for (i in t) print t[i], cnt[t[i]]+0 }
' file
Want 1
Need 1

但正则表达式只是使脚本复杂化并使它们更加脆弱(如果您想要查找的字符串包含正则表达式元字符(如.或 ),则具有正则表达式的脚本将会失败*,而第一个脚本将继续工作)并且不会增加任何价值,除非您有数十亿$8您输入中的唯一值。

相关内容