วิธีการใช้ regex กับ 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 ลงในแต่ละหมายเลขแล้วตามด้วย a "ในแต่ละบรรทัดหากมีหนึ่งเช่น

(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+)แต่มันไม่ทำงาน ฉันจะทำให้มันทำงานได้อย่างไร

หากเพ่งพิศไม่ใช่ทางออกที่ดีที่สุดจะมีอะไรให้ใช้อีกบ้าง?


ขออภัยเกี่ยวกับการทำซ้ำ แต่ก่อนอื่นฉันถามเกี่ยวกับ stackoverflow และไม่มีคำตอบที่น่าพอใจดังนั้นฉันจึงตั้งค่าสถานะสำหรับการย้ายข้อมูล แต่มันไม่ได้เกิดขึ้นซักพักแล้วดังนั้นฉันไม่ได้คาดหวังว่ามันจะเกิดขึ้นแล้วถามใน Unix.SE
ทิม

คำตอบ:


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 ") ในหนึ่งบรรทัด


UPDATE

เนื่องจาก @Tim (OP) กล่าวว่าหมายเลขที่ตามด้วย"ในบรรทัดเดียวกันอาจแตกต่างกันฉันจึงทำการเปลี่ยนแปลงบางอย่างในโซลูชันก่อนหน้าของฉันและทำให้มันใช้ได้กับตัวอย่างใหม่ของคุณ

BTW จากตัวอย่างที่ฉันรู้สึกว่ามันอาจเป็นโครงสร้างของเนื้อหาดังนั้นฉันไม่เห็นว่าตัวเลขทั้งสองจะแตกต่างกันอย่างไร อันดับแรกคือหมายเลขหน้าที่พิมพ์และอันดับที่ 2 คือ # จะเป็นดัชนีหน้า ฉันถูกไหม?

อย่างไรก็ตามคุณรู้ว่าความต้องการของคุณดีที่สุด ตอนนี้โซลูชันใหม่ยังคงมีเพ่งพิศ (ฉันแบ่งคำสั่งเป็นบรรทัดเพื่อให้อ่านง่ายขึ้น):

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' มีความแตกต่างกันอย่างไร
ทิม

คำตอบนี้ใช้ได้กับข้อกำหนด / ตัวอย่างปัจจุบันของคุณ หากมีการเปลี่ยนแปลงข้อกำหนดบางทีคุณอาจแก้ไขคำถามและให้ตัวอย่างที่ดีกว่า และจากรหัสของคุณawk -F'#'ดูเหมือนว่าคุณต้องการที่จะทำการเปลี่ยนแปลงในส่วนหลังจาก '#' หรือไม่
Kent

ขอบคุณสำหรับคำแนะนำของคุณ ฉันเพิ่งแก้ไขตัวอย่างเพื่อให้ตัวเลขสองตัวไม่เหมือนกัน
ทิม

@Tim ดูคำตอบที่อัปเดตของฉันสำหรับตัวอย่างใหม่ของคุณ
Kent

ขอบคุณ! บางคำถาม: (1) FS=OFS="\" \"#"หมายความว่าตัวคั่นของฟิลด์ในอินพุตและเอาต์พุตคือ double quote, space, double quote และ #? ทำไมต้องระบุเครื่องหมายคำพูดคู่สองครั้ง (2) ใน/.* ([0-9]+)$/ไม่$หมายถึงการสิ้นสุดของสตริง? (3) ในอาร์กิวเมนต์ที่สามของ gensub () อะไรคือความแตกต่างระหว่าง"g"และ"G"?
ทิม

7

ซึ่งแตกต่างจากเครื่องมือเกือบทุกตัวที่ให้การแทนที่ regexp awk ไม่อนุญาตการย้อนกลับเช่น\1ในข้อความแทนที่ GNU Awk ให้เข้าถึงกลุ่มจับคู่ถ้าคุณใช้matchฟังก์ชั่นแต่ไม่ได้มี~หรือหรือsubgsub

โปรดทราบว่าแม้ว่าจะ\1ได้รับการสนับสนุนข้อมูลโค้ดของคุณจะต่อท้ายสตริง+11ไม่ใช่ทำการคำนวณเชิงตัวเลข นอกจากนี้ regexp ของคุณไม่ถูกต้องคุณสิ่งที่ตรงกับที่ต้องการและไม่ได้"42"""#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 อนุญาตให้มีการตอบกลับในรูปแบบ regex หรือไม่?
Wildcard

1
@ Wildcard ไม่, awk ไม่ได้ติดตามกลุ่ม (ยกเว้นส่วนขยาย GNU ที่ฉันพูดถึง)
Gilles 'หยุดชั่วร้าย'

5

awkสามารถทำได้ แต่ไม่สามารถทำได้แม้จะใช้การอ้างอิงกลับ
GNU awkมี (บางส่วน) backreferecing ในรูปแบบของgensub

อินสแตนซ์ของ123"ถูกห่อชั่วคราว \x01และ\x02เพื่อทำเครื่องหมายว่าไม่ได้แก้ไข (สำหรับsub(). co

หรือคุณเพียงแค่ก้าวผ่านผู้สมัครที่เปลี่ยนลูปในขณะที่คุณไปซึ่งในกรณีนี้ไม่จำเป็นต้องใช้ backreferencing และ "brackets" แต่การติดตามดัชนีตัวละครเป็นสิ่งจำเป็น

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และอาเรย์splitและ\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" }'

ขอบคุณ! ในรหัสแรกของคุณ (1) "\x01\\1\"\x02"หมายความว่าอย่างไร ผมก็ยังไม่เข้าใจและ\x01 \x02(2) วิธีการที่แตกต่างกันคือผลตอบแทน$0จากgensubและ$0เป็นอาร์กิวเมนต์สุดท้ายที่จะgensub?
ทิม

@ Tim ค่าฐานสิบหก\x01และ\x02ใช้เป็นเครื่องหมายทดแทน ค่าเหล่านี้เป็นอย่างมากไม่น่าจะเป็นในการใด ๆตามปกติแฟ้มข้อความเพื่อให้พวกเขาได้อย่างเท่าเทียมกัน "สูง" ปลอดภัยที่จะใช้ (เช่น. ไม่พบการปะทะกันกับคนที่มีอยู่ก่อนก) .. พวกเขามีป้ายเพียงชั่วคราว .. เรื่อง$0=gensub(... $0).. เห็นนี้ การเชื่อมโยงฟังก์ชั่น String-Manipulationแต่โดยสรุป: มัน (gensub) ส่งคืนสตริงที่แก้ไขเนื่องจากผลลัพธ์ของฟังก์ชันและสตริงเป้าหมายดั้งเดิมจะไม่เปลี่ยนแปลง ... $0=เพียงปรับเปลี่ยนเป้าหมายดั้งเดิม ..
เตอร์

3

เนื่องจากโซลูชันใน (g) awk ดูเหมือนจะค่อนข้างซับซ้อนฉันต้องการเพิ่มโซลูชันทางเลือกใน Perl:

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

คำอธิบาย:

  • ตัวเลือก-wเปิดใช้งานการเตือน (ซึ่งจะเตือนคุณถึงผลกระทบที่ไม่พึงประสงค์ที่เป็นไปได้)
  • ตัวเลือกที่-pหมายถึงการวนรอบรหัสที่ทำงานคล้ายกับ sed หรือ awk $_ประหยัดแต่ละบรรทัดของการป้อนข้อมูลโดยอัตโนมัติในตัวแปรเริ่มต้นที่
  • ตัวเลือก-eบอก Perl ว่ารหัสโปรแกรมดังต่อไปนี้ในบรรทัดคำสั่งไม่ได้อยู่ในไฟล์สคริปต์
  • รหัสคือการทดแทน regex ( s/.../.../) บน$_โดยที่ลำดับของตัวเลขหากตามมาด้วย"จะถูกแทนที่ด้วยลำดับซึ่งตีความว่าเป็นตัวเลขในการเพิ่มนอกจากนี้บวก 11
  • การยืนยันในเชิงบวกที่มีการมองไปข้างหน้าเป็นศูนย์ (?=pattern)จะมองหาสิ่งนั้น"โดยไม่คำนึงถึงการจับคู่ดังนั้นเราไม่จำเป็นต้องทำซ้ำในการแทนที่ ตัวแปร MATCH $&ในการแทนที่จะมีเฉพาะตัวเลข
  • โมดิ/eฟายเออร์ของ regex บอกperlให้ "execute" การแทนที่เป็นโค้ดแทนที่จะใช้มันเป็นสตริง
  • โมดิ/gฟายเออร์จะทำการแทนที่ "global" โดยทำซ้ำในทุกการแข่งขันในบรรทัด

$&น่าเสียดายที่ตัวแปร MATCH จะเป็นอันตรายต่อประสิทธิภาพของรหัสในรุ่น Perl ก่อน 5.20 โซลูชันที่เร็วขึ้น (และไม่ซับซ้อนมากขึ้น) จะใช้การจัดกลุ่มและการอ้างอิงย้อนกลับ$1แทน:

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

และหากการยืนยันล่วงหน้านั้นดูสับสนเกินไปคุณสามารถเปลี่ยนเครื่องหมายคำพูดได้อย่างชัดเจน:

perl -wpe 's/(\d+)"/$1+11 . q{"}/eg' < in.txt > out.txt
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.