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


13

假设文件中有一些文本:

(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和regex的解决方案:

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

即,我要替换(\d+)\"\1+10\"\1该组代表(\d+)。但这是行不通的。我该如何运作?

如果gawk不是最佳解决方案,那么还能使用什么?


对不起,重复。但是我首先问了stackoverflow,却没有令人满意的答案,因此我标记为要迁移。但是有一段时间没有发生,所以我没想到会发生然后在Unix.SE上询问。
蒂姆(Tim)

Answers:


12

试试这个(需要gawk)。

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"))
)


EDIT2基于@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的详细用法。


谢谢!我想知道如果两个数字(例如1”和“#1”不同)如何使它起作用吗?
Tim

此答案适用于您当前的要求/示例。如果需求有所变化,也许您可​​以编辑问题,并给出更好的例子。从您的代码awk -F'#'看来,您似乎只想对'#'之后的部分进行更改?
肯特

感谢您的建议。我只是修改了我的示例,以使两个数字不同。
蒂姆,

@Tim为您的新示例查看我更新的答案。
肯特

谢谢!一些问题:(1)FS=OFS="\" \"#"表示输入和输出中字段的分隔符是双引号,空格,双引号和#吗?为什么要指定两次双引号?(2)中的/.* ([0-9]+)$/,是否$表示字符串的结尾?(3)在gensub()的第三个参数中,"g"和之间有什么区别"G"
蒂姆(Tim)

7

与几乎所有提供正则表达式替换的工具不同,awk不允许反向引用(例如\1在替换文本中)。如果您使用match函数,但GNU Awk允许访问匹配的组,但不能使用~subgsub

还请注意,即使\1受支持,您的代码段也将附加字符串+11,而不执行数值计算。另外,您的regexp不太正确,您正在匹配诸如"42""and之类的东西"#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'

您的答案的第一句话正是我所要的。但是,您说“ ...在替换文本中”的事实提出了一个后续问题:awk是否在正则表达式模式本身中允许反向引用?
2015年

1
@Wildcard不,awk不能跟踪组(我提到的GNU扩展除外)。
吉尔斯(Gilles)'所以

5

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

的实例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 }'

这是使用gensuband数组split\x01作为字段定界符(用于split)的另一种方法 。\ 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" }'

谢谢!在您的第一个代码中,(1)是什么"\x01\\1\"\x02"意思?我还是不明白\x01\x02。(2)如何不同是在返回$0通过gensub$0作为最后一个参数gensub
蒂姆(Tim)

@蒂姆 十六进制值\x01\x02用作替换标记。这些值是非常不可能在任何正常的文本文件,所以他们同样“高度”安全使用(即未遇到预先存在的那些冲突)。他们只是临时标签..再$0=gensub(... $0)..看到这link String-Manipulation Functions,但总而言之:它(gensub)返回修改后的字符串,作为该函数的结果,并且原始目标字符串未更改。......在$0=简单的修改了原来的目标..
Peter.O

3

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

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

说明:

  • Option -w启用警告(将警告您可能的不良影响)。
  • Option -p表示代码周围的循环类似于sed或awk,将输入的每一行自动保存在默认变量中$_
  • Option -e告诉perl,程序代码在命令行中,而不是在脚本文件中。
  • 该代码是上的正则表达式替换(s/.../.../$_,其中一个数字序列(如果后跟a ")将被该序列替换,该序列将被解释为加号加上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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.