如何替换文件中多行中与不同文件中的模式匹配的多个子字符串?

如何替换文件中多行中与不同文件中的模式匹配的多个子字符串?

我有一个包含多个 IP 地址和主机名的文件,另一个文件包含一些每行有多个 IP 地址的文件夹。

ip_主机名.txt

host1 10.1.1.1
host2 10.2.2.2
host3 10.3.3.3
host100 10.50.50.50

路径_ips.txt

/path1/foo/bar 10.1.1.1 10.2.2.2 10.3.3.3
/path2/foo/bar 10.3.3.3 10.7.7.7
/path3/foo/bar 10.4.4.4 10.8.8.8 10.29.29.29 10.75.75.75
/path100/foo/bar 10.60.60.60

我想更换 IP 地址路径_ips.txt主机名来自的文件ip_主机名.txt匹配每个 IP 地址的文件。

所需输出路径_ips.txt

/path1/foo/bar host1 host2 host3
/path2/foo/bar host3 host7
/path3/foo/bar host4 host8 host29 host75
/path100/foo/bar host60

我尝试在嵌套中使用 sed 来完成此操作阅读时循环如下:

#!/bin/sh

while read -r line
do
IP=$(echo $line| awk '{print $1}')
HN=$(echo $line| awk '{print $2}')

        while read -r line2
        do
               sed -i "s/$IP/$HN/g" path_ips.txt
        echo $line2 #to see the progress
        done < path_ips.txt

done < ip_hostname.txt

当 IP 地址和主机名列表不是很大时,它第一次运行良好,但是当我尝试使用更大的列表时ip_主机名.txt文件,它的行为很奇怪,结果不符合预期。不用说,它需要很长时间才能完成。

有没有更好、更有效的方法来做到这一点?

答案1

您的脚本的问题在于您sed为每个匹配的 IP 地址运行单独的命令,这使得当文件很大时脚本非常慢。

您还有一个嵌套循环,因此您O(N*M)的算法具有时间复杂度。

更好的方法是使用awk进行替换,这样您就可以在一次传递中完成所有替换:

$ awk 'NR==FNR{h[$2]=$1;next}{for (i=2;i<=NF;i++) if ($i in h) $i = h[$i]}1' ip_hostname.txt path_ips.txt 
/path1/foo/bar host1 host2 host3
/path2/foo/bar host3 host7
/path3/foo/bar host4 host8 host29 host75
...
/path100/foo/bar host60

或者采用更易读的格式

awk '
    NR == FNR {
      h[$2] = $1
      next
    }
    {
      for (i=2; i<= NF; i++)
        if ($i in h)
          $i = h[$i]
    }
    1
' ip_hostname.txt path_ips.txt

这应该具有复杂性,O((N+M)lon(N))其中Nis the size of fileip_hostname.txtMis the size of file path_ips.txt。请注意,ip_hostname.txt应该能够适应内存才能正常工作,但在现代计算机上这不应该成为问题,除非它的大小为几 GB。

答案2

这可以完全在 sed 中完成,但 awk 答案通常更具可读性。

#file toggle
1{x;s:^$:<IPs>:;x}
/^EOF$/{x;s:<IPs>:<paths>:;x;d}

#store hostname file
x;/<IPs>/{x;H;d}

#process path file
x;s: :>&:;s:$: :;G
:loop
    s:>( [^ ]+)( .*<paths>.*)\n([^ ]+)\1: \3>\2\n\3\1:
tloop
s:> .*::p

如示例所示,代码假定空格作为文件分隔符。这意味着如果存在包含空格的路径,答案很可能是错误的。

这是使用 GNU sed 进行测试的,但您可能有不同的 sed 版本。如果这没有运行,请告诉我。

跑步:

sed -nrf SCRIPT_FILE ip_hostname.txt <(echo EOF) path_ips.txt > output.txt

注意:<(echo EOF)用于告诉我的脚本第一个输入文件何时结束。

答案3

使用任何 POSIX awk:

$ cat tst.awk
NR==FNR {
    map[$2] = $1
    next
}
match($0,/([[:space:]]+([0-9]{1,3}\.){3}[0-9]{1,3})+$/) {
    path = substr($0,1,RSTART-1)
    $0 = substr($0,RSTART,RLENGTH)
    for ( i=1; i<=NF; i++ ) {
        $i = ($i in map ? map[$i] : $i)
    }
    $0 = path OFS $0
}
{ print }

$ awk -f tst.awk ip_hostname.txt path_ips.txt
/path1/foo/bar host1 host2 host3
/path3/foo/bar 10.4.4.4 10.8.8.8 10.29.29.29 10.75.75.75
/path100/foo/bar 10.60.60.60

即使路径包含空格,这也将起作用,除非路径的文件名部分可以以空格结尾,后跟看起来像 IP 地址的字符串,例如,/path/foo/bar 1.1.1.1其中1.1.1.1不是 IP 地址,而是文件名的一部分bar 1.1.1.1。如果发生这种情况,那么您需要 ip_hostname.txt 使用其他格式来分隔路径和 IP 地址。

答案4

以下 Perl 脚本读取第一个输入文件 ( ip_hostname.txt),并构建一个名为 %IPs 的关联数组(哈希),其中键是 IP 地址,值是主机名。

作为性能优化,散列的每个键实际上是IP 地址的%IPs预编译正则表达式 ( ),带有字边界标记 ( ) 和转义元字符 ( & ),因此这意味着文字而不是任何字符。qr//\b\Q\E..

预编译正则表达式将最大限度地减少编译正则表达式所花费的 CPU 时间,从 path_ips.txt 的每行​​每个 IP 地址 1 个(即 中的行数乘以ip_hostname.txt中的行数path_ips.txt)减少到每个 IP 地址仅 1 个。如果其中一个或两个文件很大,这将对性能产生显着影响。

变量$first用于跟踪脚本是否正在读取第一个文件。它在主循环之前初始化为 true (1) while,并在每个输入文件末尾设置为 false (0)。

处理第一个文件后,对于第二个文件 ( path_ips.txt) 的每一行,它会迭代%IPs哈希以搜索每个 IP 地址并将其替换为关联的主机名。然后它打印(可能已修改的)输入行。

仅更改每行上匹配的 IP 地址,其他所有内容(包括空格)保持原样。

#!/usr/bin/perl

use strict;

my %IPs;
my $first = 1;

while(<>) {
  if ($first) {
    chomp;                   # strip \n or \r\n line-endings
    my ($host,$ip) = split;  # assumes whitespace delimited input
    $IPs{qr/\b\Q$ip\E\b/} = $host;

  } else {
    foreach my $ip (keys %IPs) {
      s/$ip/$IPs{$ip}/g;
    };
    print;
  };
  $first = 0 if eof;
};

#use Data::Dump qw(dd);
#dd \%IPs;

另存为,例如,map-hostnames.pl并使用chmod +x.

示例输出(ip_hostname.txt编辑文件以包含问题中提到的所有 IP/主机的映射):

$ ./map-hostnames.pl ip_hostname.txt path_ips.txt 
/path1/foo/bar host1 host2 host3
/path2/foo/bar host3 host7
/path3/foo/bar host4 host8 host29 host75
/path100/foo/bar host60

顺便说一句,如果你想看看%IPs哈希的样子,请取消注释脚本的最后两行(需要数据::转储待安装的模块)。它看起来像这样,但包含真实ip_hostname.txt文件的内容:

{
  "(?^:\\b10\\.1\\.1\\.1\\b)"    => "host1",
  "(?^:\\b10\\.29\\.29\\.29\\b)" => "host29",
  "(?^:\\b10\\.2\\.2\\.2\\b)"    => "host2",
  "(?^:\\b10\\.3\\.3\\.3\\b)"    => "host3",
  "(?^:\\b10\\.4\\.4\\.4\\b)"    => "host4",
  "(?^:\\b10\\.50\\.50\\.50\\b)" => "host100",
  "(?^:\\b10\\.60\\.60\\.60\\b)" => "host60",
  "(?^:\\b10\\.75\\.75\\.75\\b)" => "host75",
  "(?^:\\b10\\.7\\.7\\.7\\b)"    => "host7",
  "(?^:\\b10\\.8\\.8\\.8\\b)"    => "host8",
}

相关内容