![将固定宽度文件转换为 CSV 并删除尾随空格](https://linux22.com/image/219452/%E5%B0%86%E5%9B%BA%E5%AE%9A%E5%AE%BD%E5%BA%A6%E6%96%87%E4%BB%B6%E8%BD%AC%E6%8D%A2%E4%B8%BA%20CSV%20%E5%B9%B6%E5%88%A0%E9%99%A4%E5%B0%BE%E9%9A%8F%E7%A9%BA%E6%A0%BC.png)
我的输入文件是:
$ cat -e myfile.txt
999a bcd efgh555$
8 z 7 $
1 xx xx xx 48 $
我需要一个列中没有尾随空格的 CSV:
999,a bcd efgh,555
8,z,7
1,xx xx xx,48
到目前为止,我成功地在需要的地方添加了昏迷:
$ gawk '$1=$1' FIELDWIDTHS="3 10 3" OFS=, myfile.txt
999,a bcd efgh,555
8 ,z ,7
1 ,xx xx xx ,48
我怎样才能删除尾随空格?
编辑:数据中可能已经有逗号,所以我需要:(i)将字段用双引号括起来,(ii)使用\"
(或""
按照RFC 4180)。例如,a,aab"bbccc
-> "a,aa","b\"bb","ccc"
。
- 我可以使用
gawk
(不仅awk
) - 我对任何其他解决方案持开放态度(例如
perl
)。 - 我需要一个有效的解决方案(例如不是
gawk ... | sed ...
),因为我有很多大文件要处理。 - 我知道字段宽度,因此不需要
FIELDWIDTHS
自动计算。
答案1
和perl
:
<your-file perl -C -lnse 'print map {s/\s+$//r} unpack "a3a10a3"' -- -,=,
unpack()
进行相当于 gawk 的FIELDWIDTHS
处理。
$,
,此处与 awk 的等效项OFS
设置为,
with -,=,
where-s
导致-var=value
参数被理解为分配value
给$var
。或者,您可以省略-s
, 并BEGIN{$, = ","}
在开头添加一条语句,就像BEGIN{OFS = ","}
在 awk 中代替一样-v OFS=,
。
-C
如果区域设置使用 UTF-8 作为其字符映射,则将输入视为 UTF-8 编码,忽略具有不同多字节字符映射的区域设置,因为这些天几乎从未使用过这些区域设置。
正如您所发现的,如果要修剪的空格字符都是 ASCII 字符,则可以通过使用A
说明符来进一步简化,而不是a
删除unpack()
尾随 ASCII 空格(和 NUL):
<your-file perl -C -lnse 'print unpack "A3A10A3"' -- -,=,
那是为了宽度从字符数来考虑。
对于字节数,请删除-C
.
对于字素簇的数量,您可以替换unpack "a3a10a3"
为/^(\X{3})(\X{10})(\X{3})/
。
对于显示宽度,考虑到每个字符的宽度(包括零宽、单宽和双宽字符,但不支持 TAB1、CR...等控制字符),在 中zsh
,可以这样做:
widths=(3 10 3)
while IFS= read -ru3 line; do
csv=()
for width in $widths; do
field=${(mr[width])line}
line=${line#$field}
csv+=("${(M)field##*[^[:space:]]}")
done
print -r -- ${(j[,])csv}
done 3< your-file
在r[width]
r
ight-pad 并将文本截断为给定宽度的情况下,m
这是根据显示宽度而不是字符数来完成的,并扩展到与模式相关${(M)field##*[^[:space:]]}
的前导部分,这就是直到最后一个非-whitespace (与不需要相同)。$field
M
${field%%[[:space:]]#}
set -o extendedglob
这可能会比 慢很多perl
。
如果您的文件仅包含 ASCII 文本(如示例中所示),则它们应该都是等效的。然后删除-C
for或将perl
区域设置设置为C
/ POSIX
for sed
//可能会提高性能。gawk
perl
在 UTF-8 语言环境中,输入重复 100000 次,这里我得到 1.1 秒perl
(变体为 0.34 A
,变体为 1.7 \X
),Paul 的为 1.3 秒gawk
,zsh 为 31 秒,GNU sed 's/./&,/13;s/./&,/3;s/[[:space:]]*,/,/g;s/[[:space:]]*$//'
(标准)为 2.1 秒,1.1 为sed -E 's/^(.{3})(.{10})/\1,\2,/;s/\s+,/,/g;s/\s+$//'
(非标准)。
在 C 语言环境中,分别变为 0.9 (0.27, 1.2)、0.7、31、1.3、0.5。
这些假设字段不包含,
或"
字符。某些 CSV 格式还需要引用带有前导或尾随空格的字段。
要创建正确的 CSV 输出,最简单的方法是使用Text::CSV
以下模块perl
:
<your-file perl -C -MText::CSV -lne '
BEGIN{$csv = Text::CSV->new({binary => 1})}
$csv->print(*STDOUT, [unpack "A3A10A3"])'
默认情况下,
- 分隔符是
,
- 引号是
"..."
"
""
在引号内转义- 仅引用需要引用的字段
但这些可以在new()
调用中进行调整。perldoc Text::CSV
详情请参阅。
1 尽管对于 TAB 特别而言,您可以对输入进行预处理,以expand
将这些 TAB 转换为空格序列;对于其他人来说,这个概念宽度通常很难应用,并且取决于文本发送到的显示设备。
答案2
$ cat txx
9 a bcd 55 # <- 1 trailing space here
48 z 7 # <- 2 trailing spaces here
1 xx xx xx 489
aaabbb bb bccchh
$ awk 'BEGIN { FIELDWIDTHS="3 10 3"; OFS=","; }
{ for (f = 1; f <= NF; ++f) sub (/[[:space:]]*$/, "", $f); print; }' txx
9,a bcd,55
48,z,7
1,xx xx xx,489
aaa,bbb bb b,ccc
答案3
您可以使用字符串操作和正则表达式来匹配条目末尾的一个或多个空格。然而,这意味着循环所有字段。
为此,需要首先触发一条记录根据FIELDWIDTHS
via例如进行分割$1=$1
awk 'BEGIN { FIELDWIDTHS="3 10 3" ; OFS=","}
{$1=$1 ;
for (i=1;i<NF;i++) {$i=gensub(/ +$/,"","g",$i)}}
1' infile
内菲莱
9 abc d 55
48 z 7
1 x x x xx489
输出
9,abc d,55
48,z,7
1,x x x xx,489
答案4
- 我需要一个有效的解决方案(例如不是 gawk ... | sed ...),因为我有很多大文件要处理。
请注意,对于有效的解决方案,实际上通常更好想管道。
事实上,虽然管道中的 2 个以上命令确实存在其设置的固有成本,加上流经管道的数据的持续开销,但我们还应该记住,在实践中,对于多线程多核 CPU,管道的分支很可能1彼此同时运行。因此,从性能角度来看,实际上最好将操作尽可能(合理)地拆分到管道中。
因此,虽然不像 Stephane Chazelas 的perl
解决方案那么简洁,但它很紧凑(而且对于您来说也相当快)原来的更简单的要求),为了提高效率,您还可以考虑以下内容:
(针对您原来的要求)
<input-data gawk '{$1=$1; print}' FIELDWIDTHS="3 10 3" OFS=, | gsed 's/ *,/,/g' | gsed 's/ \+$//'
(对于双引号字段和转义嵌入双引号的额外要求)
<input-data gawk '/"/{for(i=1;i<=NF;i++) gsub(/"/, "\"\"", $i)} {$1=$1; print}' FIELDWIDTHS="3 10 3" OFS='","' | gsed 's/ *","/","/g' | gsed 's/ *$/"/;s/^/"/'
在这里,我将任务分为 3 个操作块,每个操作块对应管道的每个分支,大概每个操作块都会在单独的 CPU 线程/核心上运行。
在我的四核机器上,第一个管道(您的原始要求)始终需要 0.21 秒(重定向到/dev/null
)来咀嚼 100000 倍的输入样本(即 300k 行),同样需要 20 秒消化 10M 倍的样本(30M 行)。作为比较,Stephaneperl -C -lnse 'print unpack "A3A10A3"' -- -,=,
在同一台机器上分别需要 0.31 秒和 31 秒,即在多字节支持的所有条件相同的情况下大约慢 50%,而非perl
版本-C
(不支持多字节字符)始终需要 0.23 秒和 23 秒,即仍然比上面第一个管道慢 10%。同样,gawk
仅sed
//ruby
的解决方案速度慢 100% 到 200%以他们最快的速度(即不支持多字节字符)配置。
对于您更新的要求来说更真实:上面的第二个管道分别需要 0.26 秒和 25 秒,而 Text perl
::CSV(由 Text::CSV_XS 加速)解决方案分别需要 0.76 秒和 1 分 17 秒。
您可能会注意到,在上面的示例中,使用 GNU 工具(BSD 工具可能是不同的情况),我什至不需要设置C
区域设置即可获得额外的速度。相反,我确实利用了你需要修剪的事实空间仅,而不是其他类型的空白字符,因此使用了简单的s/ */
.显然,不设置C
区域设置可以正确支持数据中可能的多字节字符。当使用 a s/[[:space:]]*/
(或类似的,如s/\s*/
) 代替 时s/ */
,除非设置区域设置,否则性能会下降一倍以上C
,因此如果您需要捕获其他类似空格的字符,同时还为输入数据保留多字节(例如 UTF8 编码)字符集,您最好在 a 中显式指定所有相关的类似空格的字符[]
,这样可以保持出色的性能。
也许可以进一步压缩单一工具解决方案以获得额外的速度(尤其是 Text perl
::CSV 解决方案当然可以优化),但是,除非它们能够以本机或编程方式满足多核支持,否则管道是值得考虑“快捷方便”性能优势,至少什么时候:
- 您可以从整个任务中识别操作块,您还可以为每个操作块选择更专门的工具
- 您可以将它们分成尽可能多的管道臂,同时最多不超过可用CPU核心/线程的数量
作为第 1 点的示例,我一直将您的用于gawk
管道的第一个臂,因为它比sed
完成工作的特定部分要快得多,而用于gawk
管道的第二个臂时,性能会下降同样的30M行样本数据需要20秒到35秒。另外,在满足您更新需求的管道中,我没有将其分成gawk
两个子操作,因为保持完整是我能想到的获取固定宽度字段的最简单、最精简的方法尽管转义输入数据中可能存在的双引号。
作为第 2 点的示例,请注意最后一个sed
执行两个s
命令的命令:我没有进一步拆分它,以免与我的计算机上的可用 CPU 核心数量相等。进一步拆分它的性能是一样的,没有额外的改进。
但人们一致认为,输入数据需要的要求越多,管道就会变得越来越复杂/尴尬/容易出错,可能比单一工具解决方案更复杂。
1. 有一些命令可确保每个管道的臂由单独的 CPU 核心运行,但这通常是不必要的,因为现代内核上会自动发生核心重新分配