Show HN: Biểu thức chính quy transductive cho chỉnh sửa văn bản
(github.com/c0stya)- TRRE là một phần mở rộng ngôn ngữ của biểu thức chính quy, bổ sung toán tử
:để biểu diễn trực tiếp phép biến đổi văn bản, và được cung cấp qua công cụ CLI thử nghiệmtrretương tựgrep -E - Dạng cơ bản là cặp transductive như
a:b, biến mẫu đầu vào thành mẫu đầu ra; xóa được biểu diễn làx:, chèn là:x, tức là phép biến đổi với chuỗi rỗng - Có thể dùng lựa chọn thay thế, lặp lại, và biến đổi dải ký tự như trong biểu thức chính quy thông thường; bài có các ví dụ như
cat:dog,[a:A-z:Z], và Caesar cipher - Bên trong, thay vì FSA của regex thông thường, nó xây dựng Finite State Transducer (FST) xử lý các cặp đầu vào-đầu ra, đồng thời hỗ trợ quyết định hóa on-the-fly mang tính thử nghiệm
- Hiện chưa có binary dựng sẵn, phải tự build; các mục TODO còn lại gồm ổn định hóa DFT, hỗ trợ Unicode đầy đủ, hoàn thiện tính năng ERE, và xử lý dải hiệu quả hơn
Vấn đề mà TRRE muốn giải quyết
- Biểu thức chính quy thông thường rất hữu ích để tìm mẫu trong văn bản, nhưng khi chỉnh sửa văn bản, logic xử lý nhóm có thể trở thành một bước hậu xử lý khá phức tạp
- TRRE mở rộng ngôn ngữ regex để đưa việc khớp mẫu và sửa đổi văn bản vào cùng một biểu thức
- Cú pháp cốt lõi là
pattern-to-match:pattern-to-generate; ví dụ đơn giản nhất làa:b, biếnathànhb - Công cụ CLI
trrelà một hiện thực để minh họa ý tưởng này và hoạt động với cảm giác tương tựgrep -E
Cú pháp biến đổi cơ bản
- Thay thế chuỗi được viết như
cat:dogecho 'cat' | ./trre 'cat:dog'sẽ in radog- Cũng có thể tạo cùng kết quả bằng biến đổi theo từng ký tự như
(c:d)(a:o)(t:g)
- Có thể dùng như
sedđể thay mọi khớp trong một chuỗi- Áp dụng
lamb:catchoMary had a little lamb.sẽ cho raMary had a little cat.
- Áp dụng
- Xóa được biểu diễn bằng cách để trống vế phải, theo dạng
string_to_delete:(x:)orsẽ xóaxkhỏixorđể tạo thànhora:trong scan mode mặc định sẽ thay mọiabằng ký hiệu rỗng rồi xóa chúng- Có thể dùng cú pháp ngoặc vuông như
[aie]:để xóa nhiều ký tự
- Chèn được biểu diễn bằng cách để trống vế trái, theo dạng
:string_to_insert(:x)orsẽ chènxtrướcorđể tạo thànhxorhad a (:little )lambsẽ chènlittletrong ngữ cảnh
Biến đổi trên regex
- TRRE hỗ trợ lựa chọn thay thế bằng
|như regex thông thường(c:b)at|(d:h)ogbiếncat dogthànhbat hog
- Toán tử lặp cũng có thể áp dụng cho biến đổi
(cat:dog)*biếncatcatcatthànhdogdogdog- Trong scan mode mặc định, chỉ với
cat:dogcũng sẽ được áp dụng lặp để cho cùng kết quả
- Khi dùng lặp ở mẫu bên trái, có thể tiêu thụ nhiều đầu vào và tạo thành một đầu ra duy nhất
(cat)*:dogbiếncatcatcatthànhdog
- Nếu dùng
*hoặc+ở mẫu bên phải, có thể gây ra vòng lặp vô hạn- Nên tránh các biểu thức như
:a* - Nếu cần lặp hữu hạn, hãy chỉ định số lần như
:(repeat-10-times){10}
- Nên tránh các biểu thức như
Biến đổi dải và bộ sinh
- Biến đổi dải ký tự được viết như
[a:A-z:Z]- Có thể biến
regular expressionsthànhREGULAR EXPRESSIONS
- Có thể biến
- Có ví dụ về Caesar cipher
[a:b-y:zz:a]biếncaesar cipherthànhdbftbs djqifs[a:zb:a-z:y]biến ngược lại thànhcaesar cipher
- Cũng có thể hoạt động như một generator, tạo nhiều đầu ra từ một đầu vào
- Mặc định sẽ dùng khớp đầu tiên có thể
- Dùng tùy chọn
-ađể tạo mọi đầu ra có thể
- Ví dụ, áp dụng
:(0|1){3}cho đầu vào rỗng có thể tạo các chuỗi nhị phân 3 bit từ000đến111 - Dùng
:(0|1){,3}?cùng với-masẽ tạo các đầu ra dạng tập con có độ dài không quá 3
Đặc tả ngôn ngữ và độ ưu tiên toán tử
- Không chính thức, TRRE được định nghĩa như các cặp
pattern-to-match:pattern-to-generate pattern-to-matchở bên trái có thể là chuỗi hoặc biểu thức chính quypattern-to-generateở bên phải thường là chuỗi, nhưng cũng có thể là regex- Toán tử
:hiện được xem là không kết hợp, nên dạngTRRE:TRREkhông được cho phép về mặt cú pháp- Dạng này có một ý nghĩa tự nhiên là phép hợp thành của các quan hệ do TRRE định nghĩa, nhưng hiện vẫn bị loại bỏ vì có thể làm độ phức tạp tăng lên
- Độ ưu tiên toán tử từ cao xuống thấp như sau
- Ký tự escape
\ - Biểu thức ngoặc vuông
[] - Gom nhóm
() - Lặp
* + ? {m,n} - Nối
- Transduction
: - Lựa chọn thay thế
|
- Ký tự escape
Chế độ và tính tham lam
trrehỗ trợ hai chế độ- Scan Mode: chế độ mặc định, áp dụng biến đổi tuần tự
- Match Mode: dùng cờ
-m, kiểm tra xem toàn bộ chuỗi có khớp biểu thức hay không
- Tùy chọn
-atạo mọi đầu ra có thể - Bộ sửa đổi
?khiến các toán tử*,+,{,}trở thành non-greedy<(.:)*>sẽ in ra<>từ<cat><dog><(.:)*?>sẽ in ra<><>từ cùng đầu vào
- Bài cũng có ví dụ thay nội dung bên trong thẻ hoặc ngoặc
<(.*?:cat)>biến<dog> <mouse>thành<cat> <cat>
Hiện thực dựa trên FST và quyết định hóa
- TRRE nội bộ xây dựng Finite State Transducer (FST)
- FST tương tự Finite State Automaton (FSA) dùng trong regex thông thường, nhưng xử lý các cặp đầu vào-đầu ra thay vì chỉ chuỗi đơn
- Khác biệt cốt lõi của TRRE gồm
- Định nghĩa một quan hệ nhị phân giữa hai ngôn ngữ chính quy
- Dùng FST thay vì FSA để suy diễn
- Hỗ trợ quyết định hóa on-the-fly mang tính thử nghiệm để cải thiện hiệu năng
- Trong các regex engine thông thường, quyết định hóa biến một ôtômat không đơn định thành ôtômat quyết định, cho phép suy diễn thời gian tuyến tính theo độ dài chuỗi đầu vào
- TRRE cũng có thể áp dụng cách tiếp cận tương tự, nhưng không phải mọi bộ biến đổi không đơn định NFT đều có thể chuyển thành bộ biến đổi quyết định DFT
- Nếu có hai chu trình “xấu” với cùng nhãn đầu vào, việc sinh trạng thái có thể rơi vào vòng lặp vô hạn
- Có cách phát hiện các vòng lặp như vậy, nhưng chi phí cao
Hiệu năng và tình trạng cài đặt
- Phiên bản không đơn định mặc định được cho là chậm hơn
sedmột chút trong ví dụ thay thế đơn giản./trre '(vodka):(VODKA)': real0m0.046ssed 's/vodka/VODKA/': real0m0.024s
- Với tác vụ phức tạp hơn, phiên bản quyết định
trre_dftcó ví dụ nhanh hơnsedsed -e 's/\(.*\)/\U\1/': real0m0.508s./trre_dft '[a:A-z:Z]': real0m0.131s
- Hiện vẫn chưa có binary dựng sẵn
- Cách cài đặt là clone kho mã rồi build và test bằng
make && sh test.sh - Các mục TODO hiện còn gồm
- Phiên bản DFT ổn định
- Hỗ trợ Unicode đầy đủ
- Hoàn thiện tính năng ERE
- Phủ định
^trong[] - Character class
- Ký hiệu neo
$^
- Phủ định
- Xử lý dải hiệu quả
Các hướng tiếp cận được tham khảo
- Cách tiếp cận khớp regex được truyền cảm hứng mạnh từ Regular Expression Matching Can Be Simple And Fast của Russ Cox
- Ý tưởng quyết định hóa transducer lấy từ Finitely Subsequential Transducers của Cyril Allauzen và Mehryar Mohri
- Cách tiếp cận parsing dùng Double-E algorithm của Erik Eidt và khá gần với Shunting Yard algorithm cổ điển
1 bình luận
Các ý kiến trên Hacker News
Thật thú vị khi xem dự án này sẽ đi đến đâu. Tuy vậy, độ ưu tiên toán tử có vẻ không tự nhiên, và có vẻ những người khác trong luồng này cũng cảm thấy tương tự
cat:dogkhiến người ta tự nhiên kỳ vọng nó tương đương với(cat):(dog), chứ không phảica(t:d)ogViệc
cat:dogđược diễn giải nhưca(t:d)ogthay vì(cat):(dog)cũng làm tôi bối rối, nhưng khi nhớ ra rằng tất cả chúng ta đang dùng regex hơi sai một chút thì tôi thấy hiểu được. Regex “vốn dĩ” nên được xem là bộ sinh chuỗi, chứ không phải bộ so khớp, nêncat|dogvề mặt hình thức có thể được xem là mở rộng thành một tập như{catog,cadog}Khi so khớp, chỉ cần lấy tập chuỗi này để so khớp chuỗi con với một văn bản lớn hơn. Vấn đề là hầu hết các engine regex thực tế không hoạt động như vậy, mà có nhiều hành vi kỳ lạ để phù hợp với kỳ vọng hoặc để đạt hiệu quả
Nếu thử nhiều công cụ regex, bạn sẽ thấy các biến thể như
(cat)|(dog)hoặc(cat)|(dog)|(ca[td]og). Vì vậy, từ góc nhìn hình thức hơn, tôi cho rằngcat:dogtạo raca(t:d)ogchứ không phải(cat):(dog)là đúng. Nhưng do hàng chục năm lạm dụng regex như một công cụ so khớp theo kỳ vọng của người dùng, giờ đây ai cũng đặt ngoặc quanh biểu thức muốn thay thếĐề xuất này thú vị và được thiết kế tốt, nhưng cuối cùng có cảm giác như đang đưa regex trở lại mô hình bộ sinh ban đầu. Vấn đề nằm ở phía công cụ nhiều hơn là ở cú pháp
Trước đây tôi từng làm việc gần với lĩnh vực này; nếu bạn chưa từng nghĩ về regex như bộ sinh tập chuỗi, có thể thử nghịch ở đây: https://onlinestringtools.com/generate-string-from-regex
Tuy nhiên, hành vi của các công cụ sinh kiểu này cũng rất đặc thù. Những công cụ tôi từng dùng có nhiều cách để giới hạn bộ sinh, chẳng hạn chỉ định ràng buộc cho closure
Nếu đẩy nó xuống sau phép nối thì có thể phát sinh vấn đề khác. Ví dụ với
:không có tính kết hợp,cat:dog:mousecó lẽ phải là bất hợp lệ, nhưng tôi chưa chắc nên xử lý thế nàoTrong phiên bản hiện tại, nó chèn epsilon, tức chuỗi rỗng. Ví dụ, để xóa cách một ký tự một lần, về mặt kỹ thuật có thể chạy
..:, tức.(.:eps)Kết quả của
echo 'abcde' | ./trre '..:'là'ace'Thực ra phép kết hợp
:cũng có thể mang nghĩa là hợp thành các quan hệ chính quy, nhưng hiện tại tôi thấy như vậy quá phức tạp[a:A-z:Z],[a-z:A-Z]tốt hơn, và tôi muốn đề xuất dạng như[a-y:b-z;z:a]thay cho[a:b-y:zz:a]Nếu bạn quan tâm đến bộ chuyển đổi trạng thái hữu hạn và các công cụ liên quan, XFST (Xerox Finite-State Transducer) rất đáng xem. Nó đã được dùng hơn 20 năm trong các ứng dụng ngôn ngữ học tính toán
Một nhà nghiên cứu người Phần Lan ở PARC từng đến lớp UT và trình bày cách xử lý hình thái học tiếng Phần Lan bằng FST; nhìn bên ngoài thôi cũng đã thấy khá ấn tượng
Bài này mô tả công việc đã làm ở PARC
OpenFst là một thư viện thực sự tuyệt vời cho các bộ chuyển đổi. Các tutorial dùng Pynini được làm dưới dạng bài tập ở Johns Hopkins và những nơi khác cũng khá ổn
[1] https://www.openfst.org/twiki/bin/view/GRM/Pynini
[2] https://www.openfst.org/
Nếu bạn đang tìm một lựa chọn thay thế cho regex chuẩn, đặc biệt khi logic nhóm khó hoặc bạn muốn các biểu thức dễ bảo trì, Rosie Pattern Language có thể phù hợp
https://gitlab.com/rosie-pattern-language/rosie/-/blob/maste...
https://rosie-lang.org/about/
Hay đấy. Khoảng năm 1997, tôi viết luận văn Diplom ngành khoa học máy tính về bộ chuyển đổi trạng thái hữu hạn, và nó kém tầm thường hơn tôi tưởng rất nhiều
Nhiệm vụ là triển khai phép hợp thành và DFA khi có thể, bao gồm cả các bộ chuyển đổi đã được hợp thành. Đó là “đại số của các bộ chuyển đổi trạng thái hữu hạn”, và ca sử dụng là hình thái học. Chủ đề bị đánh giá thấp nghiêm trọng, nên tôi phải dừng lại ở khoảng giữa. Vì vậy xin bày tỏ sự kính trọng
Về cú pháp, tôi thắc mắc liệu bạn có thực sự muốn
:liên kết chặt hơn phép nốiabkhôngThật vui khi thấy 20 năm sau dự án vẫn tiếp tục: https://www.openfst.org/twiki/bin/view/FST/WebHome
Tôi vẫn chưa chắc liệu
:có nên liên kết chặt hơn phép nối hay không. Sau khi xem khoảng 100 ví dụ, tôi thấy cách hiện tại, tức:có độ ưu tiên thấp hơn., có vẻ tự nhiên hơn; nhưng trong mã thì đúng nghĩa chỉ cần đổi một con số là thay được. Vì vậy tôi mới đăng lên đây, và thực sự cần phản hồi thực tếNgay khi muốn thực hiện một kiểu thay thế có cấu trúc nào đó, cách này trông có vẻ chưa đủ. Ví dụ, đôi khi muốn làm việc kiểu
s/"([^"]*)"/'$1'/Ngoài ra, nếu có thể đổi những phần khớp với
[']trong[^"]thành\'thì có vẻ còn hữu ích hơnNói tổng quát hơn, vì biểu thức chính quy thực chất định nghĩa một cây phân tích cú pháp cho kết quả khớp, nên sẽ hữu ích nếu có thể thực hiện các phép biến đổi tổng quát hơn trên cây đó
":'(':(\\')|[^"'])*":'"..."và đổi dấu ngoặc kép thành dấu nháy đơn'Có thể làm bằng biểu thức này:
echo '"hello world" "hello again!"' | ./trre "\":'.+?:-\":'"Kết quả là
'-' '-'Tức là với biểu thức
".+?:-", văn bản bên trong""được thay bằng ký hiệu-, đồng thời các dấu ngoặc kép bao quanh cũng được đổi. Dấu hỏi biểu thị chế độ không tham lamCó vẻ toàn bộ dự án đặt trên luận điểm rằng “biểu thức chính quy là công cụ tuyệt vời để tìm mẫu trong văn bản, nhưng với chỉnh sửa văn bản thì luôn cảm thấy không tự nhiên”, nhưng lại không có ví dụ nào
Tôi không hiểu vì sao biểu thức chính quy lại không tự nhiên cho việc chỉnh sửa. Tôi cũng không biết “chỉnh sửa” ở đây nghĩa là gì, và vì sao mọi người gặp khó khăn với các nhóm
Dự án này có nhiều ví dụ về cú pháp, nhưng tôi không hiểu vì sao nó tốt hơn biểu thức chính quy thông thường. Nếu có vài ví dụ kiểu “phiên bản regex cơ bản là thế này, phiên bản của tôi là thế này, nên nó dễ hơn” thì có lẽ tôi sẽ hiểu được dự án
Chẳng hạn, để chỉ đổi
ynằm giữaxvàzthànhY, trong Python đại khái sẽ làm như sau:pattern = r'(x)y(z)'replacement = r'\1Y\2'result = re.sub(pattern, replacement, text)Tôi muốn thay bằng mẫu
xy:Yz:result = re.trre('xy:Yz', text)Nếu
x,zlà các mẫu phức tạp hơn hoặc bản thân là regex, cách tiếp cận này có thể tiện hơnDự án hay
Mã C đọc thật sự rất thú vị. Rất tốt, tôi đang đọc
Chỉ có một nhận xét nhỏ: liên kết
theory.pdftrong README bị hỏng. PDF nằm trong thư mụcdocs/, nên chỉ cần thêmdocs/vào URL là đượcCó nói rằng dùng
*hoặc+ở phần bên phải có thể gây vòng lặp vô hạn nên cần tránh, nhưng sao không cấm luôn?Tôi hiểu rằng đặc tả cú pháp sẽ khó hơn, nhưng có vẻ không có lý do chính đáng để giữ lại
Lý do ban đầu là tôi định triển khai một phép toán thú vị gọi là hợp thành bộ biến đổi. Có thể thực hiện các phép toán đơn giản trên chuỗi và hợp thành trre như một bộ lọc, nhưng tôi vẫn chưa hoàn thiện. Vì vậy đúng là nhận xét hợp lý
Một hướng khám phá thú vị, nhưng thiếu ví dụ cho thấy vì sao nó thực sự tốt hơn. Tất nhiên cũng có thể do tôi đã quá quen với regex quá lâu
Ví dụ, tôi không thấy
(cat):(dog)trong trre tốt hơns/cat/dogở điểm nào, hay(x:)ortốt hơns/xor/orra sao. Hầu như mọi ví dụ trong đầu tôi đều có thể ánh xạ sang một regex tương đối dễNếu có lợi thế cốt lõi thì có lẽ nằm ở phần logic nhóm, nên ví dụ cũng nên tập trung vào đó. Có lẽ nên giải thích trước vì sao đây là lựa chọn tốt hơn, rồi mới giải thích cú pháp cơ bản
Ví dụ mã Caesar trông rất cần tính năng “áp dụng ngược lại”. Đây là yêu cầu phổ biến trong nhiều phép thay thế văn bản, và trong ví dụ này thì đặc biệt rõ. Cái đầu lập trình viên sẽ lập tức kêu lên: “Tại sao phải biểu diễn cùng một logic hai lần?”
Tôi vẫn chưa biết nó có hữu ích hay không, nhưng việc khám phá các lựa chọn thay thế cho hiện trạng đã tồn tại lâu là rất tuyệt. Thường thì những thử nghiệm như vậy cũng có khả năng lớn là không thành công, nhưng bản thân việc khám phá vẫn rất đáng xem
Đặc tả có vẻ khá thiếu sót. Ngay từ ví dụ đầu tiên đã lạ rồi:
$ echo 'cat' | trre 'c:da:ot:g'dogKhông rõ chuyện gì đang xảy ra ở đây. Cú pháp được viết như sau:
TRRE <- TRRE* TRRE|TRRE TRRE.TRRETRRE <- REGEX REGEX:REGEXCây phân tích cú pháp ở đây là gì? Tại sao
ckhông bị đổi thànhda? Hoặc tại saockhông bị loại bỏ rồidađổi thànhot?Ý tưởng rằng nó có ngữ nghĩa tìm kiếm/thay thế trực quan hơn so với toán tử nhóm thì hay. Thời MS-DOS có thể làm kiểu
ren .log .txtvà nó hoạt động; theo lối nghĩ kiểu bash hiện đại thì vô lý, nhưng nhìn vào là thấy ý định rất rõNếu gọi rõ toán tử đó là
~, ví dụ sẽ trông như thế này:$ echo 'cat' | trre 'c:d~a:o~t:g'dogNếu thêm các dấu ngoặc không cần thiết thì sẽ như sau:
$ echo 'cat' | trre '(c:d)~(a:o)~(t:g)'dogViệc tại sao
ckhông đổi thànhdahoàn toàn là do độ ưu tiên. Nhìn vào cuộc thảo luận này thì có vẻ tôi đã chọn sai độ ưu tiên, và điều đó gây nhầm lẫnBảng độ ưu tiên hiện tại như sau:
| 1 | ký tự escape | \ || 2 | biểu thức ngoặc vuông | [] || 3 | nhóm | () || 4 | lặp ERE cho một ký tự | * + ? {m,n} || 5 | chuyển đổi | : || 6 | nối | . (ngầm định) || 8 | lựa chọn | | |Vì vậy
:liên kết chặt hơn.tức phép nối ngầm địnhNếu thay vào đó yêu cầu regex không được rỗng, ví dụ xóa sẽ hỏng, nhưng tính mơ hồ sẽ chuyển sang phía phép nối. Tức là sẽ mơ hồ giữa
(((c:d)(a:o))(t:g))và((c:d)((a:o)(d:g))). Nếu giả định tính kết hợp thì khác biệt này có lẽ không quan trọngc:d,a:tức là không có gì, vàot:gNhưng đọc lại thì đúng là gây nhầm lẫn, và về mặt lý thuyết, chỉ ra như vậy là hợp lý. Sau khi đọc kho lưu trữ, tôi cũng bắt đầu tin rằng
cnên được đổi thànhda, nhưng không chắc chắn