Làm thế nào để sử dụng regex với AWK để thay thế chuỗi?


13

Giả sử có một số văn bản từ một tập tin:

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

Tôi muốn thêm 11 vào mỗi số theo sau là một "trong mỗi dòng nếu có một, tức là

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

Đây là giải pháp của tôi bằng cách sử dụng GNU AWK và regex:

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

tức là tôi muốn thay thế (\d+)\"bằng \1+10\", \1nhóm đại diện ở đâu (\d+). Nhưng nó không hoạt động. Làm thế nào tôi có thể làm cho nó hoạt động?

Nếu gawk không phải là giải pháp tốt nhất, những gì khác có thể được sử dụng?


Xin lỗi về sự trùng lặp. Nhưng trước tiên tôi đã hỏi về stackoverflow và không có câu trả lời thỏa đáng, vì vậy tôi đã gắn cờ cho việc di chuyển. Nhưng nó đã không xảy ra trong một thời gian, vì vậy tôi không mong đợi nó sẽ xảy ra và sau đó hỏi trên Unix.SE.
Tim

Câu trả lời:


12

Hãy thử điều này (gawk là cần thiết).

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

Kiểm tra với ví dụ của bạn:

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

Lưu ý rằng lệnh này sẽ không hoạt động nếu hai số (ví dụ 1 "và" # 1 ") khác nhau hoặc có nhiều số hơn cùng dòng với mẫu này (ví dụ: 23" ... 32 "..." # 123 ") trong một dòng.


CẬP NHẬT

Vì @Tim (OP) cho biết số theo sau "trong cùng một dòng có thể khác nhau, tôi đã thực hiện một số thay đổi trên giải pháp trước đây của mình và làm cho nó hoạt động cho ví dụ mới của bạn.

BTW, từ ví dụ tôi cảm thấy rằng nó có thể là một bảng cấu trúc nội dung, vì vậy tôi không thấy hai con số có thể khác nhau như thế nào. Đầu tiên sẽ là số trang in và thứ 2 với # sẽ là chỉ mục trang. Tôi có đúng không

Dù sao, bạn biết yêu cầu của bạn tốt nhất. Bây giờ là giải pháp mới, vẫn với gawk (Tôi chia lệnh thành các dòng để dễ đọc hơn):

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

kiểm tra với ví dụ mới của bạn :

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 dựa trên nhận xét của @Tim

(1) Có phải FS = OFS = "\" \ "#" có nghĩa là dấu phân cách của trường trong cả đầu vào và đầu ra là trích dẫn kép, dấu cách, dấu ngoặc kép và #? Tại sao chỉ định trích dẫn hai lần?

Bạn đúng cho dấu phân cách ở cả phần đầu vào và đầu ra. Nó định nghĩa dấu phân cách là:

" "#

Có hai dấu ngoặc kép, vì sẽ dễ dàng hơn để bắt được hai số bạn muốn (dựa trên đầu vào ví dụ của bạn).

(2) Trong /.* ([0-9] +) $ /, $ có nghĩa là kết thúc chuỗi không?

Chính xác!

(3) Trong đối số thứ ba của gensub (), sự khác biệt giữa "g" và "G" là gì? không có sự khác biệt giữa G và g. Kiểm tra này:

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.

Đây là từ http://www.gnu.org/s/gawk/manual/html_node/String-Fiances.html . bạn có thể đọc để có được cách sử dụng chi tiết của gensub.


Cảm ơn! Tôi tự hỏi làm thế nào để nó hoạt động nếu hai số ví dụ 1 "và" # 1 "khác nhau?
Tim

câu trả lời này hoạt động cho yêu cầu / ví dụ hiện tại của bạn. nếu yêu cầu được thay đổi, có lẽ bạn có thể chỉnh sửa câu hỏi và đưa ra một ví dụ tốt hơn. và từ mã của bạn awk -F'#', có vẻ như bạn chỉ muốn thực hiện thay đổi trên phần sau '#'?
Kent

Cảm ơn đề nghị của bạn. Tôi chỉ sửa đổi ví dụ của tôi để hai số không giống nhau.
Tim

@Tim xem câu trả lời cập nhật của tôi, cho ví dụ mới của bạn.
Kent

Cảm ơn! Một số câu hỏi: (1) có FS=OFS="\" \"#"nghĩa là dấu phân cách của trường trong cả đầu vào và đầu ra là trích dẫn kép, dấu cách, dấu ngoặc kép và #? Tại sao chỉ định trích dẫn hai lần? (2) trong /.* ([0-9]+)$/, có $nghĩa là kết thúc của chuỗi? (3) trong đối số thứ ba của gensub (), sự khác biệt giữa "g"và là "G"gì?
Tim

7

Không giống như mọi công cụ cung cấp thay thế regrec, awk không cho phép phản hồi như \1trong văn bản thay thế. GNU AWK cho phép truy cập đến các nhóm phù hợp nếu bạn sử dụng các matchchức năng , nhưng không phải với ~hoặc subhoặc gsub.

Cũng lưu ý rằng ngay cả khi \1được hỗ trợ, đoạn mã của bạn sẽ nối chuỗi +11, không thực hiện tính toán số. Ngoài ra, regrec của bạn không hoàn toàn đúng, bạn phù hợp với những thứ như "42""và không "#42".

Đây là một giải pháp awk (cảnh báo, chưa được kiểm tra). Nó chỉ thực hiện một thay thế duy nhất trên mỗi dòng.

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}'

Nó sẽ đơn giản hơn trong Perl.

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

Câu đầu tiên trong câu trả lời của bạn chính xác là những gì tôi đang tìm kiếm. Tuy nhiên, thực tế là bạn đã nói "... trong văn bản thay thế" đặt ra một câu hỏi tiếp theo: Awk có cho phép phản hồi trong chính mô hình regex không?
Wildcard

1
@Wildcard Không, awk chỉ không theo dõi các nhóm (ngoại trừ phần mở rộng GNU tôi đề cập).
Gilles 'SO- ngừng trở nên xấu xa'

5

awkcó thể làm điều đó, nhưng nó không trực tiếp, thậm chí sử dụng phản hồi.
GNU awk có (một phần) backreferecing, dưới dạng gensub .

Các trường hợp 123"được tạm bọc trong \x01\x02để đánh dấu chúng là chưa sửa đổi (ví sub(). Đồng

Hoặc bạn chỉ có thể bước qua các ứng cử viên thay đổi vòng lặp khi bạn đi, trong trường hợp đó, không cần phải có phản hồi ngược và "ngoặc"; nhưng theo dõi chỉ số nhân vật là cần thiết.

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 }'

Đây là một cách khác, sử dụng gensubvà mảng split\x01như một dấu phân cách trường (để phân tách ) .. \ x02 đánh dấu một phần tử mảng là một ứng cử viên cho phép cộng số học.

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" }'

Cảm ơn! Trong mã đầu tiên của bạn, (1) "\x01\\1\"\x02"có nghĩa là gì? Tôi vẫn không hiểu \x01\x02. (2) làm thế nào khác nhau là sự trở lại $0của gensub$0như là đối số cuối cùng gensub?
Tim

@Tim. Các giá trị hex \x01\x02được sử dụng làm dấu thay thế. Các giá trị này rất khó có trong bất kỳ tệp văn bản thông thường nào , vì vậy chúng có độ "an toàn" cao để sử dụng (nghĩa là không gặp phải xung đột với các tệp có sẵn) .. Chúng chỉ là nhãn tạm thời $0=gensub(... $0).. Xem lại Các hàm thao tác chuỗi liên kết , nhưng tóm lại: Nó (gensub) trả về chuỗi đã sửa đổi do kết quả của hàm và chuỗi đích ban đầu không bị thay đổi. ... Đơn $0=giản là sửa đổi mục tiêu ban đầu ..
Peter.O

2

Vì các giải pháp trong (g) awk dường như trở nên khá phức tạp, tôi muốn thêm một giải pháp thay thế trong Perl:

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

Giải trình:

  • Tùy chọn -wcho phép cảnh báo (sẽ cảnh báo bạn về các tác động không mong muốn có thể xảy ra).
  • Tùy chọn -pngụ ý một vòng lặp xung quanh mã hoạt động tương tự như sed hoặc awk, tự động lưu từng dòng đầu vào trong biến mặc định , $_.
  • Tùy chọn -echo biết perl rằng mã chương trình đang theo dòng lệnh, không phải trong tệp tập lệnh.
  • Mã này là một thay thế regex ( s/.../.../) trên $_, trong đó một chuỗi các chữ số, nếu nó được theo sau bởi a ", sẽ được thay thế bằng chuỗi, được hiểu là một số trong phép cộng, cộng với 11.
  • Các nhìn về phía trước khẳng định tích cực zero-width (?=pattern) vẻ cho "mà không cần dùng nó vào trận đấu, vì vậy chúng tôi không cần phải lặp lại nó trong người vào thay. Biến MATCH $&trong thay thế sau đó sẽ chỉ chứa số.
  • Công cụ /esửa đổi cho biểu thức chính quy perlsẽ "thực thi" thay thế dưới dạng mã thay vì lấy nó làm chuỗi.
  • Công cụ /gsửa đổi làm cho thay thế "toàn cầu", lặp lại nó trên mỗi trận đấu trong dòng.

Biến MATCH $&không may sẽ gây bất lợi cho hiệu suất mã trong các phiên bản Perl trước 5.20. Một giải pháp nhanh hơn (và không phức tạp hơn nhiều) sẽ sử dụng nhóm và phản hồi $1thay thế:

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

Và nếu xác nhận phía trước trông quá khó hiểu, bạn cũng có thể thay thế dấu ngoặc kép một cách rõ ràng:

perl -wpe 's/(\d+)"/$1+11 . q{"}/eg' < in.txt > out.txt
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.