【问题标题】:Replace spaces when surrounded by numeric values, but not by alpha-character当被数值包围时替换空格,但不被字母字符包围
【发布时间】:2015-02-09 17:14:01
【问题描述】:

在仅包含字母数字字符的固定宽度文件中,我想替换字母字符和数字字段(包括有符号十进制,但不包括科学计数法)以及数字和数字字段之间的空格,同时保留字母字符值之间的空格。

我知道使用awkFIELDWIDTHS 选项,但是我拥有的文件类型太多,具有太多独特的结构,无法一概而论。

这是一个玩具示例:

708 447 4797 JOHN SMITH 18000 

需要格式化如下:

708|447|4797|JOHN SMITH|18000 

使用sedperlawk 等寻找任何便携式解决方案。

编辑:

为了澄清问题并概括出更好的整体可用性,这里有更多行来测试解决方案。请继续假设任何有空格的字母字符确实应该放在一起(即假设没有出现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

【问题讨论】:

  • 如果所有内容都是固定宽度,最安全的方法是读取该行,根据您知道的宽度将字段拆分,然后用新的分隔符将字段重写。
  • @AndyLester 正如问题中所述,很遗憾,我不能依赖文件具有相同的 fwf 结构,并且字段的数量阻碍了每个文件的有效重建。
  • 如果你有123 Bob Jones Chuck Smith 456怎么办? Bob 和 Chuck 是不同的人吗?
  • 当您在上面说“字符”时,您的意思是“字母”还是其他内容(例如,它是否包含 .$[ 等字符)?当您说“数字”时,您的意思是“整数”还是其他东西(例如,它是否包括像 -80.53e7 这样的数字)?您提供的那一行确实是一个非常无望的样本输入集来进行测试。

标签: regex perl awk sed


【解决方案1】:

使用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

【讨论】:

  • 相同,在 Perl perl -pe 's/[^a-z]\K +| +(?=[^a-z])/|/gi'
  • 这实际上并不能做 OP 所说的他们想要的 replace the white spaces between character and numeric fields as well as numeric and numeric fields while leaving the white spaces between character values。它只是从给定的样本输入中产生预期的输出,但会因其他输入而失败(例如,将“4797”替换为“BILL”)。 OP 应该更好地考虑他们的输入可能是什么,而不是只提供 1 行,其中只有 1 个可能的情况。
  • 可能:sed -r 's/([[:alpha:]]) +([[:digit:]])|([[:digit:]]) +(.)/\1\3|\2\4/g' 会更好,但我可以在看到更多输入样本后检查。
  • @EdMorton:我还在这里添加了awk 答案。 OP 还编辑了问题以提供更多示例数据。
  • 是的,我想到了一个非常相似的 gawk 解决方案:FPAT='[^[:alpha:][:space:]]+|[[:alpha:]]+[[:space:]]*[[:alpha:]]*' 但你的更好,因为我假设字母字段中有一个空格。
【解决方案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

【讨论】:

  • (?&lt;=\d) ... (?=\d) 是否使这种匹配更可靠? (通常使用“零宽度正向后视”)还是为可读性文档做出的选择?
  • @G.Cito:它类似于\d ... \d,只是它不是匹配字符串的一部分。在替换的情况下,如这里,不使用后视和前瞻,您必须捕获数字并使用替换字符串中的$1$2 替换它。但是没有简单的方法来判断哪些替换匹配,因此如果不使用可执行替换,我们无法知道替换文本应该是 $1| 还是 |$2。此外,如果字符串是 AAA 9 AAA,则 ` (\d) \h+ | \h+ (\d)` 将首先匹配9 后面的空格,但不会找到第二个空格。
  • 很好的解释。为了使正则表达式健壮vis-a-vis讨厌且不断不可预测的文本,通常需要比最初看起来需要更多的思考。它们看似简单但功能强大:towhit [Jamie Zawinsky] (en.wikiquote.org/wiki/Jamie_Zawinski)。 ;-)
【解决方案3】:

使用这个正则表达式:

(?<=\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

【讨论】:

    【解决方案4】:

    通过 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 &lt;-- HERE in m/(?&lt;=[A-Za-z])h+(?=[A-Za-z])(* &lt;-- HERE SKIP)(*F)|h/ at -e line 1.。其他版本的错误消息也不同。
    【解决方案5】:

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

    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() 不支持反向引用。但是,如果您碰巧使用gawkgensub() 函数可能会起作用:

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

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

    【讨论】:

      【解决方案6】:

      这就是我突然想到的一个公认的快速懒惰刺伤它:

      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 之前有空格,则会将“|”放在开头。

      注意:本回复中上述快速/简单的方法存在问题 - 请参阅 Borodin 对我关于他/她的解决方案的问题的回复。解决方法是使用(如 Borodin 注释)(?&lt;=)(?=)zero-width look around,它允许 (\d) 内部的表达式作为“边界”工作,而不是包含在匹配中,因此 $1、@987654329不需要@、\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!

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-10-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-12-02
        • 2012-07-12
        • 2015-07-31
        • 2020-10-08
        相关资源
        最近更新 更多