他のファイルのパターンと一致するファイルの複数行にある複数の部分文字列を置き換える方法は?

他のファイルのパターンと一致するファイルの複数行にある複数の部分文字列を置き換える方法は?

複数のIPアドレスとホスト名を含むファイルと、1行に複数のIPアドレスを持ついくつかのフォルダを含む他のファイルがあります。

IP_ホスト名.txt

host1 10.1.1.1
host2 10.2.2.2
host3 10.3.3.3
host100 10.50.50.50

path_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アドレスを変更したいpath_ips.txtホスト名が出るファイルIP_ホスト名.txt各 IP アドレスに一致するファイルです。

希望の出力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

私は入れ子になった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))とファイルサイズが複雑でなければなりません。正常に動作するにはメモリに入ることができるはずですが、最新のコンピュータではサイズが数 GB でなければ問題ありません。Nip_hostname.txtMpath_ips.txtip_hostname.txt

答え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 アドレスのように見える文字列が続く場合は例外です。たとえば、IPアドレスではなくファイル名の一部である/path/foo/bar 1.1.1.1場合。これが発生した場合は、ip_hostname.txtで別の形式を使用してパスとIPアドレスを区別する必要があります。1.1.1.1bar 1.1.1.1

答え4

次のPerlスクリプトは最初の入力ファイル(ip_hostname.txt)を読み取り、%IPsという名前の連想配列(ハッシュ)を構築します。ここで、キーはIPアドレス、値はホスト名です。

パフォーマンスを最適化するために、ハッシュの各キーは実際には%IPs単語境界マーク()とエスケープされたメタ文字(&)を含むIPアドレス()のプリコンパイルされた正規表現なので、文字ではなくリテラルを意味します。qr//\b\Q\E..

ip_hostname.txt正規表現を事前にコンパイルすると、path_ips.txtの1行あたりのIPアドレスあたり1(つまり、行数xの行数path_ips.txt)からIPアドレスあたり1つまでの正規表現のコンパイルに費やされるCPU時間が最小限に抑えられます。ファイルのいずれかまたは両方が大きい場合、パフォーマンスに大きな影響を与えます。

変数は、スクリプトが$first最初のファイルを読み込んでいるかどうかを追跡するために使用されます。メインループの前にはtrue(1)に初期化され、while各入力ファイルの最後にfalse(0)に設定されます。

最初のファイルを処理したら、2番目のファイル()の各行に対してハッシュを繰り返してpath_ips.txt各IPアドレスを取得し、それを関連付けられたホスト名に置き換えます。%IPs次に、(変更できる)入力行を印刷します。

各行で一致する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スクリプトの最後の2行のコメントを外します(必須)。データ::ダンプモジュールを取り付ける必要があります)。次のように見えますが、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",
}

関連情報