我有一个包含多个 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))
其中N
is the size of fileip_hostname.txt
和M
is 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",
}