在大型文本文件中用相应的替换“值”替换出现的多个“键”

在大型文本文件中用相应的替换“值”替换出现的多个“键”

在一个大型文本文件中,我需要用不同的替换文本(我们称它们为“值”)替换几个单词(我们称它们为“键”)。目前,我用于sed此目的,如

sed -i -e 's/\bkey\b/value/' file

该文件很大,并且该过程需要几分钟。有超过 1,000 个“键值”对,目前我sed对每个“键值”对重复该过程。显然,这需要很长时间。

我想知道是否有一种方法可以将一组“键值”(模式替换)对输入sed/awk或类似的实用程序,以便在一次运行中(或更快)进行替换。 “键值”对可以以任何格式构造。

一个示例是用缩写词替换名称(例如,以 TSV 格式)

Key                                               Value
United Nations                                    UN
United States Environmental Protection Agency     EPA
International Atomic Energy Agency                IAEA
World Health Organization                         WHO

输入文本为:

联合国和世界卫生组织均对此进行了报道。这是国际原子能机构的主要领域。美国环境保护署是监督此事的联邦机构。

答案1

请注意,-i\b都是一些sed实现从perl.在这里,为什么不perl首先使用:

perl -i -pe '
  BEGIN {
    %map = (
      "key1"  => "value1",
      "key 2" => "value2"
    );
    $re = join "|", map {qr{\Q$_\E}} keys %map;
  }
  s/\b(?:$re)\b/$map{$&}/g' your-file

key => value 映射也可以表示为:

%map = qw(
   key1 value1
   key2 value2
);

或者使用相应的 perl 模块 ( Text::CSVJSON) 从 CSV 或其他结构化格式中读取...perl是一种适合文本操作的通用编程语言,因此这里是显而易见的选择,并且您可以用它做的事情没有限制。

对于简单的 TSV 来说,可能是:

<map.tsv perl -i -pe '
  BEGIN {
    <STDIN>; # skip header
    while (<STDIN>) {
      chomp;
      my ($k, $v) = split /\t/;
      $map{$k} = $v;
    }

    $re = join "|", map {qr{\Q$_\E}} keys %map;
  }
  s/\b(?:$re)\b/$map{$&}/g' your-file

请注意,如果您正在执行以下操作:

sed -i -e 's/\bK1\b/V1/g' file
sed -i -e 's/\bK2\b/V2/g' file

可以简化为:

sed -i '
  s/\bK1\b/V1/g
  s/\bK2\b/V2/g' file

或者对于您的 TSV:

<map.tsv awk -F'\t' '
   NR > 1 {
     # escape regexp operators in keys to emulate perl \Q \E:
     gsub(/[][\/\\*.^$]/, "\\\\&", $1)
     # escape /, \ and & in replacement:
     gsub(/[\\/&]/, "\\\\&", $2)
     print "s/\\b"$1"\\b/"$2"/g"
   }' | sed -i -f - your-file

它只读取(和写入)文件一次。

但在这两种情况下,如果某些价值观也在其中。例如,如果s/\bA\b/B/g使用后跟,s/\bB\b/C/g您最终会将As 变成Cs 而不是Bs。上面的方法perl没有问题,因为它只运行一个subtitute 运算符。

另请注意,perl在其正则表达式中,处理从左到右的交替,因此如果输入s/\b(?:foo|foo bar)\b/$map{$&}/g上有 , foo bar,它将替换foo,而不是foo bar

请记住,关联数组的遍历顺序是随机的。

sed-E(对于那些支持带有/-r或BRE 中的扩展正则表达式的实现\|)相反,将尝试找到最长的匹配。

perl您可以通过在加入之前按长度对键进行排序来获得相同的行为|,例如通过替换keys %mapsort {length$b <=> length$a} keys %map

最后一点:perl默认情况下按字节处理其输入,单词字符(\b匹配单词和非单词字符之间的边界)仅限于 ASCII 字母、数字和下划线,而sed实现通常根据语言环境的字符集对其进行解码。如果您的输入或键/值包含非 ASCII 字符,您可以添加 来-Mopen=locale根据区域设置的字符集对其进行解码,或者如果它采用 UTF-8(当今最常用的区域设置编码),则只需添加该-C选项。

答案2

假设您只需要像提供的示例中那样处理晴天映射(即没有正则表达式或反向引用元字符,没有大小写更改,没有子字符串,没有循环映射等),然后使用任何 awk:

$ awk -F'\t+' '
    NR==FNR { if (NR>1) map[$1]=$2; next }
    { for (key in map) gsub(key,map[key]); print }
' map_file input_file
This has been covered by both the UN and WHO. This is the main domain of the IAEA. EPA is a federal agency supervising this matter.

如果这不是您所需要的全部,那么编辑您的问题以提供更真正具有代表性的示例输入/输出。

答案3

使用(以前称为 Perl_6)

~$ raku -pe 'BEGIN my %h = ("United Nations" => "UN",  \
             "United States Environmental Protection Agency" => "EPA",  \
             "International Atomic Energy Agency" => "IAEA",  \
             "World Health Organization" => "WHO");  \
             s:g/@(%h.keys)/%h{$/}/;'   file

或者:

~$ raku -ne 'BEGIN my %h = ("United Nations" => "UN",  \
             "United States Environmental Protection Agency" => "EPA",  \
             "International Atomic Energy Agency" => "IAEA",  \
             "World Health Organization" => "WHO");  \
             put S:g/@(%h.keys)/%h{$/}/ given $_;'   file

输入示例:

This has been covered by both the United Nations and World Health Organization. This is the main domain of the International Atomic Energy Agency. United States Environmental Protection Agency is a federal agency supervising this matter.

示例输出:

This has been covered by both the UN and WHO. This is the main domain of the IAEA. EPA is a federal agency supervising this matter.

Raku 是 Perl 编程语言家族中的一种编程语言,这个答案基本上是 @Stéphane_Chazelas 发布的优秀 Perl 答案的翻译。 Raku 的最佳“用例”可能是您需要一致地处理 Unicode 替换,因为 Raku 为内置 Unicode 提供了高级支持。

简而言之,使用感兴趣的/对%h创建哈希。请注意 - 如果您尝试直接在 Raku 正则表达式中使用哈希值,您将收到警告:“keyvalue保留在正则表达式中使用哈希变量相反,%h.keys哈希值keys首先被获取并@(…)强制转换为匹配器半中的数组(正则表达式匹配器中的$-sigiled 或-sigiled 变量指示 Raku 按字面意思插入字符串化内容)。在替换半中,匹配变量是解码为/对的对应值。@$/valuekeyvalue

[第二个示例使用-ne带有 Raku 的“big-S”表示法的命令行标志S///——返回结果字符串]。

当然,为了更全面地复制给出的其他答案,您可以使用<|w><?wb>,它们是 Raku 的零宽度单词边界锚点 - 相当于\b其他语言中的锚点。因此上面的最后一行变成:

s:g/ <?wb> @(%h.keys) <?wb> /%h{$/}/;

您甚至可以使用 Raku 的<<左字边界和>>右字边界(Unicode 符号«»可以):

s:g/ << @(%h.keys) >> /%h{$/}/;


从 TSV 文件开始:

如果您从 2 列 TSV 文件中获取键/值对(而不是如上所述的内联),则代码会简化很多。Text::CSV按如下方式在命令行使用 Raku 的模块(注意:.skip(1)如果 TSV 文件没有标头,请删除该调用)。不要忘记包含.[*;*]索引括号代码,因为 Raku 将每一行视为添加到哈希中的两个元素(键和值)至关重要%h

~$ raku -MText::CSV -pe 'BEGIN my %h = csv(in => "/path/to/kv_pairs.tsv", sep => "\t").skip(1).[*;*];   
                         s:g/ << @(%h.keys) >> /%h{$/}/;'   file
This has been covered by both the UN and WHO. This is the main domain of the IAEA. EPA is a federal agency supervising this matter.

或者:

~$ raku -MText::CSV -ne 'BEGIN my %h = csv(in => "/path/to/kv_pairs.tsv", sep => "\t").skip(1).[*;*];   
                         put S:g/ << @(%h.keys) >> /%h{$/}/ given $_;'   file
This has been covered by both the UN and WHO. This is the main domain of the IAEA. EPA is a federal agency supervising this matter.

https://docs.raku.org/language/regexes
https://docs.raku.org
https://raku.org

答案4

假如说

  • 替代品价值观您的键值映射文件本身不能包含这将要求替换(包括它们自己的关联密钥!),并且
  • 映射文件是制表符分隔的

以下awk程序应该有效:

awk -F'\t' 'NR==FNR{repl[$1]=$2;klen[$1]=length($1);next}
            {for (key in repl) {
               while (i=index($0,key)) {
                 $0=substr($0,1,i-1) repl[key] substr($0,i+klen[key])
               }
             }
            }1' mapfile.txt input.txt

这将首先将输入字段分隔符设置为 TAB,并首先处理映射文件,然后处理实际的输入文件。

  • 在处理第一个文件时(由 表示FNR,每个文件行计数器,等于NR全局行计数器),它repl用要执行的替换填充数组,并在单独的数组 中跟踪“键”长度klen。然后,它会跳到下一行进行处理。
  • 当处理第二个文件时,条件NR==FNR不再满足并因此被跳过,它将针对每个输入行循环遍历所有替换键(即数组的所有索引repl),并使用该index()函数检查它们是否出现在输入行上。
  • key如果是这样,则通过从子字符串重新组装输入行来替换 的出现the key,替换,然后是 后面的子字符串key
  • 它在while循环中执行此操作,以确保替换所有出现的情况,以防某个key输入行上出现多次。
  • 使用这种“手动”方法而不是基于正则表达式的原因是,通过这种方式,您对值的外观gsub()没有任何限制。key如果我们将gsub(),key与正则表达式特有的字符一起使用,可能会导致意外的行为。

对于您的输入示例,输出将如下所示:

联合国和世界卫生组织均对此进行了报道。这是国际原子能机构的主要领域。 EPA 是负责监督此事的联邦机构。

笔记并非所有awk版本和实现都可以执行就地编辑(相当于标志-i)。如果您有一个相当新的 GNU Awk (> 4.1.0),您可以使用-i inplace该函数的扩展。

另请注意,在当前形式下,该程序并未实现对替换的“字边界”约束。

相关内容