替换数字值之间的空格,但不替换字母字符之间的空格

3
在一个仅包含字母数字字符的固定宽度文件中,我想要替换字母和数字字段之间(包括有符号小数,但不包括科学计数法)以及数字和数字字段之间的空格,同时保留字母字符值之间的空格。
我知道可以使用awk的FIELDWIDTHS选项,但我所拥有的文件类型具有太多具有太多独特结构的字段,无法进行概括。
以下是一个玩具示例:
708 447 4797 JOHN SMITH 18000 

需要按照以下格式进行格式化:
708|447|4797|JOHN SMITH|18000 

寻找使用sed、perl、awk等任何便携式解决方案。
编辑:
为了澄清问题并提高整体可用性,这里有更多的行来测试解决方案。请继续假设任何带空格的字母字符确实意味着要保持在一起(即假设没有出现“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

1
如果每个字段的宽度都是固定的,最安全的方法是读取该行,根据已知的宽度分割字段,然后使用新的分隔符将字段重新写出。 - Andy Lester
@AndyLester 正如问题所指定的那样,我不幸不能依赖文件具有相同的 fwf 结构,并且字段数量阻止了对每个字段进行有效的重建。 - mlegge
1
如果你有 123 Bob Jones Chuck Smith 456,那么Bob和Chuck是不同的人吗? - dawg
当您在上面提到“字符”时,是指“字母”还是其他内容(例如,是否包括像“。”,“$”,“ [”等字符)?当您说“数字”时,是指“整数”还是其他内容(例如,是否包括像“-8”,“0.5”和“3e7”这样的数字)?您提供的那一行确实是一个非常无望的样本输入集来进行测试。 - Ed Morton
6个回答

3

使用 sed

sed -r 's/([^[:alpha:]]) +| +([^[:alpha:]])/\1|\2/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=$1} 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

1
同样的,在Perl中 perl -pe 's/[^a-z]\K +| +(?=[^a-z])/|/gi' - hwnd
1
这种方法通常无法实现OP所说的想要“替换字符和数字字段之间以及数字和数字字段之间的空格,同时保留字符值之间的空格”的操作。它只能从给定的样本输入中产生预期的输出,但在其他输入情况下会失败(例如,将“4797”替换为“BILL”)。OP应该更好地考虑他们的输入可能是什么,而不仅仅提供一个只有1个可能情况的单行输入。 - Ed Morton
1
可能更好的方式是使用 sed -r 's/([[:alpha:]]) +([[:digit:]])|([[:digit:]]) +(.)/\1\3|\2\4/g',但在看到更多输入样本后再进行确认。 - anubhava
1
@EdMorton:我在这里也添加了一个awk答案。 OP还编辑了问题以提供更多的示例数据。 - anubhava
1
我有一个非常类似的gawk解决方案:FPAT='[^[:alpha:][:space:]]+|[[:alpha:]]+[[:space:]]*[[:alpha:]]*',但是你的更好,因为我假设字母字段内只有一个空格。 - Ed Morton
显示剩余5条评论

2

这就是必要的全部内容。

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) ... (?=\d) 这样使用 "零宽度正向先行断言" 通常会使匹配更可靠吗?还是这只是为了提高可读性和文档说明而做出的选择? - G. Cito
1
@G.Cito:它类似于\d ... \d,不同的是它不是匹配字符串的一部分。在这种替换的情况下,如果不使用后向和前向查找,你需要捕获数字并在替换字符串中使用 $1$2 进行替换。但是,我们无法简单地确定哪个备选方案匹配了,因此我们无法知道替换文本应该是 $1| 还是 |$2,除非使用可执行替换。此外,如果字符串是 AAA 9 AAA,那么 (\d) \h+ | \h+ (\d) 将首先匹配空格后面的 9,但第二个空格将找不到。 - Borodin
非常好的解释。为了使正则表达式强大而应对讨厌和不断变幻莫测的文本,通常需要比一开始看起来更多的思考。它们看似简单,但却具有强大的功能:就像 Jamie Zawinsky 所说的那样。;-) - G. Cito

2
使用这个正则表达式:
(?<=\d)[[:blank:]]+(?!$)|[[:blank:]]+(?=\d)

示例

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

1
我喜欢anubhava的sed解决方案,但是对我来说,将所有空格转换为新分隔符,然后识别需要切换回来的内容似乎更明显。以下代码可从您的示例数据中生成所需输出,并适应Ed Morton关于处理附近字母字段的问题。
sed -r 's/ +/|/g; s/([[:alpha:]])\|([[:alpha:]])/\1 \2/g'

它的优点是更短且更易于阅读。(好吧,并不是非常容易。毕竟,这还是sed。)

可能存在一个问题,就是这种方法不能保留文本字段内部的空格。也就是说,“JOHN SMITH”将被转换为“JOHN SMITH”。

避免这种情况的一种方法如下:

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

我认为这与anubhava的解决方案几乎相同,只是它符合您将数字内容分隔开而不是非字母内容的要求。

你可能会认为在awk中做这种事情很容易,但事实证明,awk的sub()gsub()不支持反向引用。 但是,如果您恰好使用gawk,则gensub()函数可能适用:

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

或者

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

1

一些通过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

我从版本v5.8.4 built for sun4-solaris-64int中得到了错误信息Quantifier follows nothing in regex; marked by <-- HERE in m/(?<=[A-Za-z])h+(?=[A-Za-z])(* <-- HERE SKIP)(*F)|h/ at -e line 1.。其他版本也会有不同的错误信息。 - Ed Morton

0

这是我草率地尝试的结果:

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

我这样理解它:“用原始数字和|替换一个数字后面跟着多个水平空格或多个水平空格后面跟着一个数字;”。 它将保留字符串的字母部分中的多个空格,但如果在此情况下的123之前有空格,则会在开头放置“|”。

NB:上述快速/简单方法存在问题-请参见Borodin对我的问题的回答。 修复方法是使用(如Borodin所指出的)(?<=) (?=)零宽度环视,它允许内部表达式(\d)作为“边界”工作,并且不包含在匹配中,因此不需要$1$2\1\2,只需用|替换水平空格即可。

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!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接