![awk 命令从标题包含特定子字符串的 csv 文件中提取列](https://linux22.com/image/229349/awk%20%E5%91%BD%E4%BB%A4%E4%BB%8E%E6%A0%87%E9%A2%98%E5%8C%85%E5%90%AB%E7%89%B9%E5%AE%9A%E5%AD%90%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%20csv%20%E6%96%87%E4%BB%B6%E4%B8%AD%E6%8F%90%E5%8F%96%E5%88%97.png)
我有一个巨大的 csv 文件,其格式为
aaa1, "aaa2, aa214", aa21, "aa, a14", aa211, aa44, aaa445
data, data, data, data, data, data, data,
........................................
........................................
我想提取标题包含特定字符串的列,比如说a2
.对于上面的示例,这包括列aaa2
等等aa21
。
我尝试过的 awk 命令是
awk --csv 'NR==1 {for (i=1; i<=NF; i++) if ($i ~ /a2/) print $i}' file.csv
但这只返回匹配的标题,而不返回它们下面的列。请指出我正确的方向。我使用的是Linux系统。
答案1
mlr
确实支持这种伪造的 CSV 格式,并且可以基于正则表达式剪切字段:
$ mlr --csv --csv-trim-leading-space --allow-ragged-csv-input cut -rf a2 your-file.csv
"aaa2, aa214",aa21,aa211
data,data,data
不过,这不会扩展到内存不适合的 CSV。为了--allow-ragged-csv-input
应对每行中字段数量与示例中不同的 CSV,在任何情况下都必须完整读取文件,以了解有多少个列(没有标题的列会自动分配数字标题) )。
答案2
使用 GNU awkFPAT
并假设字段不包含换行符:
awk -v FPAT='[^,]*|\\s*("([^"]|"")*")\\s*' -v OFS=',' '
NR==1 {
for ( inFldNr=1; inFldNr<=NF; inFldNr++ ) {
if ( $inFldNr ~ /a2/ ) {
out2in[++numOutFlds] = inFldNr
}
}
}
{
for ( outFldNr=1; outFldNr<=numOutFlds; outFldNr++ ) {
inFldNr = out2in[outFldNr]
printf "%s%s", $inFldNr, (outFldNr<numOutFlds ? OFS : ORS)
}
}
' file.csv
"aaa2, aa214", aa21, aa211
data, data, data
我没有使用--csv
(也需要 GNU awk),因为您的输入文件不是有效的 CSV(,
s 和第一个"
s 之间有空格,并且由于,
第二行末尾的尾随而具有比标题更多的数据列),因此不应期望 CSV 解析器能够处理它。另外,即使您解决了这个问题,--csv
也会从每个列标题周围去掉引号,我猜您想保留它们,当并非所有字段都用双引号引起来时,这会有点问题。仅当字段可以包含换行符并且您无论如何都想从字段周围删除引号时,使用--csv
才比使用正确的设置明显更好。FPAT
如果你确实想尝试--csv
那么这个(未经测试的)可能对你有用:
awk --csv -v OFS=',' '
NR==1 {
for ( inFldNr=1; inFldNr<=NF; inFldNr++ ) {
if ( $inFldNr ~ /a2/ ) {
out2in[++numOutFlds] = inFldNr
}
}
}
{
for ( outFldNr=1; outFldNr<=numOutFlds; outFldNr++ ) {
inFldNr = out2in[outFldNr]
outVal = $inFldNr
if ( outVal ~ ("[" OFS ORS "\"]") ) {
gsub(/"/,"\"\"",outVal)
outVal = "\"" outVal "\""
}
printf "%s%s", outVal, (outFldNr<numOutFlds ? OFS : ORS)
}
}
' file.csv
但是没有简单的方法可以告诉"
在循环中添加 s 时哪些前导/尾随空格(如果有的话)最初位于引号内还是引号外,因此我只是将整个字段用引号括起来。
看使用 awk 高效解析 csv 的最稳健方法是什么有关使用 awk 解析 CSV 的更多信息。
答案3
使用乐(以前称为 Perl_6)
...使用 Raku 的Text::CSV
模块:
~$ raku -MText::CSV -e 'csv(in => csv(in => $*IN, sep => ", "), out => $*OUT);' < file
上面将把 CSV 文件(所有列)读入内存。该文件通过 std-in 接收,所有列都通过std-out$*IN
输出。$*OUT
请注意自定义", "
字段分隔符。
要过滤特定列(删除所有其他列),请使用 Raku 的grep
key:k
参数,该参数返回任何找到的列的数字索引:
~$ raku -MText::CSV -e 'my @aoa = csv(in => $*IN, sep => ", ");
my @col-nbrs = @aoa[0].grep(/a2/, :k);
for @aoa.map( *.[@col-nbrs]) {
.map(q["] ~ * ~ q["]).join("\t").put
};' < file
输入示例:
aaa1_a, "aaa2, aa214_b", aa21_c, "aa, a14_d", aa211_e, aa44_f, aaa445_g
data1_a, data1_b, data1_c, data1_d, data1_e, data1_f, data1_g,
data2_a, data2_b, data2_c, data2_d, data2_e, data2_f, data2_g,
示例输出(TSV,来自上面):
"aaa2, aa214_b" "aa21_c" "aa211_e"
"data1_b" "data1_c" "data1_e"
"data2_b" "data2_c" "data2_e"
上面包含与“a2”匹配的所有列名称。为了使输出更具可读性,每个字段的文本都用引号引起来,然后在选项卡join
上进行编辑\t
。不加引号,代码就简化了不少。下面的列被连接\t\t
并输出:
~$ raku -MText::CSV -e 'my @aoa = csv(in => $*IN, sep => ", ");
my @col-nbrs = @aoa[0].grep(/a2/, :k);
for @aoa.map( *.[@col-nbrs]) {
$_.join("\t\t").put
};' < file
aaa2, aa214_b aa21_c aa211_e
data1_b data1_c data1_e
data2_b data2_c data2_e
最后,您可以利用 的Text::CSV
输出功能,默认情况下: 1.逗号join
上的列,
,以及 2. 包含空格的双引号字段。
% raku -MText::CSV -e 'my @aoa = csv(in => $*IN, sep => ", ");
my @col-nbrs = @aoa[0].grep(/a2/, :k);
my @filtered; for @aoa.map( *.[@col-nbrs] ) {
@filtered.push($_);
}; csv(in => @filtered, out => $*OUT);' < file
"aaa2, aa214_b",aa21_c,aa211_e
data1_b,data1_c,data1_e
data2_b,data2_c,data2_e
https://raku.land/zef:Tux/Text::CSV
https://github.com/Tux/CSV/blob/master/doc/Text-CSV.md
https://docs.raku.org
https://raku.org
答案4
另一个非常方便的工具是鸭数据库。跑步
duckdb --csv -c "SELECT COLUMNS('.*a2.*') from read_csv_auto('input.csv',HEADER = true)" >output.csv
你得到
AAA2,AAA214 | AA21 | AA211 |
---|---|---|
数据 | 数据 | 数据 |
数据 | 数据 | 数据 |