有没有办法用 sed 进行多次替换,而不用链接替换?

hug*_*omg 19 sed text-processing

我想编写一个 shell 脚本,用 BB 替换 A,用 AA 替换 B。例如,AYB变成BBYAA。对于这种事情,我通常的解决方案是将多个调用链接到 sed,如下所示:

sed 's/A/BB/g;s/B/AA/g'
Run Code Online (Sandbox Code Playgroud)

但是,这在这种情况下不起作用,因为A最终被翻译成AAAA而不是BB. tr似乎也不是一种选择,因为替换文本长于一个字符。还有什么我可以做的吗?如果它使用 sed 或 tr 以外的其他东西也没关系。

Kus*_*nda 14

您无法使用 in 中的单个替换来完成整个操作sed,但是您可以根据两个子字符串AB是单个字符还是更长的字符串,以不同的方式正确地完成整个操作。

假设两个子AB是单字符...

你想转变AYBBBYAA. 去做这个,

  1. 每一个变化A,以BBA使用y/AB/BA/
  2. AAAusing替换新字符串中的每一个s/A/AA/g
  3. BBBusing替换新字符串中的每一个s/B/BB/g
$ echo AYB | sed 'y/AB/BA/; s/B/BB/g; s/A/AA/g'
BBYAA
Run Code Online (Sandbox Code Playgroud)

结合最后两个步骤得到

$ echo AYB | sed 'y/AB/BA/; s/[AB]/&&/g'
BBYAA
Run Code Online (Sandbox Code Playgroud)

事实上,这里的操作顺序并不重要:

$ echo AYB | sed 's/[AB]/&&/g; y/AB/BA/'
BBYAA
Run Code Online (Sandbox Code Playgroud)

sed编辑命令y///翻译中的字符在它的第一个参数在其第二个参数对应的字符,有点像tr实用程序执行。这是在一个操作中完成的,所以您不需要使用临时为交换ABy/AB/BA/。在一般情况下,y///比什么如转换单个字符更快s///g的(因为没有正则表达式的时候),并且它也能插入新行用绳子\n,它的标准s///命令不能做(s///在GNUsed能明显这样做,因为非便携式便利扩展)。

&在的替换部分的字符s///的命令将通过在任何相匹配的第一个参数的表达式来代替,所以s/[AB]/&&/g将增加一倍的任何AB输入数据中的字符。


对于多字符子,假设子串是不同的(即,一个子未在其它发现,如在的情况下oofoo),使用像

$ echo fooxbar | sed 's/foo/@/g; s/bar/foofoo/g; s/@/barbar/g'
barbarxfoofoo
Run Code Online (Sandbox Code Playgroud)

即,通过在数据中找不到的中间字符串交换两个字符串。请注意,中间字符串可以是数据中未找到的任何字符串,而不仅仅是单个字符。


gle*_*man 13

这是需要循环的问题,以便您可以同时搜索两种模式。

awk '
    BEGIN {
        regex = "A|B"
        map["A"] = "BB"
        map["B"] = "AA"
    }
    {
        str = $0
        result = ""
        while (match(str, regex)) {
            found = substr(str, RSTART, RLENGTH)
            result = result substr(str, 1, RSTART-1) map[found]
            str = substr(str, RSTART+RLENGTH)
        }
        print result str
    }
'
Run Code Online (Sandbox Code Playgroud)

当然,如果 Perl 可用,则有一个等效的 oneliner:

perl -pe '
    BEGIN { %map = ("A" => "BB", "B" => "AA"); }
    s/(A|B)/$map{$1}/g;
'
Run Code Online (Sandbox Code Playgroud)

如果没有任何模式包含特殊字符,您还可以动态构建正则表达式:

perl -pe '
    BEGIN {
        %map = ("A" => "BB", "B" => "AA");
        $regex = join "|", keys %map;
    }
    s/($regex)/$map{$1}/g;
'
Run Code Online (Sandbox Code Playgroud)

顺便说一句,Tcl 有一个内置命令,用于调用string map,但编写 Tcl oneliners 并不容易。


演示按长度对键进行排序的效果:

  1. 没有排序

    $ echo ABBA | perl -pe '
        BEGIN {
            %map = (A => "X", BB => "Y", AB => "Z");
            $regex = join "|", map {quotemeta} keys %map;
            print $regex, "\n";
        }
        s/($regex)/$map{$1}/g
    '
    
    Run Code Online (Sandbox Code Playgroud)
    A|AB|BB
    XYX
    
    Run Code Online (Sandbox Code Playgroud)
  2. 带排序

    $ echo ABBA | perl -pe '
          BEGIN {
              %map = (A => "X", BB => "Y", AB => "Z");
              $regex = join "|", map {quotemeta $_->[1]}
                                 reverse sort {$a->[0] <=> $b->[0]}
                                 map {[length, $_]}
                                 keys %map;
              print $regex, "\n";
          }
          s/($regex)/$map{$1}/g
      '
    
    Run Code Online (Sandbox Code Playgroud)
    BB|AB|A
    ZBX
    
    Run Code Online (Sandbox Code Playgroud)

在 perl 中对“普通”排序与 Schwartzian 进行基准测试:子程序中的代码直接从文档中提取sort

#!perl
use Benchmark   qw/ timethese cmpthese /;

# make up some key=value data
my $key='a';
for $x (1..10000) {
    push @unsorted,   $key++ . "=" . int(rand(32767));
}

# plain sorting: first by value then by key
sub nonSchwartzian {
    my @sorted = 
        sort { ($b =~ /=(\d+)/)[0] <=> ($a =~ /=(\d+)/)[0] || uc($a) cmp uc($b) } 
        @unsorted
}

# using the Schwartzian transform
sub schwartzian {
    my @sorted =
        map  { $_->[0] }
        sort { $b->[1] <=> $a->[1] || $a->[2] cmp $b->[2] }
        map  { [$_, /=(\d+)/, uc($_)] } 
        @unsorted
}

# ensure the subs sort the same way
die "different" unless join(",", nonSchwartzian()) eq join(",", schwartzian());

# benchmark
cmpthese(
    timethese(-10, {
        nonSchwartzian => 'nonSchwartzian()',
        schwartzian    => 'schwartzian()',
    })
);
Run Code Online (Sandbox Code Playgroud)

运行它:

$ perl benchmark.pl
Benchmark: running nonSchwartzian, schwartzian for at least 10 CPU seconds...
nonSchwartzian: 11 wallclock secs (10.43 usr +  0.05 sys = 10.48 CPU) @  9.73/s (n=102)
schwartzian: 11 wallclock secs (10.13 usr +  0.03 sys = 10.16 CPU) @ 49.11/s (n=499)
                 Rate nonSchwartzian    schwartzian
nonSchwartzian 9.73/s             --           -80%
schwartzian    49.1/s           405%             --
Run Code Online (Sandbox Code Playgroud)

使用 Schwartzian 变换的代码要快 4 倍。

其中比较函数 length针对每个元素:

Benchmark: running nonSchwartzian, schwartzian for at least 10 CPU seconds...
nonSchwartzian: 11 wallclock secs (10.06 usr +  0.03 sys = 10.09 CPU) @ 542.52/s (n=5474)
schwartzian: 10 wallclock secs (10.21 usr +  0.02 sys = 10.23 CPU) @ 191.50/s (n=1959)
                Rate    schwartzian nonSchwartzian
schwartzian    191/s             --           -65%
nonSchwartzian 543/s           183%             --
Run Code Online (Sandbox Code Playgroud)

Schwartzian 使用这种廉价的排序功能要慢得多。

我们现在可以摆脱辱骂性评论吗?

  • @ user414777,我回滚了您的编辑。我在这里使用的技术是 [Schwartzian 变换](https://en.wikipedia.org/wiki/Schwartzian_transform),它更有效,缓存字符串长度,而不必在排序算法中为每次迭代执行它。 (2认同)

Qua*_*odo 9

使用awk,您可以使用pattern1作为字段分隔符FS,将replacement1用作输出字段分隔符OFS。然后,遍历每个字段并替换模式2replacement2

awk '{for (f=1;f<=NF;f++){gsub(p,r,$f)} $1=$1}1' FS=A OFS=BB p=B r=AA file
Run Code Online (Sandbox Code Playgroud)

重点$1=$1是强制重建记录,否则它将失败0A例如。

这是符合 POSIX 标准的并且不涉及中间字符串,因此它是万无一失的。


sch*_*ity 6

我想一个解决方案是先将 A 或 B 替换为字符串中不存在的另一个字符,然后替换该字符。这样就避免了 A 和 B 之间的切换。虽然需要一串sed's :

$ echo AYB | sed -e 's/A/#/g' -e 's/B/AA/g' -e 's/#/BB/g'
BBYAA
Run Code Online (Sandbox Code Playgroud)

  • 您的临时角色不需要是可打印的,我喜欢在这些情况下使用 CTRL-G。这有一个额外的好处,那就是如果出现问题并且你“cat”一个仍然有替换字符的文件。 (3认同)

gue*_*t_7 6

您可以GNU sed通过将 all 更改As为 record separator来做到这一点\n,它肯定不会出现。

echo AYB |
sed -e '
  y/A/\n/
  s/[\nB]/&&/g
  y/\nB/BA/
'
BBYAA
Run Code Online (Sandbox Code Playgroud)


Mak*_*yen 6

您可以使用以下方法通过一系列直接替换为任意输入文本完成此操作sed

sed 's/Q/Qz/g; s/A/Qa/g; s/B/AA/g; s/Qa/BB/g; s/Qz/Q/g;'
Run Code Online (Sandbox Code Playgroud)

打开一个令牌空间来保存或表示中间/任意值

对于此类问题,有一个通用解决方案,适用于您唯一可以做的就是直接替换的情况,并且适用于任意输入文本。

诀窍是您首先在文本的中间副本中创建一个标记空间(即变量空间),该空间可用于表示任意值,以便您在以后的替换中使用的标记不能存在于文本的中间副本中。在您进行进一步替换时输入文本。例如:

s/Q/Qz/g
Run Code Online (Sandbox Code Playgroud)

这使得文本不能再包含任何Q后跟除,之外的任何内容z,并且每个Qz实际上都代表 a Q。这意味着您可以自由使用Q后跟任何字符,而不是z代表您想要的任何字符。

在这种特定情况下,您也不能使用QAand QB,因为您要替换单个AB字符。

因此,要生成您想要的整体替换,您将执行以下替换序列:

s/Q/Qz/g  # Open your Q* token-space.
          # You can now use any Q* other than Qz, QA, and QB to represent another value.
          # The restriction to not use QA and QB is only because this specific case requires
          # substituting for the single A and B characters.
s/A/Qa/g  # Temporarily represent all A as Qa.
s/B/AA/g  # Change all B to AA, as desired in the question.
s/Qa/BB/g # Change all Qa placeholders with BB.
s/Qz/Q/g  # Restore all Q, closing the token-space.
Run Code Online (Sandbox Code Playgroud)

所以,总的来说,这可以写成:

sed 's/Q/Qz/g; s/A/Qa/g; s/B/AA/g; s/Qa/BB/g; s/Qz/Q/g;'
Run Code Online (Sandbox Code Playgroud)

这会产生输出:

 $ echo AYB | sed 's/Q/Qz/g; s/A/Qa/g; s/B/AA/g; s/Qa/BB/g; s/Qz/Q/g;'
 BBYAA
Run Code Online (Sandbox Code Playgroud)

发生了什么:

代换 缓冲区中的文本 评论
s/Q/Qz/g AYB 打开您的Q*令牌空间。这对这个示例输入没有任何作用,但确保这将适用于任意输入文本。
s/A/Qa/g QaYB 暂时将所有表示AQa
s/B/AA/g QaYAA 所有更改BAA,可以根据需要在问题。
s/Qa/BB/g BBYAA 用 更改所有Qa占位符BB
s/Qz/Q/g BBYAA 恢复所有Q,关闭令牌空间。同样,对这个示例输入文本没有任何作用,但需要处理任意输入文本。

您可以使用任何字符作为标记空间中的第一个字符

对于标记空间的第一个字符,您可以使用任何单个字符。我一般用Q,因为它在文本中出现频率低,是ASCII字符集中的一个字母,好记。Z也适合。如果您使用更宽的字符集,则选择使用频率更低的字符(可能是符号)是有利的。出于性能原因,理想的情况是选择一个在您将使用的文本中不存在的字符,这样第一个和最后一个替换实际上不会做任何事情。然而,这只是性能和空间问题,而不是功能问题。换句话说,它可以与您选择的任何角色一起工作,但如果它需要做的工作更少,它会更快。

顾虑

  1. 任何可能特别依赖于您用作标记空间开头的字符的模式都必须说明它由您选择的标记表示。在上述情况下,如果您想用Q字符进行其他一些替换,那么您需要在打开标记空间之前或关闭标记空间之前Q将中间文本中表示的所有内容都考虑在内,Qz或者进行这些替换。通常,您只需选择一个不同的字符作为标记空间的起始字符即可解决此问题。
  2. 这既不是 CPU 效率最高的方法,也不是实现此目的的空间效率最高的方法。真正的编程语言可以使用各种其他结构更有效地完成此类任务。然而,这是一种很好的技巧,可以在进行替换时放在你的后兜里,这是你可以使用的工具。