如何使用正则表达式和 AWK 进行字符串替换?

如何使用正则表达式和 AWK 进行字符串替换?

假设文件中有一些文本:

(bookmarks
("Chapter 1 Introduction 1" "#1"
("1.1 Problem Statement and Basic Definitions 23" "#2")
("Exercises 31" "#30")
("Notes and References 42" "#34"))
)

我想将 11 添加到每个数字,然后"在每行中添加一个(如果有的话),即

(bookmarks
("Chapter 1 Introduction 12" "#12"
("1.1 Problem Statement and Basic Definitions 34" "#13")
("Exercises 42" "#41")
("Notes and References 53" "#45"))
)

这是我使用 GNU AWK 和正则表达式的解决方案:

awk -F'#' 'NF>1{gsub(/"(\d+)\""/, "\1+11\"")}'

即,我想替换(\d+)\"\1+10\"\1代表 的组在哪里(\d+)。但这不起作用。我怎样才能让它发挥作用?

如果 gawk 不是最好的解决方案,还可以使用什么?

答案1

试试这个(需要呆呆)。

awk '{a=gensub(/.*#([0-9]+)(\").*/,"\\1","g",$0);if(a~/[0-9]+/) {gsub(/[0-9]+\"/,a+11"\"",$0);}print $0}' YourFile

测试以你的例子:

kent$  echo '(bookmarks
("Chapter 1 Introduction 1" "#1"
("1.1 Problem Statement and Basic Definitions 2" "#2")
("Exercises 30" "#30")
("Notes and References 34" "#34"))
)
'|awk '{a=gensub(/.*#([0-9]+)(\").*/,"\\1","g",$0);if(a~/[0-9]+/) {gsub(/[0-9]+\"/,a+11"\"",$0);}print $0}'   
(bookmarks
("Chapter 1 Introduction 12" "#12"
("1.1 Problem Statement and Basic Definitions 13" "#13")
("Exercises 41" "#41")
("Notes and References 45" "#45"))
)

请注意,如果两个数字(例如 1" 和 "#1")不同,或者该模式同一行中有更多数字(例如 23" ...32"..."# 123") 在一行中。


更新

由于@Tim(OP)说同一行中后面的数字"可能不同,因此我对之前的解决方案做了一些更改,并使其适用于您的新示例。

顺便说一句,从这个例子中我觉得它可能是一个目录结构,所以我不明白这两个数字有什么不同。第一个是打印的页码,第二个带 # 的是页面索引。我对吗?

不管怎样,你最了解自己的需求。现在新的解决方案,仍然使用 gawk (我将命令分成几行以使其更易于阅读):

awk 'BEGIN{FS=OFS="\" \"#"}{if(NF<2){print;next;}
        a=gensub(/.* ([0-9]+)$/,"\\1","g",$1);
        b=gensub(/([0-9]+)\"/,"\\1","g",$2); 
        gsub(/[0-9]+$/,a+11,$1);
        gsub(/^[0-9]+/,b+11,$2);
        print $1,$2
}' yourFile

测试和你的新的例子:

kent$  echo '(bookmarks
("Chapter 1 Introduction 1" "#1"
("1.1 Problem Statement and Basic Definitions 23" "#2")
("Exercises 31" "#30")
("Notes and References 42" "#34"))
)
'|awk 'BEGIN{FS=OFS="\" \"#"}{if(NF<2){print;next;}
        a=gensub(/.* ([0-9]+)$/,"\\1","g",$1);
        b=gensub(/([0-9]+)\"/,"\\1","g",$2); 
        gsub(/[0-9]+$/,a+11,$1);
        gsub(/^[0-9]+/,b+11,$2);
        print $1,$2
}'                        
(bookmarks
("Chapter 1 Introduction 12" "#12"
("1.1 Problem Statement and Basic Definitions 34" "#13")
("Exercises 42" "#41")
("Notes and References 53" "#45"))
)


编辑2基于@Tim 的评论

(1) FS=OFS="\" \"#" 是否表示输入和输出中的字段分隔符都是双引号、空格、双引号和#?为什么要指定两次双引号?

您对于输入和输出部分中的分隔符都是正确的。它将分隔符定义为:

" "#

有两个双引号,因为更容易捕获您想要的两个数字(根据您的示例输入)。

(2)/.*([0-9]+)$/中,$是否表示字符串结束?

确切地!

(3) gensub()的第三个参数中,“g”和“G”有什么区别? G和g之间没有区别。看一下这个:

gensub(regexp, replacement, how [, target]) #
    Search the target string target for matches of the regular expression regexp. 
    If "how" is a string beginning with ‘g’ or ‘G’ (short for “global”), then 
        replace all matches of regexp with replacement.

这是来自http://www.gnu.org/s/gawk/manual/html_node/String-Functions.html。您可以阅读以获取 gensub 的详细用法。

答案2

与几乎所有提供正则表达式替换的工具不同,awk 不允许反向引用,例如\1在替换文本中。如果您使用 GNU Awk 可以访问匹配的组match功能,但不能与~sub或 一起使用gsub

另请注意,即使\1支持,您的代码片段也会附加字符串+11,而不是执行数值计算。另外,您的正则表达式不太正确,您正在匹配诸如"42""和 not 之类的东西"#42"

这是一个 awk 解决方案(警告,未经测试)。它只对每行执行一次替换。

awk '
  match($0, /"#[0-9]+"/) {
    n = substr($0, RSTART+2, RLENGTH-3) + 11;
    $0 = substr($0, 1, RSTART+1) n substr($0, RSTART+RLENGTH-1)
  }
  1 {print}'

如果用 Perl 的话会更简单。

perl -pe 's/(?<="#)[0-9]+(?=")/$1+11/e'

答案3

awk可以做到,但它不是直接的,即使使用反向引用也是如此。
GNU awk有(部分)反向引用,其形式为根子

的实例123"被暂时包裹起来 \x01并将\x02它们标记为未修改(对于sub().co

或者,您可以单步执行循环,随时更改候选者,在这种情况下,不需要反向引用和“括号”;但需要跟踪字符索引。

awk '{$0=gensub(/([0-9]+)\"/, "\x01\\1\"\x02", "g", $0 )
      while ( match($0, /\x01[0-9]+\"\x02/) ) {
        temp=substr( $0, RSTART, RLENGTH )
        numb=substr( temp, 2, RLENGTH-3 ) + 11
        sub( /\x01[0-9]+\"\x02/, numb "\"" ) 
      } print }'

这是另一种方法,使用 gensub数组splitand\x01作为字段分隔符(例如分裂).. \x02 将数组元素标记为算术加法的候选元素。

awk 'BEGIN{ ORS="" } {
     $0=gensub(/([0-9]+)\"/, "\x01\x02\\1\x01\"", "g", $0 )
     split( $0, a, "\x01" )
     for (i=0; i<length(a); i++) { 
       if( substr(a[i],1,1)=="\x02" ) { a[i]=substr(a[i],2) + 11 }
       print a[i]
     } print "\n" }'

答案4

由于 (g)awk 中的解决方案似乎变得相当复杂,我想在 Perl 中添加替代解决方案:

perl -wpe 's/\d+(?=")/$&+11/eg' < in.txt > out.txt

解释:

  • 选项-w启用警告(这将警告您可能出现的不良影响)。
  • Option-p意味着围绕代码进行循环,其工作方式类似于 sed 或 awk,自动将每行输入保存在默认变量 中$_
  • 选项-e告诉 perl 程序代码在命令行上,而不是在脚本文件中。
  • 该代码是 上的正则表达式替换 ( s/.../.../) $_,其中数字序列如果后跟",将被该序列替换,解释为加法中的数字加 11。
  • 零宽度正前瞻断言 (?=pattern)查找 ,但"不将其带入匹配中,因此我们不必在替换中重复它。替换中的MATCH 变量$&将仅包含数字。
  • /e正则表达式的修饰符告诉将perl替换作为代码“执行”,而不是将其作为字符串。
  • 修饰符/g使替换成为“全局”,并在该行中的每个匹配项上重复它。

遗憾的是, MATCH 变量$&会损害 5.20 之前的 Perl 版本中的代码性能。更快(并且不会更复杂)的解决方案将使用分组和反向引用$1

perl -wpe 's/(\d+)?="/$1+11/eg' < in.txt > out.txt

如果前瞻断言看起来太混乱,您还可以显式替换引号:

perl -wpe 's/(\d+)"/$1+11 . q{"}/eg' < in.txt > out.txt

相关内容