在一个大型文本文件中,我需要用不同的替换文本(我们称它们为“值”)替换几个单词(我们称它们为“键”)。目前,我用于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::CSV
、JSON
) 从 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
您最终会将A
s 变成C
s 而不是B
s。上面的方法perl
没有问题,因为它只运行一个s
ubtitute 运算符。
另请注意,perl
在其正则表达式中,处理从左到右的交替,因此如果输入s/\b(?:foo|foo bar)\b/$map{$&}/g
上有 , foo bar
,它将替换foo
,而不是foo bar
。
请记住,关联数组的遍历顺序是随机的。
sed
-E
(对于那些支持带有/-r
或BRE 中的扩展正则表达式的实现\|
)相反,将尝试找到最长的匹配。
perl
您可以通过在加入之前按长度对键进行排序来获得相同的行为|
,例如通过替换keys %map
为sort {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 正则表达式中使用哈希值,您将收到警告:“key
value
保留在正则表达式中使用哈希变量相反,%h.keys
哈希值keys
首先被获取并@(…)
强制转换为匹配器半中的数组(正则表达式匹配器中的$
-sigiled 或-sigiled 变量指示 Raku 按字面意思插入字符串化内容)。在替换半中,匹配变量是解码为/对的对应值。@
$/
value
key
value
[第二个示例使用-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
如果是这样,则通过从子字符串重新组装输入行来替换 的出现前thekey
,替换,然后是 后面的子字符串key
。- 它在
while
循环中执行此操作,以确保替换所有出现的情况,以防某个key
输入行上出现多次。 - 使用这种“手动”方法而不是基于正则表达式的原因是,通过这种方式,您对值的外观
gsub()
没有任何限制。key
如果我们将gsub()
,key
与正则表达式特有的字符一起使用,可能会导致意外的行为。
对于您的输入示例,输出将如下所示:
联合国和世界卫生组织均对此进行了报道。这是国际原子能机构的主要领域。 EPA 是负责监督此事的联邦机构。
笔记并非所有awk
版本和实现都可以执行就地编辑(相当于标志-i
)。如果您有一个相当新的 GNU Awk (> 4.1.0),您可以使用-i inplace
该函数的扩展。
另请注意,在当前形式下,该程序并未实现对替换的“字边界”约束。