3 điểm bởi GN⁺ 2024-03-21 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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, \Z với "cat\n" khác nhau giữa PHP, ECMAScript, Python, Go, Java 8, .NET 7.0 và Rust; \z trong 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 re củ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 đủ; \z\Z là 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 Pythonmô tả về các cú pháp regex khác, mức độ hỗ trợ và ý nghĩa của \z\Z khá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; \z\Z không được hỗ trợ
    • Python: "cat$" khớp bất kể có multiline hay không, còn "cat\z""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; \Z khô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
  • \z trong 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

 
GN⁺ 2024-03-21
Ý 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ỗi

    • Tôi cũng nhìn tiêu đề và nghĩ “đương nhiên là không rồi, ai lại nói thế nhỉ?”
      Tô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 $cuối chuỗi, còn trước giờ tôi luôn xem nó là cuối dòng
    • Tôi thấy cách bài viết gọi ^ là “đầu chuỗi” hơi vướng
      Trê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 \A hơn, cuối chuỗi thì gần với \Z hơn
    • Tôi cũng từng nghĩ vậy, nhưng khi tự thử trong Perl thì $ mặc định hoạt động giống một positive lookahead assertion cho cuối chuỗi
      Nó 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 ở $
    • Với tôi thì Vim mới là thứ gieo vào đầu tôi cách hiểu đó, hơn cả grep
  • Regex 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 định
    Cá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òng
    Khô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ăng
    Có 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

    • Đầu óc tôi suy nghĩ theo regex của Perl trước, rồi mới dịch sang cho khớp với những phần thiếu nhất quán của ngôn ngữ mình đang dùng
      Đặ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ét perl vào pipeline hơn nhiều
    • Tôi tò mò không biết ông đã ngấm nó bằng cách nào
      Perl 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 /m thì ngay trước bất kỳ ký tự xuống dòng nào

    • Việc bỏ sót Perl, ngôn ngữ có thể nói là gắn với regex mạnh mẽ nhất, có vẻ là một thiếu sót khá lớn
      Có 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 ^$đầu/cuối chuỗi, đồng thời đưa vào ^^$$ cho đầu/cuối dòng
    Không có chế độ nhiều dòng và cũng không cần đến nó
    Còn có \h là khoảng trắng ngang, \v là khoảng trắng dọc
    Nhờ 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ờ

    • Vì vậy nên người cứng đầu này không dùng được Perl 6
      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 ^$ dùng cho dòng, còn ^^$$ dùng cho chuỗi
      Vì 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ó
    • Nếu là tôi thì có lẽ tôi sẽ chọn đúng theo hướng ngược lại
      ^^ nhìn còn “giống bắt đầu” hơn ^
    • Gần như mọi regex tôi từng viết đều giả định đầu/cuối chuỗi
      Vì thông thường người ta đưa từng dòng vào regex để xử lý, nên việc dùng ^$ đơn lẻ cho toàn bộ chuỗi phần nào vẫn giữ được tính tương thích ngược
  • Tô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ó lúc tôi từng cảm thấy mình biết hết mọi phương ngữ
      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
    • Thư viện chuẩn C++ theo ISO/IEC 14882 yêu cầu triển khai sáu cú pháp regex tiêu chuẩn de facto hợp pháp: IEEE Std 1003.1-2008, tức BRE, ERE, awk, grep, egrep của POSIX, cùng với EcmaScript 3 của ECMA-262
      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/…
    • Những nhánh lớn mà tôi biết là POSIX, Perl/PCRE, và RE2 phía Go
      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
    • Các ngôn ngữ ra đời sau Perl nhìn chung đều dùng một biến thể nào đó của cú pháp regex Perl, nhưng lúc nào cũng có khác biệt nhỏ
      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án
    • Điều thú vị là RFC 9485 https://datatracker.ietf.org/doc/rfc9485/ “I-Regexp: An Interoperable Regular Expression Format” mới được xuất bản vào tháng 10 năm ngoái
  • Mọ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 ^$, dù ở chế độ một dòng hay nhiều dòng, đều là dựa trên dòng
    Vớ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 \A\Z hoặc thứ tương đương
    Cả 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\z
    https://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