Biểu thức chính quy `$` không phải lúc nào cũng là “cuối chuỗi”
(sethmlarson.dev)- Trong Python
re,$có thể khớp không chỉ với cuối chuỗi mà còn với ngay trước ký tự xuống dòng cuối cùng ở cuối chuỗi, ngay cả khi chế độ multiline đã tắt - Không nên cho rằng vì
^trông giống “đầu chuỗi” nên$cũng hoạt động đối xứng hoàn toàn; ý nghĩa thực tế của nó thay đổi theo từng cách triển khai regex - Kết quả của
$,\z,\Zvới"cat\n"khác nhau giữa PHP, ECMAScript, Python, Go, Java 8, .NET 7.0 và Rust;\ztrong Python được bổ sung mới ở Python 3.14 - Nếu chấp nhận cả ký tự xuống dòng ở cuối, thì
$ở chế độ multiline sẽ khớp"cat\n"trên mọi nền tảng trong bảng; nhưng nếu chỉ muốn khớp cuối chuỗi không bao gồm xuống dòng thì việc chọn cú pháp sẽ khác - Nếu không muốn khớp với ký tự xuống dòng cuối cùng, thì trên đa số nền tảng nên dùng
\z; còn với Python trước 3.14 và ECMAScript thì cần cân nhắc các phương án thay thế khác
Vị trí mà $ khớp trong Python re
- Trong module biểu thức chính quy
recủa Python,$có thể khớp với cuối chuỗi hoặc ngay trước ký tự xuống dòng cuối cùng ở cuối chuỗi, ngay cả khi chế độ multiline đã tắt cat$khớp với"lolcat"nhưng không khớp với"internet cat video", nên có vẻ đơn giản; tuy nhiên nếu có ký tự xuống dòng ở cuối như"cat\n"thì kết quả có thể khác với dự đoán- Khi chỉ định
re.MULTILINE,$sẽ khớp với cuối chuỗi và cuối mỗi dòng, tức là ngay trước mỗi ký tự xuống dòng - Ngay cả ở mặc định,
$vẫn khớp với cuối chuỗi; và nếu chuỗi kết thúc bằng ký tự xuống dòng thì nó cũng khớp ngay trước ký tự đó
Khớp mà loại trừ ký tự xuống dòng cuối cùng
- Nếu muốn khớp nghiêm ngặt chỉ với cuối chuỗi, chỉ dùng
$có thể là chưa đủ;\zvà\Zlà các ứng viên cho anchor cuối chuỗi - Dựa trên tài liệu biểu thức chính quy của Python và mô tả về các cú pháp regex khác, mức độ hỗ trợ và ý nghĩa của
\zvà\Zkhác nhau tùy từng cách triển khai - Sự khác biệt với
"cat\n"như sau- PHP:
"cat$"khớp bất kể có multiline hay không,"cat\z"không khớp, còn"cat\Z"khớp - ECMAScript:
"cat$"ở chế độ multiline khớp, còn"cat$"khi không multiline thì không khớp;\zvà\Zkhông được hỗ trợ - Python:
"cat$"khớp bất kể có multiline hay không, còn"cat\z"và"cat\Z"đều không khớp với"cat\n" - Go và Rust:
"cat$"ở chế độ multiline khớp, còn"cat$"khi không multiline và"cat\z"thì không khớp;\Zkhông được hỗ trợ - Java 8 và .NET 7.0:
"cat$"khớp bất kể có multiline hay không,"cat\z"không khớp, còn"cat\Z"khớp
- PHP:
\ztrong Python được bổ sung mới ở Python 3.14; ở các phiên bản trước đó nó chưa được hỗ trợ- Nếu chấp nhận ký tự xuống dòng ở cuối, thì
$ở chế độ multiline sẽ khớp"cat\n"một cách nhất quán trên mọi nền tảng trong bảng - Nếu không muốn khớp với ký tự xuống dòng ở cuối, thì trên đa số nền tảng nên dùng
\z; với Python trước 3.14 nên dùng\Z, còn với ECMAScript thì nên dùng$không ở chế độ multiline - Dữ liệu trong bảng được thu thập từ regex101.com và chưa được kiểm chứng bằng runtime thực tế
1 bình luận
Ý kiến trên Hacker News
Từ trước đến nay tôi vẫn nghĩ
^là “đầu dòng”, còn$là “cuối dòng”Khi xử lý regex, rất nhiều lúc ta làm việc với văn bản theo từng dòng nên kết quả thường giống nhau, nhưng cách tôi hình dung các toán tử đó vẫn gần với “dòng” hơn là “chuỗi”
Có lẽ là vì tôi tiếp xúc với regex qua
grep, nên đã hình thành thói quen xem đầu vào là các dòng chứ không phải một chuỗiTôi dùng regex gần 20 năm rồi nhưng có lẽ đây là lần đầu tiên nghe nói
$là cuối chuỗi, còn trước giờ tôi luôn xem nó là cuối dòng^là “đầu chuỗi” hơi vướngTrên thực tế, cũng như
$là “cuối dòng” thì^là “đầu dòng”, còn đầu chuỗi thì giống\Ahơn, cuối chuỗi thì gần với\Zhơn$mặc định hoạt động giống một positive lookahead assertion cho cuối chuỗiNó không match và consume ký tự xuống dòng
Chỉ trong chế độ nhiều dòng thì nó mới match tại vị trí xuống dòng, nhưng ngay cả khi đó cũng có vẻ không consume
Trên thực tế, khi dùng
$, tôi không thể tạo được một regex vừa bắt ký tự cuối của một dòng, vừa consume ký tự xuống dòng, rồi bắt ký tự đầu của dòng tiếp theo; nhóm bắt chỉ đơn giản kết thúc ở$grepRegex POSIX và regex của Python là khác nhau
Nói chung, cú pháp regex không mang tính phổ quát, nên bạn phải xem tài liệu của đúng implementation mình dùng
Theo chương 9 của POSIX, regex hoạt động trên chuỗi, nhưng một số utility lại giới hạn xử lý theo từng dòng
Ngoài ra,
$được mô tả là một anchor cố định ở cuối chuỗi được đem ra match, nên rốt cuộc$mang nghĩa cuối chuỗi hay cuối dòng là do utility hoặc mode quyết địnhCác công cụ phổ biến như
grep,sed,awk, Python mặc định hoạt động theo dòng, nên thường được hiểu là cuối dòngKhông tồn tại một cú pháp regex phổ quát duy nhất
Nếu không biết đang dùng ngôn ngữ nào và tùy chọn gì, thì không thể đọc hay viết regex một cách ổn định
https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1...
Đây là dịp rất thích hợp để giới thiệu Robert Elder cho những ai chưa biết ông ấy
Ông ấy làm nội dung rất hay trên YouTube và blog, còn trong series về regex thì đào khá sâu vào sự khác biệt trong cách nhiều công cụ triển khai hành vi regex
Video gần đây cũng rất đáng xem: https://www.youtube.com/watch?v=ys7yUyyQA-Y
Cũng có nhiều nội dung mà độc giả HN có thể quan tâm, và còn nói về những chủ đề như thực tế cùng khó khăn của nghề tư vấn
https://www.youtube.com/@RobertElderSoftware
https://blog.robertelder.org/
https://blog.robertelder.org/regular-expressions/
https://www.youtube.com/watch?v=cK87ktENPrI
Khi học Perl, regex là một trong những thứ đầu tiên tôi thật sự ngấm được, và đến giờ Perl vẫn nằm ở một góc quen thuộc trong đầu tôi nhờ cuốn sách “Camel”
Điều quan trọng nhất lúc này là mỗi implementation đều khác nhau, nên tôi đã hình thành thói quen cứ bắt tay vào việc gì là lại lôi bảng tham chiếu tương ứng ra xem
Ví dụ, regex của Emacs không hỗ trợ ký tự từ dạng
\w, mà phải dùng các character class kiểu như\s_-, điều này khá bực mình, nhưng tôi vẫn cho rằng Emacs là số một về tài liệu và khả năng khám phá tính năngCó utility thì cần escape dấu ngoặc, có utility thì không; đôi khi hành vi này còn cấu hình được, đôi khi thì không
Tôi đã đi qua đủ các giai đoạn bối rối, khó chịu, bất bình, và giờ thì chỉ đơn giản là chấp nhận
Khái niệm ở đâu cũng giống nhau, chỉ là phương ngữ khác nhau thôi
Đặc biệt trong shell, thay vì còn phải nhớ
sed/grep/awkđang là GNU hay BSD, tôi thường xuyên nhétperlvào pipeline hơn nhiềuPerl trông như thể một con mèo đi ngang qua bàn phím vậy
Tôi như nghe thấy vô số quản lý tuyển dụng tệ hại đang thêm câu “làm sao match được cuối chuỗi trong regex?” vào danh sách câu hỏi gài bẫy của họ
Thật lạ khi bỏ Perl ra khỏi một danh sách liên quan đến regex
Trong tài liệu
perlre,$được mô tả như sau: match ở cuối chuỗi, hoặc ngay trước ký tự xuống dòng ở cuối chuỗi, hoặc nếu dùng/mthì ngay trước bất kỳ ký tự xuống dòng nàoCó lẽ điều đó cũng cho thấy Perl giờ đã bị đẩy ra ngoài vùng quan tâm của số đông
Raku, trước đây là Perl 6, quy định
^và$là đầu/cuối chuỗi, đồng thời đưa vào^^và$$cho đầu/cuối dòngKhông có chế độ nhiều dòng và cũng không cần đến nó
Còn có
\hlà khoảng trắng ngang,\vlà khoảng trắng dọcNhờ suy nghĩ lại hoàn toàn và viết lại từ đầu, họ có lợi thế là có thể rút kinh nghiệm từ việc hành vi cũ từng khiến mọi người bất ngờ
Nó tạo cảm giác như thứ cú pháp kiểu “line noise” đã học suốt mấy chục năm bị trộn ngẫu nhiên lại với nhau
Mặc định đáng ra nên ngược lại thì sẽ rõ ràng hơn
Có lẽ sẽ tự nhiên hơn nếu
^và$dùng cho dòng, còn^^và$$dùng cho chuỗiVì nó trông giống như
^^line1$\n^line2$\n^line3$\n$Hơn nữa, Perl 6 không có mặt ở khắp nơi, còn Perl 5 thì ở đâu cũng có
^^nhìn còn “giống bắt đầu” hơn^Vì thông thường người ta đưa từng dòng vào regex để xử lý, nên việc dùng
^và$đơn lẻ cho toàn bộ chuỗi phần nào vẫn giữ được tính tương thích ngượcTôi không chắc có ai cho rằng regex đã được chuẩn hóa hay không
Mỗi lần chuyển sang môi trường mới là lại phải học lại
Có thể còn nhiều phương ngữ regex khác nữa nhưng tôi chưa gặp, và trong phạm vi tôi biết thì hầu hết việc đều giải quyết được
Nó giống như lái xe thuê vậy
Nó vận hành hơi khác xe của tôi, có vài tính năng thiếu đi và vài tính năng được thêm vào, nhưng nhìn chung thì phần lớn vẫn khá giống nhau
Vì vậy ít nhất với tôi, regex đúng là đã được chuẩn hóa bằng nhiều tiêu chuẩn chính thức công khai
https://open-std.org/jtc1/sc22/…
https://pubs.opengroup.org/onlinepubs/9699919799/…
https://262.ecma-international.org/14.0/…
Nhiều hệ thống, bao gồm cả JavaScript, đã triển khai PCRE vì Perl bổ sung rất nhiều mở rộng hữu ích lên hệ POSIX
Nếu tôi nhớ không nhầm thì RE2 là hướng muốn kiềm chế các vấn đề hiệu năng và hành vi kỳ quặc của các hệ hiện có, và tôi từng nghĩ toàn bộ nó được triển khai bằng Go
Mãi sau tôi mới biết RE2 ra đời trước cả Go
Dù vậy, ý nghĩa của
$và cách chuyển sang chế độ nhiều dòng thường vẫn khá nhất quánMọi người đang nhầm lẫn giữa chuỗi và dòng
Chuỗi là một dãy ký tự, còn dòng có thể được hiểu theo hai cách
Nếu xem ký tự xuống dòng là ký hiệu kết thúc dòng thì một dòng là 0 hoặc nhiều ký tự không phải xuống dòng kèm theo một ký tự xuống dòng, và nếu cuối cùng không có xuống dòng thì đó không phải là một dòng hoàn chỉnh
POSIX dùng cách nhìn này
Nếu xem xuống dòng là ký hiệu phân tách dòng thì một dòng là một chuỗi gồm 0 hoặc nhiều ký tự không phải xuống dòng
Dù theo cách nào thì nội dung của dòng cũng kết thúc trước ký tự xuống dòng
Ngữ nghĩa của
^và$, dù ở chế độ một dòng hay nhiều dòng, đều là dựa trên dòngVới ngữ nghĩa dựa trên chuỗi, hay nếu xử lý tệp thì đôi khi có thể xem là ngữ nghĩa trên toàn bộ tệp, nên dùng
\Avà\Zhoặc thứ tương đươngCả hai cách diễn giải đều có ưu điểm riêng
Khi truyền văn bản theo kiểu nối tiếp, việc coi xuống dòng là ký hiệu kết thúc dòng giúp dễ biết đã nhận được một dòng hoàn chỉnh hay chưa
Trong tệp văn bản, xem xuống dòng là ký hiệu phân tách dòng có thể tiện hơn vì dòng cuối sẽ không bị coi là ở trạng thái sai, nhưng nếu có ký hiệu kết thúc dòng thì có thể phát hiện được những dòng bị ghi dở dang
Vì chuyện này mà đã có vài lỗi nghiêm trọng trong các ứng dụng dựa trên Ruby
Luôn phải dùng
\A\zhttps://homakov.blogspot.com/2012/05/saferweb-injects-in-var...
https://sakurity.com/blog/2015/02/28/openuri.html
https://sakurity.com/blog/2015/06/04/mongo_ruby_regexp.html