lechihuy

Học Regex theo kiểu "120"

Hồi mới học lập trình, mình tập tành viết một side-project cho việc lưu các URL video Youtube cho các nhóm người dùng với nhau. Yêu cầu là trước khi cho phép lưu URL, phải kiểm tra URL truyền vào có khớp với định dạng URL video của Youtube hay không?

Thế là mình bắt tay vào nghiên cứu (cụ thể là lục tung trên Stackoverflow) thì tìm ra được đoạn Regex bên dưới:

Regex
/http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?‌​[\w\?‌​=]*)?/

Thoạt nhìn, mình thấy không khác gì đang đọc dãy ký tự thời Ai Cập cổ đại 🙂.

Đây là tôi mỗi khi debug Regex vừa copy trên mạng
Đây là tôi mỗi khi debug Regex vừa copy trên mạng

Lúc đó mình cũng chả thèm buồn quan tâm đến việc nó hoạt động như thế nào, các ký tự thể hiện điều gì? Miễn nó chạy là được mà nhỉ... Dù gì chắc cũng sẽ hiếm gặp lại nó hoặc nếu có gặp thì cứ bỏ chút thời gian "nghiên cứu" là ra ấy mà, thế là mình quyết định ngó lơ nó.

Cho tới một ngày mình gặp một yêu cầu so khớp một định dạng mà mình có "nghiên cứu" cả 2 tiếng đồng hồ vẫn không ra hướng giải quyết (cụ thể là mò đoạn Regex dọn sẵn trên Stackoverflow). Lúc này mình mới bắt đầu nghi ngờ nhân sinh, chẳng lẽ không có ai gặp trường hợp giống mình, hay cái trường hợp của mình nó dị hợm quá? Vì quá cay cú, thế là mình quyết định bắt đầu tìm hiểu Regex (lần này là nghiêm túc tìm hiểu thật).

Regex là gì mà sao "khó nuốt" như vậy?

Hồi xưa đi học thì thầy cô hay dạy mình là học gì cũng phải học định nghĩa, khái niệm của thứ đó trước. Thế là sau khi tìm đọc một số định nghĩa khá "khó nuốt" về Regex thì mình cũng tự diễn giải một định nghĩa theo cách hiểu của mình như sau:

Khái niệm

Regex (hay RegEx hay RegExr) là một chuỗi các ký tự được quy ước trước để tạo nên các khuôn mẫu (pattern). Những pattern này được sử dụng để so khớp, phân tách, tìm kiếm hoặc thay thế chuỗi.

(đọc lại thì thấy cũng không dễ hiểu hơn là mấy 🙂)

Rồi, sau khi học khái niệm xong thì chúng ta tới bước tìm hiểu công thức, mình cũng hì hụi coi các kiểu Regex "trên trời dưới đất" rồi rút ra được cấu trúc tổng quát như sau:

Cấu trúc của Regex
Cấu trúc của Regex
Trong đó
  • Regex pattern được bọc trong cặp / (dấu gạch chéo). * Biểu tượng cờ (⚑) đại diện cho các Regex flag (hay còn gọi là Regex modifier), nhằm để nâng cao chế độ tìm kiếm cho Regex. Các flag này là tùy chọn, hoặc có thể có một hoặc kết hợp nhiều flag với nhau.

Sau khi định hình được cấu trúc cũng như thành phần của một Regex, mình bắt đầu chuyển sang tới bước "khó nhai" nhất, đó chính là hiểu được ý nghĩa của các ký tự "Hi Lạp cổ" được quy ước trong Regex 🥲. Ban đầu mình vẫn ngây thơ lắm, cứ nghĩ chịu khó đọc cách dùng rồi học thuộc cách sử dụng của "tụi" ký tự này thì áp dụng làm được ngay ấy mà...

NHƯNG KHÔNG, người tính không bằng trời tính 🙂!

Ban đầu, mình vào trang MDN để đọc qua các định nghĩa sặc mùi "học thuật" về các ký hiệu trong Regex nhưng thật sự không thể vào đầu một thằng chưa biết gì về Regex như mình. Thế là mình nghĩ nên giảm độ "ảo tưởng" về vốn tiếng Anh và tìm kiếm các trang hướng dẫn về Regex bằng tiếng Việt. Thoạt đầu, vì là tiếng mẹ đẻ nên đọc hiểu cũng dễ hơn, nhưng càng về sau mình vẫn không sao áp dụng được những gì đã học... Một phần có lẽ danh sách các ký hiệu Regex còn dài hơn cả con đường Võ Văn Kiệt ở Sài Gòn hoặc có khi tại mình "ngu" quá 🥲?

Thế rồi "cái khó ló cái khôn", mình lại bắt đầu nãy ra mấy cái ý tưởng ngược đời: "Tại sao không từ nhu cầu thực tiễn rồi tìm kiếm cần học những gì để làm được điều đó?".

Học Regex theo kiểu 120

Mình gọi cách học này là học Regex theo kiểu 120 (đọc là "one-to-zero"), tức là từ những thứ "đã có gì" để học những thứ "chưa là gì". Với cách học này, mình sẽ tìm kiếm những nhu cầu thực tiễn để tìm ra cách giải quyết nó bằng Regex. Thế là mình đã đặt ra các yêu cầu từ dễ đến khó và cố gắng giải quyết chúng...

Yêu cầu #1: Tìm kiếm chữ cái "i" đầu tiên trong chuỗi bất kỳ

Yêu cầu đầu tiên này mình quyết định để ở mức độ dễ nhất. Với yêu cầu này thì mình cũng không cần tìm hiểu gì nhiều và đã nhanh chóng giải quyết nó bằng Regex bên dưới.

Regex
/i/
Test string
1 match
K
i
m
o
c
h
i
space
enter
tab
Góc "khoe mẽ"

Regex Matcher (mình đã đặt tên cho widget ở trên là như vậy 🙂) là do mình tự viết nhằm phục vụ cho các bài viết liên quan đến Regex. Nó sẽ nhận đầu vào là một Regex và một đoạn chuỗi kiểm thử.

js
<Regex pattern={/i/} testString={"Kimochi"} />

Nhiệm vụ của nó là so khớp đoạn chuỗi kiểm thử với Regex được cung cấp, và phần đầu ra sẽ highlight các kết quả so khớp (hay gọi là match), kèm với đó là hiển thị số lượng match.

Với ví dụ trên thì chữ "i" tìm thấy đầu tiên đã được highlight, tương đương với số lượng match là 1.

Yêu cầu #2: Tìm kiếm tất cả chữ cái "i" có trong chuỗi bất kỳ

Khi nghĩ ra yêu cầu này thì mình tự nhiên "nghi ngờ nhân sinh"... Tại sao ở yêu cầu #1, chuỗi "Kimochi" có 2 chữ "i" nhưng sao kết quả chỉ lấy chữ "g" đầu tiên (chắc không phải do Regex Matcher mình viết bị bug đâu nhỉ 🙂?).

Sau một hồi "hỏi thăm" anh Google, anh Stackoverflow và những "ông anh" khác thì mình đã hiểu vấn đề.

Kiến thức

Regex mặc định chỉ trả về match đầu tiên, để có thể trả về tất cả các match, ta cần thêm Regex flag g (viết tắt của global).

OK, vậy để giải quyết yêu cầu #2 này mình chỉ cần thêm Regex flag g như sau:

Regex
/i/g
Test string
2 matches
K
i
m
o
c
h
i
space
enter
tab

Lần này thì nó đã lấy hết tất cả chữ "i" có trong chuỗi "Kimochi" rồi.

Có vẻ nãy giờ hơi thuận lợi, mình cảm thấy hơi "dễ dãi" với bản thân nên chắc sẽ tăng độ khó cho yêu cầu tiếp theo lên một chút.

Yêu cầu #3: Tìm kiếm tất cả chữ số có trong chuỗi bất kỳ

Với yêu cầu một ký tự nhất định thì mình đã dễ dàng làm được rồi, vậy với chữ số (bao gồm các chữ số từ 0-9) thì làm như thế nào nhỉ? Thôi mình cứ viết bừa xem sao.

Regex
/0123456789/g
Test string
0 match
N
g
à
y
 
1
0
 
t
h
á
n
g
 
0
6
 
n
ă
m
 
2
0
0
1
space
enter
tab

Chậc 😒! Mình cứ nghĩ một "phát" ăn ngay nhưng không được thật, chẳng có cái match nào xuất hiện cả.

Thế rồi mình lại lao đầu vào tìm kiếm, nào là "how to match only number with regex", "regex only number"... thì sau đó mình "giác ngộ" được rằng:

Kiến thức

Sử dụng cặp [...] (thuộc nhóm character class ) để so khớp bất kỳ ký tự nào trong tập hợp ký tự được khai báo trong nó.

Giờ việc mình cần làm chỉ là bọc các chữ số kia trong cặp [] là xong việc.

Regex
/[0123456789]/g
Test string
8 matches
N
g
à
y
 
1
0
 
t
h
á
n
g
 
0
6
 
n
ă
m
 
2
0
0
1
space
enter
tab

Nhìn cái Regex dài ngoằn cũng hơi "phèn", thế là mình tìm hiểu và biết có thể rút gọn nó bằng ký tự - như này:

Regex
/[0-9]/g
Test string
8 matches
N
g
à
y
 
1
0
 
t
h
á
n
g
 
0
6
 
n
ă
m
 
2
0
0
1
space
enter
tab

hoặc bằng ký tự \d (tương đương với [0-9]) như này:

Regex
/[\d]/g
Test string
8 matches
N
g
à
y
 
1
0
 
t
h
á
n
g
 
0
6
 
n
ă
m
 
2
0
0
1
space
enter
tab

Uiss! Tự nhiên lúc này cảm thấy mình hơi bị "master" rồi đó. Khoan, sao kết quả có tới 8 match dữ vậy? Đúng ra là chỉ nên có 3 match là "10", "06" và "2001" thôi chứ 🙂.

Sau một hồi tìm kiếm thì mình biết rằng [] chỉ match tối đa 1 ký tự, thêm vào đó là có ký tự +, nó sẽ giúp ta giải quyết vấn đề này.

Kiến thức

Ký tự + (thuộc nhóm quantifier ) sẽ lặp lại biểu thức (hoặc có thể gọi là atom hay token) đặt trước nó ít nhất một lần và không có giới hạn.

Như vậy để có thể đạt được kết quả như mong muốn, mình chỉ cần chỉnh lại Regex như sau:

Regex
/[\d]+/g
Test string
3 matches
N
g
à
y
 
1
0
 
t
h
á
n
g
 
0
6
 
n
ă
m
 
2
0
0
1
space
enter
tab

Chà! Yêu cầu này tiếp thu được khá nhiều kiến thức mới nhỉ?

Yêu cầu #4: Tìm kiếm tất cả địa chỉ Gmail hợp lệ trong danh sách bất kỳ

Đầu tiên mình sẽ phân tích cấu trúc của một địa chỉ Gmail. Theo Google, nó sẽ có dạng username@gmail.com, trong đó username là các ký tự từ a-z, 0-9 và dấu chấm (.), giới hạn từ 6-30 ký tự.

Đây là danh sách địa chỉ e-mail đầu vào trong yêu cầu này.

text
<p>johndoe@gmail.com</p>
lisa@gmail.com
dennie@outlook.com
lechihuy@gmail.com

Yêu cầu đưa ra là tìm kiếm dòng khớp chính với định dạng của địa chỉ Gmail, tức ở danh sách trên chỉ có dòng thứ 2 và thứ 4 là thỏa mãn.

Mình thử áp dụng các kiến thức vừa "nạp" được từ các yêu cầu trước và đưa ra phiên bản Regex đầu tiên.

Regex
/[a-z0-9\.\-]+@gmail.com/g
Test string
3 matches
<
p
>
j
o
h
n
d
o
e
@
g
m
a
i
l
.
c
o
m
<
/
p
>
­

l
i
s
a
@
g
m
a
i
l
.
c
o
m
­

d
e
n
n
i
e
@
o
u
t
l
o
o
k
.
c
o
m
­

l
e
c
h
i
h
u
y
@
g
m
a
i
l
.
c
o
m
space
enter
tab

Có vẻ như tại dòng 1, dù không thỏa với yêu cầu đưa do được bọc bởi cặp thẻ <p></p> nhưng vẫn được match. Lý do tại sao nhỉ? Ngồi ngẫm nghĩ một hồi lâu, mình thấy không phải Regex trên sai mà có lẽ chưa đủ ràng buộc. Thứ mình thiếu ở đây có thể là ràng buộc Regex nên kiểm tra từ điểm đầu đến điểm cuối của mỗi dòng.

Mình loay hoay tìm kiếm trên mạng thì tìm được cặp ký hiệu ^...$.

Kiến thức

Cặp ký hiệu ^...$ (còn được gọi là input boundary assertion ) sẽ kiểm tra tại điểm bắt đầu và điểm kết thúc của chuỗi kiểm thử, nếu có thêm Regex flag m (multi line), sẽ kiểm tra tại điểm đầu và điểm cuối của mỗi dòng.

Có vẻ với hai thông tin này mình sẽ dễ dàng giải quyết yêu cầu này. Giờ thì áp dụng thử xem sao...

Regex
/^[a-z0-9\.\-]+@gmail.com$/gm
Test string
2 matches
<
p
>
j
o
h
n
d
o
e
@
g
m
a
i
l
.
c
o
m
<
/
p
>
­

l
i
s
a
@
g
m
a
i
l
.
c
o
m
­

d
e
n
n
i
e
@
o
u
t
l
o
o
k
.
c
o
m
­

l
e
c
h
i
h
u
y
@
g
m
a
i
l
.
c
o
m
space
enter
tab

Bingo! Kết quả hoàn toàn như mong đợi 😀.

Yêu cầu 5: Lấy URL và nội dung của thẻ liên kết trong đoạn HTML cho trước

Đến yêu cầu này có vẻ hơi khó rồi, nhưng trường hợp này cũng khá phổ biến trong phát triển phần mềm. Yêu cầu đề ra là từ đoạn HTML cho trước, dùng Regex để tìm kiếm các thẻ a và lấy các thông tin gồm URL và nội dung văn bản của liên kết đó.

html
<ul>
<li>Facebook: <a href="https://fb.com/lch106" class="link">lch106</a></li>
<li>
Github: <a href="https://github.com/lechihuy" class="link">lechihuy</a>
</li>
</ul>

Chẳng hạn với đoạn HTML trên, ta sẽ tìm kiếm trước các cặp thẻ a, sau đó lấy URL trong thuộc tính href và nội dung văn bản ở giữa cặp thẻ a ở mỗi thẻ tìm được. Như các bạn đã biết, các URL và nội dung văn bản liên kết đều là các giá trị không xác định, có thể chứa bất kỳ ký tự nào, có thể có hoặc không.

Vậy Regex có các ký tự nào thõa mãn hai điều kiện trên không?

Sau một hồi tìm kiếm, mình đã tìm được hướng giải quyết bằng cách kết hợp hai ký tự .* với nhau.

Kiến thức

Ký tự . (hay gọi là wildcard ) sẽ so khớp bất kỳ ký tự nào ngoài trừ ký tự xuống dòng. Ký tự * (thuộc nhóm quantifier ) sẽ lặp lại biểu thức (hoặc có thể gọi là atom hay token) đặt trước nó ít nhất 0 lần và không có giới hạn.

Vậy khi kết hợp hai ký tự này lại như thế này .* thì ý nghĩa của nó là so khớp bất cứ ký tự nào ngoại trừ ký tự xuống dòng ít nhất 0 lần và không có giới hạn.

Ngoài ra, vì cặp thẻ <a></a> có chứa ký tự gạch chéo (/), ký tự này thuộc nhóm ký tự đặc biệt trong regex. Vì vậy để không bị lỗi cú pháp khi sử dụng nó, mình cần phải "escape" bằng cách thêm ký tự \ trước nó.

Mình quyết định áp dụng các thông tin trên và viết ngay và luôn Regex đầu tiên.

Regex
/<a href=".*">.*<\/a>/g
Test string
1 match
<
u
l
>
<
l
i
>
F
a
c
e
b
o
o
k
:
 
<
a
 
h
r
e
f
=
"
h
t
t
p
s
:
/
/
f
b
.
c
o
m
/
l
c
h
1
0
6
"
 
c
l
a
s
s
=
"
l
i
n
k
"
>
l
c
h
1
0
6
<
/
a
>
<
/
l
i
>
<
l
i
>
G
i
t
h
u
b
:
 
<
a
 
h
r
e
f
=
"
h
t
t
p
s
:
/
/
g
i
t
h
u
b
.
c
o
m
/
l
e
c
h
i
h
u
y
"
 
c
l
a
s
s
=
"
l
i
n
k
"
>
l
e
c
h
i
h
u
y
<
/
a
>
<
/
l
i
>
<
/
u
l
>
space
enter
tab

Có vẻ như kết quả không như mong đợi... Để dễ quan sát hơn, mình sẽ bọc các token .* vào cặp (), nó gọi là capturing group trong Regex. Các group này sẽ giúp ta lưu trữ lại các giá trị so khớp được thuộc phạm vi của token đó và có thể dùng để tham chiếu.

Regex
/<a href="(.*)">(.*)<\/a>/g
Test string
1 match
<
u
l
>
<
l
i
>
F
a
c
e
b
o
o
k
:
 
<
a
 
h
r
e
f
=
"
h
t
t
p
s
:
/
/
f
b
.
c
o
m
/
l
c
h
1
0
6
"
 
c
l
a
s
s
=
"
l
i
n
k
"
>
l
c
h
1
0
6
<
/
a
>
<
/
l
i
>
<
l
i
>
G
i
t
h
u
b
:
 
<
a
 
h
r
e
f
=
"
h
t
t
p
s
:
/
/
g
i
t
h
u
b
.
c
o
m
/
l
e
c
h
i
h
u
y
"
 
c
l
a
s
s
=
"
l
i
n
k
"
>
l
e
c
h
i
h
u
y
<
/
a
>
<
/
l
i
>
<
/
u
l
>
space
enter
tab
Góc "khoe mẽ"

Regex Matcher của mình sẽ highlight các capturing group bằng những mã màu khác nhau, cặp () trong Regex cũng sẽ được highlight với mã màu tương ứng để dễ dàng nhận biết 🧙‍♀️.

Như những gì kết quả hiển thị, có lẽ token .* (đại diện cho URL) không biết nên dừng khi gặp ký tự nháy đôi đóng (") đầu tiên mà nó cứ tiếp tục so khớp cho tới khi gặp ký tự nháy đôi đóng (") cuối cùng, dẫn tới kết quả không chính xác. Vậy làm thế nào để quy ước được điều đó?

Sau một hồi tìm kiếm, hết bằng tiếng "Tây" rồi qua tiếng "Ta", cuối cùng mình đã tìm thấy được cặp thuộc tính đặc biệt trong Regex, đó là greedy và lazy 🙂.

Kiến thức

Mặc định, Regex sẽ hoạt động theo cơ chế greedy (tạm dịch là tham lam), nó sẽ so khớp nhiều ký tự nhất có thể. Còn đối với lazy (hay có thể gọi là non-greedy), nó sẽ so khớp ít ký tự nhất có thể. Để có thể thiết lập cơ chế lazy, ta sử dụng ký tự ? đằng sau các ký tự quantifier.

OK, thử sửa lại Regex một xíu nào!

Regex
/<a href="(.*?)">(.*?)<\/a>/g
Test string
2 matches
<
u
l
>
<
l
i
>
F
a
c
e
b
o
o
k
:
 
<
a
 
h
r
e
f
=
"
h
t
t
p
s
:
/
/
f
b
.
c
o
m
/
l
c
h
1
0
6
"
 
c
l
a
s
s
=
"
l
i
n
k
"
>
l
c
h
1
0
6
<
/
a
>
<
/
l
i
>
<
l
i
>
G
i
t
h
u
b
:
 
<
a
 
h
r
e
f
=
"
h
t
t
p
s
:
/
/
g
i
t
h
u
b
.
c
o
m
/
l
e
c
h
i
h
u
y
"
 
c
l
a
s
s
=
"
l
i
n
k
"
>
l
e
c
h
i
h
u
y
<
/
a
>
<
/
l
i
>
<
/
u
l
>
space
enter
tab

Hoạt động rồi! Mình sẽ lấy group 1 để giải thích cách mà lazy hoạt động. Khi token .* so khớp các ký tự, thì lazy sẽ "nhắc" với .* rằng cần dừng lại khi gặp ký tự nằm bên phải của ký tự ?, cụ thể trường hợp này là dấu ".

Cơ chế lazy trong Regex

Giờ có thể "gáy" được chưa các bạn 😁? Mình đùa thôi, dù Regex hiện tại đã phân tách được các thẻ a với nhau và lấy được nội dung văn bản liên kết, nhưng có lẽ việc lấy URL vẫn chưa hoạt động được 🥲.

Có vẻ như đoạn token dùng để tìm kiếm giá trị thuộc tính href của mình vẫn hơi "cứng nhắc". Hiện tại nó đang ràng buộc thuộc tính href phải đứng sau thẻ a và cách nhau bằng một dấu khoảng trắng. Vì vậy mà Regex buộc phải tìm kiếm tới dấu nháy đôi đóng (") cuối cùng trong thẻ a thì mới thỏa mãn. Nhưng trong thực tế, đôi khi một thẻ a sẽ có nhiều thuộc tính hoặc thuộc tính href có thể nằm ở bất kỳ vị trí nào. Vậy để linh hoạt hơn, mình sẽ "nâng cấp" Regex một chút xíu.

Regex
/<a.+?href="(.*?)".*?>(.*?)<\/a>/g
Test string
2 matches
<
u
l
>
<
l
i
>
F
a
c
e
b
o
o
k
:
 
<
a
 
h
r
e
f
=
"
h
t
t
p
s
:
/
/
f
b
.
c
o
m
/
l
c
h
1
0
6
"
 
c
l
a
s
s
=
"
l
i
n
k
"
>
l
c
h
1
0
6
<
/
a
>
<
/
l
i
>
<
l
i
>
G
i
t
h
u
b
:
 
<
a
 
h
r
e
f
=
"
h
t
t
p
s
:
/
/
g
i
t
h
u
b
.
c
o
m
/
l
e
c
h
i
h
u
y
"
 
c
l
a
s
s
=
"
l
i
n
k
"
>
l
e
c
h
i
h
u
y
<
/
a
>
<
/
l
i
>
<
/
u
l
>
space
enter
tab

Hiện tại Regex trên đã thỏa mãn các yêu cầu đưa ra. Tuy nhiên, giả sử trường hợp giá trị của thuộc tính href được bọc bởi cặp '' thì sao? Chắc chắn Regex trên sẽ không thỏa mãn.

Regex
/<a.+?href="(.*?)".*?>(.*?)<\/a>/g
Test string
0 match
<
u
l
>
<
l
i
>
F
a
c
e
b
o
o
k
:
 
<
a
 
h
r
e
f
=
'
h
t
t
p
s
:
/
/
f
b
.
c
o
m
/
l
c
h
1
0
6
'
 
c
l
a
s
s
=
'
l
i
n
k
'
>
l
c
h
1
0
6
<
/
a
>
<
/
l
i
>
<
l
i
>
G
i
t
h
u
b
:
 
<
a
 
h
r
e
f
=
'
h
t
t
p
s
:
/
/
g
i
t
h
u
b
.
c
o
m
/
l
e
c
h
i
h
u
y
'
 
c
l
a
s
s
=
'
l
i
n
k
'
>
l
e
c
h
i
h
u
y
<
/
a
>
<
/
l
i
>
<
/
u
l
>
space
enter
tab

Vậy để có thể lựa chọn thay thế giữa ký tự ' hoặc ký tự " trong Regex, ta có thể sử dụng ký tự | (hay còn gọi là disjunction ). Mình sẽ áp dụng nó vào Regex của mình như sau:

Regex
/<a.+?href=('|")(.*?)('|").*?>(.*?)<\/a>/g
Test string
2 matches
<
u
l
>
<
l
i
>
F
a
c
e
b
o
o
k
:
 
<
a
 
h
r
e
f
=
'
h
t
t
p
s
:
/
/
f
b
.
c
o
m
/
l
c
h
1
0
6
'
 
c
l
a
s
s
=
'
l
i
n
k
'
>
l
c
h
1
0
6
<
/
a
>
<
/
l
i
>
<
l
i
>
G
i
t
h
u
b
:
 
<
a
 
h
r
e
f
=
'
h
t
t
p
s
:
/
/
g
i
t
h
u
b
.
c
o
m
/
l
e
c
h
i
h
u
y
'
 
c
l
a
s
s
=
'
l
i
n
k
'
>
l
e
c
h
i
h
u
y
<
/
a
>
<
/
l
i
>
<
/
u
l
>
space
enter
tab

Có vẻ nó đã hoạt động, nhưng chắc chỉ là có vẻ thôi 🙂, vì một "thứ" đập vào mắt mình chính là màu sắc highlight hai group ('|") là những mã màu khác nhau. Mình sinh nghi rằng nếu mình viết HTML "râu ông này cắm cầm bà kia" kiểu

html
<a href='https://google.com">...</a>

thì chắc sẽ lỗi quá 🙂. Thôi thì cứ thử kiểm tra xem sao...

Regex
/<a.+?href=('|")(.*?)('|").*?>(.*?)<\/a>/g
Test string
2 matches
<
u
l
>
<
l
i
>
F
a
c
e
b
o
o
k
:
 
<
a
 
h
r
e
f
=
'
h
t
t
p
s
:
/
/
f
b
.
c
o
m
/
l
c
h
1
0
6
"
 
c
l
a
s
s
=
'
l
i
n
k
'
>
l
c
h
1
0
6
<
/
a
>
<
/
l
i
>
<
l
i
>
G
i
t
h
u
b
:
 
<
a
 
h
r
e
f
=
'
h
t
t
p
s
:
/
/
g
i
t
h
u
b
.
c
o
m
/
l
e
c
h
i
h
u
y
"
 
c
l
a
s
s
=
'
l
i
n
k
'
>
l
e
c
h
i
h
u
y
<
/
a
>
<
/
l
i
>
<
/
u
l
>
space
enter
tab

Chắc mình nghĩ nên chuyển sang làm nghề bói toán thay vì phải tiếp tục viết bài này ...

Nguyên nhân vụ này mình đã nắm được rồi, bản chất do mình quy ước hai group khác nhau nên chúng không ràng buộc lẫn nhau, mạnh "ông" nào làm phần của "ông" đó. Ngồi ngẫm nghĩ một hồi thì chợt nhớ về thuật ngữ "tham chiếu" trong ngôn ngữ lập trình. Vậy Regex có hỗ trợ nó không nhỉ?

"regex reference group", tra cứu một "phát" ăn luôn...

Kiến thức

Ký tự \n (hay còn gọi là backreference ) được dùng để tham chiếu ngược cách so khớp của capturing group, với n là một số dương tương ứng với vị trí của capturing group muốn tham chiếu ngược, vị trí group đầu tiên bắt đầu từ 1.

Chắc đây sẽ là phiên bản Regex cuối cùng để giải quyết yêu cầu này.

Regex
/<a.+?href=('|")(.*?)\1.*?>(.*?)<\/a>/g
Test string
2 matches
<
u
l
>
<
l
i
>
F
a
c
e
b
o
o
k
:
 
<
a
 
h
r
e
f
=
'
h
t
t
p
s
:
/
/
f
b
.
c
o
m
/
l
c
h
1
0
6
"
 
c
l
a
s
s
=
'
l
i
n
k
'
>
l
c
h
1
0
6
<
/
a
>
<
/
l
i
>
<
l
i
>
G
i
t
h
u
b
:
 
<
a
 
h
r
e
f
=
'
h
t
t
p
s
:
/
/
g
i
t
h
u
b
.
c
o
m
/
l
e
c
h
i
h
u
y
"
 
c
l
a
s
s
=
'
l
i
n
k
'
>
l
e
c
h
i
h
u
y
<
/
a
>
<
/
l
i
>
<
/
u
l
>
space
enter
tab

Dù giá trị URL trả về sai nhưng nó đã hoạt động theo đúng cách mà HTML phân tích cú pháp.

Phù!! Cuối cùng cũng thỏa mãn được yêu cầu này... (hơi cảm thấy "master" cùng với yêu cầu #5 🙂)

Yêu cầu 6: Tìm kiếm mệnh giá Việt Nam đồng trong chuỗi bất kỳ

Vì là yêu cầu cuối cùng trong bài này nên mình nghĩ chắc va chạm "tiền bạc" xíu cho nó có hứng thú 🤑. Với yêu cầu này, từ một chuỗi văn bản bất kỳ, tìm kiếm các mệnh giá Việt Nam đồng.

Chẳng hạn mình có đoạn chuỗi kiểm thử bên dưới:

plaintext
Mẹ cho Huy 25000đ để ra tiệm vàng đổi lấy $1. Vậy nếu mẹ đưa 2500000đ thì đổi Huy sẽ đổi được bao nhiêu tiền đô la Mỹ?

Giờ mình muốn lấy các giá trị 25000 và 2500000 trên đoạn chuỗi đó thì phải làm sao đây? Thử trước bản Regex đầu tiên rồi phân tích tiếp vậy 🙂.

Regex
/[\d]+đ/g
Test string
2 matches
M
 
c
h
o
 
H
u
y
 
2
5
0
0
0
đ
 
đ
 
r
a
 
t
i
m
 
v
à
n
g
 
đ
i
 
l
y
 
$
1
.
 
V
y
 
n
ế
u
 
m
 
đ
ư
a
 
2
5
0
0
0
0
0
đ
 
t
h
ì
 
đ
i
 
H
u
y
 
s
 
đ
i
 
đ
ư
c
 
b
a
o
 
n
h
i
ê
u
 
t
i
n
 
đ
ô
 
l
a
 
M
?
space
enter
tab

Trải qua vô số các yêu cầu phức tạp (cụ thể là 5 yêu cầu) thì tới bây giờ mình cũng đã tự giải quyết một yêu cầu khá nhanh chóng 😎.

Ụa mà khoan, hình như vẫn có gì đó không đúng ở đây? Regex Matcher của mình match luôn cả ký tự "đ" phía sau mệnh giá. Hmm... thì tất nhiên là phải vậy rồi, vì nó được khai báo trong Regex mà. Vậy có cách nào có thể so khớp các chữ số nằm trước ký tự "đ" mà không bao gồm nó được không nhỉ 🤔?

Vâng, "cứu tinh" của chúng ta đây rồi, ngồi mò mãi mình mới tìm ra được ký tự này...

Kiến thức

Ký tự (?=...) (thuộc nhóm lookahead assertion ) được dùng so khớp với Regex pattern chứa bên trong nó nhưng sẽ không trả về match ở đầu ra.

Vận dụng ký tự trên vào Regex thì ta có được kết quả như sau:

Regex
/[\d]+(?=đ)/g
Test string
2 matches
M
 
c
h
o
 
H
u
y
 
2
5
0
0
0
đ
 
đ
 
r
a
 
t
i
m
 
v
à
n
g
 
đ
i
 
l
y
 
$
1
.
 
V
y
 
n
ế
u
 
m
 
đ
ư
a
 
2
5
0
0
0
0
0
đ
 
t
h
ì
 
đ
i
 
H
u
y
 
s
 
đ
i
 
đ
ư
c
 
b
a
o
 
n
h
i
ê
u
 
t
i
n
 
đ
ô
 
l
a
 
M
?
space
enter
tab

Lời kết

Sau vài ngày ngồi mày mò và "giải ngược" từ 6 yêu cầu trên thì mình cảm thấy như được "unlock skill" Regex. Đối với bản thân mình lúc đó, Regex không còn là điều xa lạ và đáng sợ nữa. Mình bắt đầu học Regex một cách chủ động hơn, tìm kiếm thêm các ký tự khác để nâng cao kỹ năng của mình. Các bạn cũng có thể tham khảo trang MDN để có thể "bỏ túi" cho mình thêm nhiều ký tự Regex hay ho (vì các ký tự mình sử dụng trong bài này chỉ là một phần nhỏ thôi 🙂).

Và giờ các bạn có thể thử kéo lên trên đầu bài viết và đọc lại đoạn Regex "Hi Lạp cổ đại", chiêm nghiệp và xem bản thân mình hiểu được bao nhiêu phần trăm nhé!

Hẹn gặp lại 👋.

© lechihuy.dev làm với