当被数值包围而不是被字母字符包围时替换空格

Replace spaces when surrounded by numeric values, but not by alpha-character

在一个只包含字母数字字符的固定宽度文件中,我想替换字母字符和数字字段(包括带符号的十进制,但不包括科学记数法)之间的白色 spaces 以及数字和数字字段,同时在字母字符值之间留下白色 spaces。

我知道使用 awkFIELDWIDTHS 选项,但我拥有的文件类型有太多字段和太多独特的结构,无法概括。

这是一个玩具示例:

708 447 4797 JOHN SMITH 18000 

需要格式化如下:

708|447|4797|JOHN SMITH|18000 

正在寻找使用 sedperlawk 等的任何便携式解决方案

编辑:

为了澄清问题并进行概括以获得更好的整体可用性,这里有更多行来测试解决方案。请继续假设任何具有 space 的字母字符确实应该放在一起(即假设没有出现 Bob Jones Chuck Smith)。

708 447 4797 JOHN SMITH 18000
708 447 4797 JOHN SMITH    18000
708  447  4797  JOHN SMITH  18000
708 -3.00 4797 JOHN SMITH 18000

应该导致:

708|447|4797|JOHN SMITH|18000
708|447|4797|JOHN SMITH|18000
708|447|4797|JOHN SMITH|18000
708|-3.00|4797|JOHN SMITH|18000

使用sed:

sed -r 's/([^[:alpha:]]) +| +([^[:alpha:]])/|/g' file
708|447|4797|JOHN SMITH|18000
708|447|4797|JOHN SMITH|18000
708|447|4797|JOHN SMITH|18000
708|-3.00|4797|JOHN SMITH|18000

编辑: 使用 gnu-awk

awk -v OFS='|' 'BEGIN { 
  FPAT="[^[:alpha:] ]+[[:alpha:]]+( +[[:alpha:]]+)*"
} {=} 1' file
708|447|4797|JOHN SMITH|18000
708|447|4797|JOHN SMITH|18000
708|447|4797|JOHN SMITH|18000
708|-3.00|4797|JOHN SMITH|18000

通过 Perl 的一些其他方式,

$ echo '708 447 4797 JOHN SMITH 18000' | perl -pe 's/(?<=[A-Za-z])\h+(?=[A-Za-z])(*SKIP)(*F)|\h/|/g' 
708|447|4797|JOHN SMITH|18000

$ echo '708 447 4797 JOHN SMITH 18000' | perl -pe 's/(?<![A-Za-z])\h+|\h+(?![A-Za-z])/|/g' 
708|447|4797|JOHN SMITH|18000

这就是所有必要的

use strict;
use warnings;
use 5.010;

my $s = '708 447 4797 JOHN SMITH 18000';
$s =~ s/ (?<=\d) \h+ | \h+ (?=\d) /|/axg;
say $s;

输出

708|447|4797|JOHN SMITH|18000

使用这个正则表达式:

(?<=\d)[[:blank:]]+(?!$)|[[:blank:]]+(?=\d)

DEMO

Perl 演示:

$ cat /tmp/nums.txt
708 447 4797 JOHN SMITH 18000
708 447 4797 JOHN SMITH    18000
708  447  4797  JOHN SMITH  18000
708 -3.00 4797 JOHN SMITH 18000

$ perl -pe 's/(?<=\d)[[:blank:]]+(?!$)|[[:blank:]]+(?=\d)/|/g' /tmp/nums.txt
708|447|4797|JOHN SMITH|18000
708|447|4797|JOHN SMITH|18000
708|447|4797|JOHN SMITH|18000
708|-3.00|4797|JOHN SMITH|18000

虽然我喜欢 anubhava 的 sed 解决方案,但对我来说更明显的是将所有空格转换为新的分隔符,然后确定需要切换回来的内容。以下内容从您的示例数据中生成您想要的输出,并且还适应了 Ed Morton 关于处理附近 alpha 字段的关注:

sed -r 's/ +/|/g; s/([[:alpha:]])\|([[:alpha:]])/ /g'

它的优点是更短且更易于阅读。 (好吧,没那么容易,毕竟还是sed。)

一个可能的问题是这不会保留 文本字段内的空白。也就是说,JOHN SMITH 将转换为 JOHN SMITH

避免这种情况的方法是:

sed -r 's/([[:digit:]]) +/|/g; s/ +([[:digit:]])/|/g'

我认为这几乎等同于 anubhava 的解决方案,除了它符合您围绕数字内容而不是围绕非字母内容分隔字段的要求。

您认为这种事情在 awk 中也很容易,但事实证明,awk 的 sub()gsub() 不支持反向引用。但是,如果您碰巧使用 gawkgensub() 函数可能会起作用:

gawk '{gsub(/ +/,"|"); print gensub(/([[:alpha:]])\|([[:alpha:]])/, "\1 \2", "g", [=12=]);}

gawk '{print gensub(/([[:digit:]]) +/,"\1|","g",gensub(/ +([[:digit:]])/,"|\1","g",[=13=]));}'

这就是我想到的,这是公认的快速懒惰的尝试:

perl -pe 's/(\d)\h+|\h+(\d)/|/g' <<< "123 49 5440 G.  Cito 1967 23456" 
123|49|5440|G.  Cito|1967|23456

我是这样读的:"Replace a digit followed by more than one horizontal space OR more than one horizontal space followed by a digit; with the original digit and |"。它会在字符串的字母部分保留多个 space,但如果在这种情况下 123 之前有 space,则会将“|”放在开头。

注意:此回复中上面的 quick/easy 方法存在问题 - 请参阅 Borodin 对我关于 his/her 解决方案的问题的回复。解决方法是使用(如 Borodin 所指出的)(?<=) (?=) zero-width look around,它允许 (\d) 中的表达式作为 "boundary" 而不被包含在比赛中,不需要 </code>、<code></code> 和 <code>,只有水平 space 被替换为 |

perl -pe 's/(?<=\d)\h+|\h+(?=\d)/|/g' <<<"9 AAA 9 AAA 54 G. Cito 1967 123"
9|AAA|9|AAA|54|G. Cito|1967|123

谢谢@Borodin!