Nhờ chỉnh code loại trùng text dùng REGEXP (1 người xem)

Liên hệ QC

Người dùng đang xem chủ đề này

eke_rula

Thành viên tích cực
Tham gia
12/11/16
Bài viết
1,076
Được thích
1,245
Em có đoạn code:
PHP:
Sub tachtrung2()
    Dim i As Long, j As Long, text As String, text2 As String, text3 As String
    text3 = "Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;2017;2017;dien;EXCEL;2017"
    text = Replace(";" & text3 & ";", ";", "   ")
    With CreateObject("vbscript.regexp")
        .Global = True
        .ignorecase = True
        .Pattern = "((\s\w+\s).+)\2"
        Do While .test(text)
            text = .Replace(text, "$1 ")
            i = 0
            For Each subl In .Execute(text)
                If InStr(text2, Trim(.Execute(text).Item(i).submatches(1))) = 0 Then
                    text2 = text2 & " | " & Trim(.Execute(text).Item(i).submatches(1))
                End If
                i = i + 1
            Next
        Loop
    End With
    MsgBox ("- " & Application.Trim(text) + ChrW(10) & "- " & text2)
End Sub
Kết quả của đoạn code trên tạo ra là chuỗi sau khi loại trùng hết (text) và liệt kê các chuỗi text bị trùng (text2)
- Sau khi chay code được text2, nhưng đoạn text2 lúc nào cũng có dấu "|" ở đầu và cuối, em có thể dùng Application.SUBSTITUTE để loại dấu "|" đầu và cuối, nhưng trong VBA có hàm replace , em đã thử replace(text,text2,"",,1) nhưng không được, cho em hỏi là em có thể dùng replace trong trường hợp này được không?
- Kết quả text2="| Dien | dan | 2017 | phap | Dan | excel" , bị trùng chữ "dan" và "Dan", em đã dùng Instr để xác định xem xuất hiện trong chuỗi không nhưng sao kết quả text2 vẫn có trùng, nhờ các anh/chị xem và chỉnh code dùm em.
Em cám ơn!!
 
Lần chỉnh sửa cuối:
VBScript không phải là cong cụ chuyên text.
Muốn chơi Regex thì tìm mấy cái diễn đàn về Perl. Tuy nhiên, nếu bạn vào đó thì sẽ thấy chính họ cũng nhìn nhận Regex không phải là công cụ tốt để làm vệc này.
 
Upvote 0
VBScript không phải là cong cụ chuyên text.
Muốn chơi Regex thì tìm mấy cái diễn đàn về Perl. Tuy nhiên, nếu bạn vào đó thì sẽ thấy chính họ cũng nhìn nhận Regex không phải là công cụ tốt để làm vệc này.
Em đang nghiên cứu về Regexp,nên thấy nó xử lý tốt về text, tách những cái mình muốn được trong chuỗi text phức tạp, miễn sao viết được cái pattern hợp lý thì vấn đề trở nên rất đơn giản. Anh có thể giới thiệu cho em vài cách xử lý text khác trong VBA được không ạ, em cám ơn anh!!!
 
Upvote 0
Em đang nghiên cứu về Regexp,nên thấy nó xử lý tốt về text, tách những cái mình muốn được trong chuỗi text phức tạp, miễn sao viết được cái pattern hợp lý thì vấn đề trở nên rất đơn giản. Anh có thể giới thiệu cho em vài cách xử lý text khác trong VBA được không ạ, em cám ơn anh!!!
Chà dạo này thấy Anh nghiên cứu nhiều hè. Chắc Anh tính làm lớn đây.
 
Upvote 0
Em đang nghiên cứu về Regexp,nên thấy nó xử lý tốt về text, tách những cái mình muốn được trong chuỗi text phức tạp, miễn sao viết được cái pattern hợp lý thì vấn đề trở nên rất đơn giản. Anh có thể giới thiệu cho em vài cách xử lý text khác trong VBA được không ạ, em cám ơn anh!!!

Cỗ máy Regex lấy từ VBScript. Vì là script cho nên MS chế nó theo mẫu của JavaScript. Có một số giới hạn. Điển hình là nó khong có tính năng "dòm ngược".
Nếu muốn vọc cái này thì chơi trong JavaScript đã hơn. Ngôn ngữ này cho phép nối kết quả (piping) cho nên mấy cái lệnh đi vòng giải quyết rất gọn.
 
Upvote 0
Em có đoạn code:
PHP:
Sub tachtrung2()
    Dim i As Long, j As Long, text As String, text2 As String, text3 As String
    text3 = "Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;2017;2017;dien;EXCEL;2017"
    text = Replace(";" & text3 & ";", ";", "   ")
    With CreateObject("vbscript.regexp")
        .Global = True
        .ignorecase = True
        .Pattern = "((\s\w+\s).+)\2"
        Do While .test(text)
            text = .Replace(text, "$1 ")
            i = 0
            For Each subl In .Execute(text)
                If InStr(text2, Trim(.Execute(text).Item(i).submatches(1))) = 0 Then
                    text2 = text2 & " | " & Trim(.Execute(text).Item(i).submatches(1))
                End If
                i = i + 1
            Next
        Loop
    End With
    MsgBox ("- " & Application.Trim(text) + ChrW(10) & "- " & text2)
End Sub
Kết quả của đoạn code trên tạo ra là chuỗi sau khi loại trùng hết (text) và liệt kê các chuỗi text bị trùng (text2)
- Sau khi chay code được text2, nhưng đoạn text2 lúc nào cũng có dấu "|" ở đầu và cuối, em có thể dùng Application.SUBSTITUTE để loại dấu "|" đầu và cuối, nhưng trong VBA có hàm replace , em đã thử replace(text,text2,"",,1) nhưng không được, cho em hỏi là em có thể dùng replace trong trường hợp này được không?
- Kết quả text2="| Dien | dan | 2017 | phap | Dan | excel" , bị trùng chữ "dan" và "Dan", em đã dùng Instr để xác định xem xuất hiện trong chuỗi không nhưng sao kết quả text2 vẫn có trùng, nhờ các anh/chị xem và chỉnh code dùm em.
Em cám ơn!!
Quan điểm của tôi: Nếu dùng RegExp mà bỏ được vòng lập (gọn hơn cách thông thường) thì tôi sẽ dùng. Bằng ngược lại, nó dài dòng hơn thì thôi.. nghỉ xài
Bài này cứ dùng InStr bình thường cho dễ hiểu
 
Upvote 0
Quan điểm của tôi: Nếu dùng RegExp mà bỏ được vòng lập (gọn hơn cách thông thường) thì tôi sẽ dùng. Bằng ngược lại, nó dài dòng hơn thì thôi.. nghỉ xài
Bài này cứ dùng InStr bình thường cho dễ hiểu

Bạn này muốn thử Regex chứ không hẳn là muốn giải đề bài.
Chỉ rất tiếc là cái đề tài bạn ấy chọn lại đúng cái chỗ nhược của Regex.
 
Upvote 0
Quan điểm của tôi: Nếu dùng RegExp mà bỏ được vòng lập (gọn hơn cách thông thường) thì tôi sẽ dùng. Bằng ngược lại, nó dài dòng hơn thì thôi.. nghỉ xài
Bài này cứ dùng InStr bình thường cho dễ hiểu
Em chỉ muốn nghiên cứu về Reg thôi anh ạ, làm mấy bài như tách số điện thoại, tách email , hay tách một đoạn nào đấy trong một chuỗi phức tạp thì phải công thức rất phức tạp hay phải dùng SUBSTITUTE rất nhiều lần, như anh nói bài này có nhiều cách , dùng split kết hợp với Dictionary, hay split và instr thì đơn giản hơn ạ, chứ tách chuỗi dạng phức tạp theo em nghĩ regexp sẽ lợi thế hơn, nếu nói không đúng mong anh bỏ qua!!!
 
Upvote 0
Em chỉ muốn nghiên cứu về Reg thôi anh ạ, làm mấy bài như tách số điện thoại, tách email , hay tách một đoạn nào đấy trong một chuỗi phức tạp thì phải công thức rất phức tạp hay phải dùng SUBSTITUTE rất nhiều lần, như anh nói bài này có nhiều cách , dùng split kết hợp với Dictionary, hay split và instr thì đơn giản hơn ạ, chứ tách chuỗi dạng phức tạp theo em nghĩ regexp sẽ lợi thế hơn, nếu nói không đúng mong anh bỏ qua!!!
Nếu tôi làm thì bài này tôi sẽ dùng 1 trong 2 cách:
- Split + Dictionary
- Split + InStr
tuy nhiên tôi thấy InStr đã quá đủ rồi
 
Upvote 0
Em chỉ muốn nghiên cứu về Reg thôi anh ạ, làm mấy bài như tách số điện thoại, tách email , hay tách một đoạn nào đấy trong một chuỗi phức tạp thì phải công thức rất phức tạp hay phải dùng SUBSTITUTE rất nhiều lần, như anh nói bài này có nhiều cách , dùng split kết hợp với Dictionary, hay split và instr thì đơn giản hơn ạ, chứ tách chuỗi dạng phức tạp theo em nghĩ regexp sẽ lợi thế hơn, nếu nói không đúng mong anh bỏ qua!!!

Nhận dạng chuoix đúng là sở trường của Regex. Cái tên của nó là vậy mà.

Tuy nhiên, từ "phức tạp" nó cũng có vấn đề chủ quan của nó. Trong đề bài này, cái token của bạn là một tử,, Và từ bất kể, tức là đại trà. Regex không có cái gì để ưu việt với các giải thuật duyệt chuỗi khác cả. Tất cả tính chất "so chuỗi", và "dòm trước ngó sau" của nó đều không sử dụng được hiệu quả.
 
Upvote 0
Nhấn edit lầm. Xin lỗi
 
Upvote 0
Em có đoạn code:
PHP:
Sub tachtrung2()
    Dim i As Long, j As Long, text As String, text2 As String, text3 As String
    text3 = "Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;2017;2017;dien;EXCEL;2017"
    text = Replace(";" & text3 & ";", ";", "   ")
    With CreateObject("vbscript.regexp")
        .Global = True
        .ignorecase = True
        .Pattern = "((\s\w+\s).+)\2"
        Do While .test(text)
            text = .Replace(text, "$1 ")
            i = 0
            For Each subl In .Execute(text)
                If InStr(text2, Trim(.Execute(text).Item(i).submatches(1))) = 0 Then
                    text2 = text2 & " | " & Trim(.Execute(text).Item(i).submatches(1))
                End If
                i = i + 1
            Next
        Loop
    End With
    MsgBox ("- " & Application.Trim(text) + ChrW(10) & "- " & text2)
End Sub
Kết quả của đoạn code trên tạo ra là chuỗi sau khi loại trùng hết (text) và liệt kê các chuỗi text bị trùng (text2)
- Sau khi chay code được text2, nhưng đoạn text2 lúc nào cũng có dấu "|" ở đầu và cuối, em có thể dùng Application.SUBSTITUTE để loại dấu "|" đầu và cuối, nhưng trong VBA có hàm replace , em đã thử replace(text,text2,"",,1) nhưng không được, cho em hỏi là em có thể dùng replace trong trường hợp này được không?
- Kết quả text2="| Dien | dan | 2017 | phap | Dan | excel" , bị trùng chữ "dan" và "Dan", em đã dùng Instr để xác định xem xuất hiện trong chuỗi không nhưng sao kết quả text2 vẫn có trùng, nhờ các anh/chị xem và chỉnh code dùm em.
Em cám ơn!!



Khi có yếu tố trùng lặp đầu tiên phải nghĩ ngay đến việc đưa dữ liệu vào mảng (ARR) và sử dụng "Dictionary"
Còn nếu bạn vẫn muốn sử dụng regular expression, (già rồi ngại xem và chỉnh sửa code của người khác ^^) bạn thử tham khảo cách tôi liệt kê "Text2" như sau:
Mã:
Sub a()
Dim i&, tmp, text2$
Dim str As String, oMatch As IMatchCollection2
    str = "Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;2017;2017;dien;EXCEL;2017"
    str = "#" & Replace(str, ";", "#") & "#" ' chẳng hiểu sao lại phải thay thế ; thành #; nếu đã thay sao không thay bằng Chr(0) ?????
    With New RegExp
        .Global = True
        .IgnoreCase = True
        .Pattern = "#(\w+)#.*\1+"
        Do While .Test(str)
            Set oMatch = .Execute(str)
            tmp = oMatch(i).SubMatches(0)
            text2 = text2 & tmp
            str = Replace(str, tmp, "", , , vbTextCompare)
        Loop
        MsgBox text2
    End With
End Sub
 
Upvote 0
Khi có yếu tố trùng lặp đầu tiên phải nghĩ ngay đến việc đưa dữ liệu vào mảng (ARR) và sử dụng "Dictionary"
Còn nếu bạn vẫn muốn sử dụng regular expression, (già rồi ngại xem và chỉnh sửa code của người khác ^^) bạn thử tham khảo cách tôi liệt kê "Text2" như sau:
Mã:
Sub a()
Dim i&, tmp, text2$
Dim str As String, oMatch As IMatchCollection2
    str = "Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;2017;2017;dien;EXCEL;2017"
    str = "#" & Replace(str, ";", "#") & "#" ' chẳng hiểu sao lại phải thay thế ; thành #; nếu đã thay sao không thay bằng Chr(0) ?????
    With New RegExp
        .Global = True
        .IgnoreCase = True
        .Pattern = "#(\w+)#.*\1+"
        Do While .Test(str)
            Set oMatch = .Execute(str)
            tmp = oMatch(i).SubMatches(0)
            text2 = text2 & tmp
            str = Replace(str, tmp, "", , , vbTextCompare)
        Loop
        MsgBox text2
    End With
End Sub
Em chạy nó báo lỗi chỗ này oMatch As IMatchCollection2 anh ạ.
chẳng hiểu sao lại phải thay thế ; thành #; nếu đã thay sao không thay bằng Chr(0) ?????: 2 cái pattern khác nhau anh ạ, nên em nó replace nó về " ", cái pattern của em là để chạy cả text và text2 (text là loại bỏ không trùng luôn), cái parttern của anh chỉ liệt kê text2 thôi.
Replace(str, tmp, "", , , vbTextCompare) anh có thể giải thích dùm em đoạn này được không ạ? Em cám ơn anh!!!
 
Lần chỉnh sửa cuối:
Upvote 0
Em chạy nó báo lỗi chỗ này oMatch As IMatchCollection2 anh ạ.
chẳng hiểu sao lại phải thay thế ; thành #; nếu đã thay sao không thay bằng Chr(0) ?????: 2 cái pattern khác nhau anh ạ, nên em nó replace nó về " ", cái pattern của em là để chạy cả text và text2 (text là loại bỏ không trùng luôn), cái parttern của anh chỉ liệt kê text2 thôi.
Replace(str, tmp, "", , , vbTextCompare) anh có thể giải thích dùm em đoạn này được không ạ? Em cám ơn anh!!!
báo lỗi vì bạn chưa khai báo regex theo kiểu "sớm" , ( vào tool --> reference --> tìm thến microsoft vbscritp regular,....)
Replace(str, tmp, "", , , vbTextCompare) = replace( chuỗi chứa chuỗi cần thay thế , chuỗi thay thế bằng chuỗi khác, chuỗi thay thế, không phân biệt chữ hoa chữ thường)
 
Upvote 0
báo lỗi vì bạn chưa khai báo regex theo kiểu "sớm" , ( vào tool --> reference --> tìm thến microsoft vbscritp regular,....)
Replace(str, tmp, "", , , vbTextCompare) = replace( chuỗi chứa chuỗi cần thay thế , chuỗi thay thế bằng chuỗi khác, chuỗi thay thế, không phân biệt chữ hoa chữ thường)
Dạ em chỉnh lại cái new regexp thanh vbscript.regexp nên chạy được rồi anh, hic, cái anh dùng là replace của VBA , nãy giờ em tưởng là của Regexp, nên thấy lạ, em cám ơn anh!!!
 
Upvote 0
Em có đoạn code:
PHP:
Sub tachtrung2()
    Dim i As Long, j As Long, text As String, text2 As String, text3 As String
    text3 = "Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;2017;2017;dien;EXCEL;2017"
    text = Replace(";" & text3 & ";", ";", "   ")
    With CreateObject("vbscript.regexp")
        .Global = True
        .ignorecase = True
        .Pattern = "((\s\w+\s).+)\2"
        Do While .test(text)
            text = .Replace(text, "$1 ")
            i = 0
            For Each subl In .Execute(text)
                If InStr(text2, Trim(.Execute(text).Item(i).submatches(1))) = 0 Then
                    text2 = text2 & " | " & Trim(.Execute(text).Item(i).submatches(1))
                End If
                i = i + 1
            Next
        Loop
    End With
    MsgBox ("- " & Application.Trim(text) + ChrW(10) & "- " & text2)
End Sub
Kết quả của đoạn code trên tạo ra là chuỗi sau khi loại trùng hết (text) và liệt kê các chuỗi text bị trùng (text2)
- Sau khi chay code được text2, nhưng đoạn text2 lúc nào cũng có dấu "|" ở đầu và cuối, em có thể dùng Application.SUBSTITUTE để loại dấu "|" đầu và cuối, nhưng trong VBA có hàm replace , em đã thử replace(text,text2,"",,1) nhưng không được, cho em hỏi là em có thể dùng replace trong trường hợp này được không?
- Kết quả text2="| Dien | dan | 2017 | phap | Dan | excel" , bị trùng chữ "dan" và "Dan", em đã dùng Instr để xác định xem xuất hiện trong chuỗi không nhưng sao kết quả text2 vẫn có trùng, nhờ các anh/chị xem và chỉnh code dùm em.
Em cám ơn!!
Đọc code không hiểu hết, có vài ý này:
- Cái "For Each subl In .Execute(text)", biến "subl" không thấy xuất hiện trong các câu lệnh sau
- Dan & dan trong chuỗi text2 dùng thử ucase() hoặc lcase() xem sao
 
Upvote 0
Khi có yếu tố trùng lặp đầu tiên phải nghĩ ngay đến việc đưa dữ liệu vào mảng (ARR) và sử dụng "Dictionary"
...

Bạn có thấy cái "\2" nằm ở cuối cái pattern của chủ thớt hôn?
Cái kỹ thuật back reference này là kỹ thuật cao cấp. Không phải của dân mới học.
 
Upvote 0
Đọc code không hiểu hết, có vài ý này:
- Cái "For Each subl In .Execute(text)", biến "subl" không thấy xuất hiện trong các câu lệnh sau
- Dan & dan trong chuỗi text2 dùng thử ucase() hoặc lcase() xem sao
Cái "subl" là đại diện cho mỗi item trong regexp, nó chính là .Execute(text).Item(i) đấy bạn, vì regexp tạo ra dạng mảng collection nên mình dùng for Each để lấy ra.
Instr không phân biệt chữ hoa chữ thường thì phải, vì mấy cái text kia đều có hoa thường hết, nhưng loại được. Có lẽ nên dùng replace sẽ hợp lý hơn!!!
 
Lần chỉnh sửa cuối:
Upvote 0
Bạn có thấy cái "\2" nằm ở cuối cái pattern của chủ thớt hôn?
Cái kỹ thuật back reference này là kỹ thuật cao cấp. Không phải của dân mới học.
Cái \2 là cái (\s\w+\s) được lưu trong submacthes, và được dùng để liệt kê cái text2. Bài này em giải từ bài của anh @dhn46 thấy có anh @huuthang_bd giải rồi nhưng chỉ liệt kê cái text2 , chứ chưa loại trùng cái text, nên em làm lại với cái pattern khác, em có đọc bài về regexp của anh @hungpecc1 và anh @quanghai1969, thấy có mấy bài rất hay về Backreference của anh @siwtom nên có thể hiểu được phần nào.
 
Lần chỉnh sửa cuối:
Upvote 0
Cái "subl" là đại diện cho mỗi item trong regexp, nó chính là .Execute(text).Item(i) đấy bạn, vì regexp tạo ra dạng mảng collection nên mình dùng for Each để lấy ra.
Nếu thế thì trong vòng lặp for each có lẽ nên thay .Execute(text).Item(i) = subl thì dễ hiểu hơn
Instr không phân biệt chữ hoa chữ thường thì phải, vì mấy cái text kia đều có hoa thường hết, nhưng loại được. Có lẽ nên dùng replace sẽ hợp lý hơn!!!
Hình như hàm instr có phân biệt hoa thường instr( start, str1, str2, compare ) chỗ màu đỏ thì phải.

Dạng bài này hình như trước đây đã có lần thấy không dùng 1 vòng lặp nào cả .
 
Lần chỉnh sửa cuối:
Upvote 0
Nếu thế thì trong vòng lặp for each có lẽ nên thay .Execute(text).Item(i) = subl thì dễ hiểu hơn

Hình như hàm instr có phân biệt hoa thường instr( start, str1, str2, compare ) chỗ màu đỏ thì phải.

Dạng bài này hình như trước đây đã có lần thấy không dùng 1 vòng lặp nào cả .
Tại mình làm tắt bạn ạ, viết đúng là for each subl...next subl, không dùng for each thì dùng fphor i=0 to .Execute(text).Count - 1 cũng được
Mình dùng lcase hay ucase thì được rồi bạn ạ, nhưng ngộ quá trong chuỗi này "Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;2017;2017;dien;EXCEL;2017" , các chữ khác đều có viết hoa viết thường tùm lum hết , mà loại được ,mà riêng chữa "dan" lại phải dùng ucase hay lcase mới được.
Theo mình nghĩ cái code mình phải chạy vòng lặp mới được, vì pattern đó nó làm 2 việc là loại trùng các chữ bị trùng nhau và liệt kê các chữ trùng nhau đấy, chỉ riêng phần liệt kê không là phải chạy vòng lặp rồi, vì mỗi cái số \2 nó sẽ lưu vào submatches vì là nó tự nghi nhớ nên những đoạn tetxt phù hợp với pattern là nó tự động đưa vào luôn, nếu mà đoạn text bị trùng nhiều hơn 2 lần thì cái submacthes chắc chắn sẽ bị trùng nên mới dùng các cách để xét trùng (trong bài đang dùng instr) đồng thời mỗi cái submaches đó lại thuộc các item khác nhau, nên phải dùng vòng lặp duyệt qua mới lấy ra được.
Riêng phân loại trùng đoạn text trên mà không dùng vòng lặp mình không nghĩ ra được cái pattern khác bạn ạ, bạn làm giúp mình loại trùng mà không dùng vòng lặp được không bạn, không cần liệt kê các phần tử trùng. Cám ơn bạn.
 
Lần chỉnh sửa cuối:
Upvote 0
Cái \2 là cái (\s\w+\s) được lưu trong submacthes, và được dùng để liệt kê cái text2. Bài này em giải từ bài của anh @dhn46 thấy có anh @huuthang_bd giải rồi nhưng chỉ liệt kê cái text2 , chứ chưa loại trùng cái text, nên em làm lại với cái pattern khác, em có đọc bài về regexp của anh @hungpecc1 và anh @quanghai1969, thấy có mấy bài rất hay về Backreference của anh @siwtom nên có thể hiểu được phần nào.

Như tôi đã nói qua, nhìn thấy cái \2 thì biết cái pattern đó dùng bạckreference. Mà đã dùng kỹ thuật này thì là cao cấp. Bạn có thể dùng watch và debug để tìm hiểu lỗi.

siwtom là tay chuyên nghiệp về code ứng dụng trên Delphi. Đương nhiên là kỹ thuật cao rồi. Tuy nhiên, nếu bạn muốn biết thêm về Regex thì nên tìm vào các diễn đàn chuyên Unix (Linux, Ubuntu,...). Ba cái phân tích chuỗi này (kể cả réc éc) thì Perl mới là chúa tể. VBScript không hổ trợ ba cái dòm trước ngó sau này mạnh lắm nên tôi lười đi sâu.
 
Upvote 0
Riêng phân loại trùng đoạn text trên mà không dùng vòng lặp mình không nghĩ ra được cái pattern khác bạn ạ, bạn làm giúp mình loại trùng mà không dùng vòng lặp được không bạn, không cần liệt kê các phần tử trùng. Cám ơn bạn.
Để tìm lại file lưu hoặc đường dẫn sẽ gửi lại bạn.
Thân chào
 
Upvote 0
siwtom là tay chuyên nghiệp về code ứng dụng trên Delphi.
Lại bị gọi tên :D
Công việc của tôi không đòi hỏi. Chỉ là đam mê thôi.
Ba cái phân tích chuỗi này (kể cả réc éc) thì Perl mới là chúa tể. VBScript không hổ trợ ba cái dòm trước ngó sau này mạnh lắm nên tôi lười đi sâu.
Đúng 200%
Có một số giới hạn. Điển hình là nó khong có tính năng "dòm ngược"..
Đúng 200%. Tôi tiếc đứt ruột là ...

Có
mẫu2(?=mẫu1) - tìm các đoạn có dạng mẫu2 mà sau chúng là đoạn có dạng mẫu1
mẫu2(?!mẫu1) - tìm các đoạn có dạng mẫu2 mà sau chúng không có đoạn dạng mẫu1

nhưng không có
Tìm các đoạn có dạng mẫu2 mà trước chúng là đoạn có dạng mẫu1
Tìm các đoạn có dạng mẫu2 mà trước chúng không có đoạn dạng mẫu1

Em chỉ muốn nghiên cứu về Reg thôi anh ạ
Ví dụ:
Mã:
Sub tachtrung2()
Dim text As String, text3 As String
    text3 = "Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;2017;2017;dien;EXCEL;2017"
'    de phong co dau cach
    text = Replace(text3 & ";", " ", "")
    With CreateObject("vbscript.regexp")
        .Global = True
        .ignorecase = True
        .Pattern = "(\w+;)((\w+;)*)\1"
        Do While .test(text)
            text = .Replace(text, "$1$2")
        Loop
    End With
    MsgBox Mid(text, 1, Len(text) - 1)
End Sub

Hoặc chuỗi văn bản tự nhiên hơn - các từ cách nhau bằng dấu cách.
Mã:
Sub tachtrung2()
Dim text As String, text3 As String
    text3 = "Dien     Dien Dan    dien giai phap   dien    dan phap   excel  Excel phap   dan  2017 2017   dien  EXCEL 2017"
'    loai dau cach thua
    text = WorksheetFunction.Trim(text3) & " "
    With CreateObject("vbscript.regexp")
        .Global = True
        .ignorecase = True
        .Pattern = "(\w+ )((\w+ )*)\1"
        Do While .test(text)
            text = .Replace(text, "$1$2")
        Loop
    End With
    MsgBox Mid(text, 1, Len(text) - 1)
End Sub
 
Lần chỉnh sửa cuối:
Upvote 0
Lại bị gọi tên :D
Công việc của tôi không đòi hỏi. Chỉ là đam mê thôi.

Đúng 200%

Đúng 200%. Tôi tiếc đứt ruột là ...

Có
mẫu2(?=mẫu1) - tìm các đoạn có dạng mẫu2 mà sau chúng là đoạn có dạng mẫu1
mẫu2(?!mẫu1) - tìm các đoạn có dạng mẫu2 mà sau chúng không có đoạn dạng mẫu1

nhưng không có
Tìm các đoạn có dạng mẫu2 mà trước chúng là đoạn có dạng mẫu1
Tìm các đoạn có dạng mẫu2 mà trước chúng không có đoạn dạng mẫu1


Ví dụ:
Mã:
Sub tachtrung2()
Dim text As String, text3 As String
    text3 = "Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;2017;2017;dien;EXCEL;2017"
'    de phong co dau cach
    text = Replace(text3 & ";", " ", "")
    With CreateObject("vbscript.regexp")
        .Global = True
        .ignorecase = True
        .Pattern = "(\w+;)((\w+;)*)\1"
        Do While .test(text)
            text = .Replace(text, "$1$2")
        Loop
    End With
    MsgBox Mid(text, 1, Len(text) - 1)
End Sub

Hoặc chuỗi văn bản tự nhiên hơn - các từ cách nhau bằng dấu cách.
Mã:
Sub tachtrung2()
Dim text As String, text3 As String
    text3 = "Dien     Dien Dan    dien giai phap   dien    dan phap   excel  Excel phap   dan  2017 2017   dien  EXCEL 2017"
'    loai dau cach thua
    text = WorksheetFunction.Trim(text3) & " "
    With CreateObject("vbscript.regexp")
        .Global = True
        .ignorecase = True
        .Pattern = "(\w+ )((\w+ )*)\1"
        Do While .test(text)
            text = .Replace(text, "$1$2")
        Loop
    End With
    MsgBox Mid(text, 1, Len(text) - 1)
End Sub
Cái code này của anh rất hay đấy ạ, dù code của em về cách chạy thì giống code anh, nhưng cái pattern của anh ổn hơn, em phải chỉnh cái parttern mấy lần để cho phù hợp với text những lần chạy sau, cái dấu * của anh rất hay , em dùng .+ nên bắc buộc lúc nào cũng phải có 1 kí tự nên rơi vào trường hợp dien;dien; là chịu. Anh cho em hỏi tý, khi code chạy vòng lặp Do thứ 2 thì cái đoạn phù hợp nhất với pattern là "Dien;Dien;Dan;dien;giai;phap;dien;", nhưng nếu em nhìn cái pattern của anh là"(\w+ ; )((\w+ ; )*)\1" thì đoạn "Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;" nó cũng phù hợp, nếu regexp để defaut thì nó sẽ lấy đoạn xa nhất tức là đoạn thứ 2. Không biết em có hiểu sai chỗ này của anh không, anh hãy giải thích cho em chỗ này, cám ơn anh!!
 
Upvote 0
nhưng nếu em nhìn cái pattern của anh là"(\w+ ; )((\w+ ; )*)\1" thì đoạn "Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;" nó cũng phù hợp
Không phải.

Sau vòng Do thứ 1 thì
text = "Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;2017;2017;EXCEL;2017;"

Chú ý: pattern không có dấu cách nhưng trong bài viết tôi thay cụm <dấu chấm phẩy+ dấu ")"> bằng <dấu chấm phẩy+ dấu cách + dấu ")"> vì nếu không thì script thay cụm bằng hình mặt cười.

Đoạn ((\w+; )*) sẽ hoặc là trống hoặc phải có dạng:
<từ1>;<từ2>;...<từk>;
Tức nếu không rỗng thì phải kết thúc bằng dấu chấm phẩy ;
(phải là <từ1> chứ không thể <một phần của từ1>. Vì trước nó phải có dấu chấm phẩy của (\w+; ))

Từ đây thấy rõ là đoạn:
"n;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;

không khớp với pattern. Vì nếu khớp thì:

((\w+; )*) = "Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;da"

rõ ràng không kết thúc bằng dấu chấm phẩy.

Đoạn
"Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;"

cũng không khớp với pattern vì lúc đó phải có (\w+; ) = "Dien;" và \1 = "dan;", vô lý. Vì (\w+; ) và \1 phải y hệt nhau (không phân biệt chữ hoa hay thường). Không có chuyện "n;" = "n;" vì cả (\w+; ) và \1 phải là <từ>; chứ không thể là <một phần của từ>;
---------------
Ở vòng Do thứ 2 ta có tận 3 đoạn khớp:
1. "Dien;Dien;Dan;dien;giai;phap;dien;"
\1 = "dien;" và nó được bỏ

2. "dan;phap;excel;Excel;phap;dan;"
\1 = "dan;" và nó được bỏ

3. "2017;2017;EXCEL;2017;"
\1 = "2017;" và nó được bỏ

Do đó sau vòng Do thứ 2 ta có:
text = "Dien;Dien;Dan;dien;giai;phap;dan;phap;excel;Excel;phap;2017;2017;EXCEL;"
---------------
Các tính chất của pattern:
1. ((\w+; )*) hoặc là trống hoặc là chuỗi các từ cách nhau bằng dấu chấm phẩy và kết thức bằng dấu chấm phẩy.
2. (\w+; ) và \1 phải là 1 từ và sau nó có dấu chấm phẩy. Là một từ chứ không là một phần của từ vì trước \1 phải là dấu chấm phẩy của (\w+; ) (khi ((\w+; )*) rỗng), hoặc dấu chấm phẩy của ((\w+; )*)
 
Lần chỉnh sửa cuối:
Upvote 0
Cái "subl" là đại diện cho mỗi item trong regexp, nó chính là .Execute(text).Item(i) đấy bạn, vì regexp tạo ra dạng mảng collection nên mình dùng for Each để lấy ra.
Instr không phân biệt chữ hoa chữ thường thì phải,
Nếu bạn dùng vbBinaryCompare (mặc định) thì phân biệt chữ hoa thường. Muốn không phân biệt thì phải dùng vbTextCompare

Tại sao lại dùng .Execute(text).Item(i) trong IF ... End If khi đó chính là subl?

Tóm lại thay
Mã:
i = 0
            For Each subl In .Execute(text)
                If InStr(text2, Trim(.Execute(text).Item(i).submatches(1))) = 0 Then
                    text2 = text2 & " | " & Trim(.Execute(text).Item(i).submatches(1))
                End If
                i = i + 1
            Next
bằng
Mã:
For Each subl In .Execute(text)
                If InStr(1, text2, Trim(subl.submatches(1)), vbTextCompare) = 0 Then
                    text2 = text2 & " | " & Trim(subl.submatches(1))
                End If
            Next
Nhưng code thực ra không đúng.
Nếu bạn có vd. text3 = "Dan;com;dan" thì text2 sẽ rỗng trong khi phải có text2 = "Dan". Tại sao?
Vì .Execute(text) sẽ trả về tập rỗng do Execute được thực hiện cho text = " Dan com ", tức cho text ở dòng text = .Replace(text, "$1 ") chứ không phải cho text = " Dan com dan " ở dòng Do While. Vậy ta sửa thành
Mã:
Do While .test(text)
    Set match = .Execute(text)
    text = .Replace(text, "$1 ")
    For Each subl In match
        If InStr(1, text2, Trim(subl.submatches(1)), vbTextCompare) = 0 Then
            text2 = text2 & " | " & Trim(subl.submatches(1))
        End If
    Next
Loop
Nhưng code trên vẫn chưa đúng vì mới giải quyết xong vấn đề .Execute(text). Còn vấn đề InStr thì chưa chính xác.
Bạn thử với text3 = "Dan;An;com;an;pho;dan" thì text2 không có "An". Vì sao?
Sau Do While thứ 1 có text = " Dan An com an Pho " và text2 = " | Dan"
Trong vòng thứ 2 thì Trim(subl.submatches(1)) = "An", sẽ tìm thấy trong text2 nên điều kiên
Mã:
InStr(1, text2, Trim(subl.submatches(1)), vbTextCompare) = 0
sẽ không thỏa nên "An" không được thêm vào text2.
Vậy phải sửa thành
Mã:
text2 = "|"
Do While .test(text)
    Set match = .Execute(text)
    text = .Replace(text, "$1 ")
    For Each subl In match
        If InStr(1, text2, "|" & Trim(subl.submatches(1)) & "|", vbTextCompare) = 0 Then
            text2 = text2 & Trim(subl.submatches(1)) & "|"
        End If
    Next
Loop

Mổ sẻ thế là đủ rồi nhỉ ;)
 
Upvote 0
Không phải.

Sau vòng Do thứ 1 thì
text = "Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;2017;2017;EXCEL;2017;"

Chú ý: pattern không có dấu cách nhưng trong bài viết tôi thay cụm <dấu chấm phẩy+ dấu ")"> bằng <dấu chấm phẩy+ dấu cách + dấu ")"> vì nếu không thì script thay cụm bằng hình mặt cười.

Đoạn ((\w+; )*) sẽ hoặc là trống hoặc phải có dạng:
<từ1>;<từ2>;...<từk>;
Tức nếu không rỗng thì phải kết thúc bằng dấu chấm phẩy ;
(phải là <từ1> chứ không thể <một phần của từ1>. Vì trước nó phải có dấu chấm phẩy của (\w+; ))

Từ đây thấy rõ là đoạn:
"n;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;

không khớp với pattern. Vì nếu khớp thì:

((\w+; )*) = "Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;da"

rõ ràng không kết thúc bằng dấu chấm phẩy.

Đoạn
"Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;"

cũng không khớp với pattern vì lúc đó phải có (\w+; ) = "Dien;" và \1 = "dan;", vô lý. Vì (\w+; ) và \1 phải y hệt nhau (không phân biệt chữ hoa hay thường). Không có chuyện "n;" = "n;" vì cả (\w+; ) và \1 phải là <từ>; chứ không thể là <một phần của từ>;
---------------
Ở vòng Do thứ 2 ta có tận 3 đoạn khớp:
1. "Dien;Dien;Dan;dien;giai;phap;dien;"
\1 = "dien;" và nó được bỏ

2. "dan;phap;excel;Excel;phap;dan;"
\1 = "dan;" và nó được bỏ

3. "2017;2017;EXCEL;2017;"
\1 = "2017;" và nó được bỏ

Do đó sau vòng Do thứ 2 ta có:
text = "Dien;Dien;Dan;dien;giai;phap;dan;phap;excel;Excel;phap;2017;2017;EXCEL;"
---------------
Các tính chất của pattern:
1. ((\w+; )*) hoặc là trống hoặc là chuỗi các từ cách nhau bằng dấu chấm phẩy và kết thức bằng dấu chấm phẩy.
2. (\w+; ) và \1 phải là 1 từ và sau nó có dấu chấm phẩy. Là một từ chứ không là một phần của từ vì trước \1 phải là dấu chấm phẩy của (\w+; ) (khi ((\w+; )*) rỗng), hoặc dấu chấm phẩy của ((\w+; )*)
Cám ơn anh đã giải thích rất chi tiết cho em, em đã biết thêm một số cái, đặc biệt chỗ dấu *, mới đầu xem em cứ ý là dấu * trong regexp và trong công thức excel là như nhau, nhưng không phải vậy dấu * trong công thức là đại diện cho một chuỗi bất kì (có thể không có), còn trong regexp thì nó lặp lại phần tử đứng trước nó với tần suất >=0, còn + thì >0, bởi vậy nhằm lúc làm ra sai mà không hiểu sao.
Em vẫn có chỗ thắc mác thêm anh ạ, như chỗ này:
(\w+; ) và \1 phải là 1 từ và sau nó có dấu chấm phẩy.
Lúc trước làm thì em có chạy thử cái pattern này:
Mã:
Pattern = "((\w+;).*)\2"
text = .Replace(text, "$1")
(xin lỗi vì phải cho vào đây vì hiện mặt cười). Em dùng debug để kiểm tra khi đoạn text còn là "Dien;Dan;giai;phap;phap;excel;Excel;2017;2017;" thì nó nhận diện đoạn phù hợp nhất với Pattern là "n;Dan;", "phap;phap;","excel;Excel;","2017;2017;" . Em cũng hiểu là \w+ là nó sẽ lấy những phần tử trong tập w đến khi nào không có thì dừng theo nguyên tắc như vậy thì lẽ ra nó phải nhận diện chỉ có phap;phap;,excel;Excel;,2017;2017; thôi anh nhỉ, sao nó lại lụm thằng "n;Dan;", bởi vậy em mới thắc mắc ở bài trên, anh giải thích cho em chỗ này, cám ơn anh!!
 
Lần chỉnh sửa cuối:
Upvote 0
Nếu bạn dùng vbBinaryCompare (mặc định) thì phân biệt chữ hoa thường. Muốn không phân biệt thì phải dùng vbTextCompare

Tại sao lại dùng .Execute(text).Item(i) trong IF ... End If khi đó chính là subl?

Tóm lại thay
Mã:
i = 0
            For Each subl In .Execute(text)
                If InStr(text2, Trim(.Execute(text).Item(i).submatches(1))) = 0 Then
                    text2 = text2 & " | " & Trim(.Execute(text).Item(i).submatches(1))
                End If
                i = i + 1
            Next
bằng
Mã:
For Each subl In .Execute(text)
                If InStr(1, text2, Trim(subl.submatches(1)), vbTextCompare) = 0 Then
                    text2 = text2 & " | " & Trim(subl.submatches(1))
                End If
            Next
Nhưng code thực ra không đúng.
Nếu bạn có vd. text3 = "Dan;com;dan" thì text2 sẽ rỗng trong khi phải có text2 = "Dan". Tại sao?
Vì .Execute(text) sẽ trả về tập rỗng do Execute được thực hiện cho text = " Dan com ", tức cho text ở dòng text = .Replace(text, "$1 ") chứ không phải cho text = " Dan com dan " ở dòng Do While. Vậy ta sửa thành
Mã:
Do While .test(text)
    Set match = .Execute(text)
    text = .Replace(text, "$1 ")
    For Each subl In match
        If InStr(1, text2, Trim(subl.submatches(1)), vbTextCompare) = 0 Then
            text2 = text2 & " | " & Trim(subl.submatches(1))
        End If
    Next
Loop
Nhưng code trên vẫn chưa đúng vì mới giải quyết xong vấn đề .Execute(text). Còn vấn đề InStr thì chưa chính xác.
Bạn thử với text3 = "Dan;An;com;an;pho;dan" thì text2 không có "An". Vì sao?
Sau Do While thứ 1 có text = " Dan An com an Pho " và text2 = " | Dan"
Trong vòng thứ 2 thì Trim(subl.submatches(1)) = "An", sẽ tìm thấy trong text2 nên điều kiên
Mã:
InStr(1, text2, Trim(subl.submatches(1)), vbTextCompare) = 0
sẽ không thỏa nên "An" không được thêm vào text2.
Vậy phải sửa thành
Mã:
text2 = "|"
Do While .test(text)
    Set match = .Execute(text)
    text = .Replace(text, "$1 ")
    For Each subl In match
        If InStr(1, text2, "|" & Trim(subl.submatches(1)) & "|", vbTextCompare) = 0 Then
            text2 = text2 & Trim(subl.submatches(1)) & "|"
        End If
    Next
Loop

Mổ sẻ thế là đủ rồi nhỉ ;)
Cám ơn anh đã giải thích đoạn chữ hoa chữ thường dùm em, thật ra mới đầu em có nghĩ tới nhưng em cho là không phải tại chỗ này , vì đoạn text này "
"Dien;Dien;Dan;dien;giai;phap;dien;dan;phap;excel;Excel;phap;dan;2017;2017;dien;EXCEL;2017", các chữ khác đề có chữ hoa và chữ thường như nhau chẵng hạn "Dien" và "dien", "excel" và "EXCEL".. mà không bị, chỉ bị mỗi "Dan" và "dan". Sau khi xem lại thì nó loại được là do may mắn, ví dụ cặp "Dien" và "dien" nó lưu vào submatches là "Dien" và loại thằng "dien" đi khi replace, nên thằng Instr sẽ chẵng có cơ hội để so sánh... Nhưng thằng "Dan" và "dan" nó lại khác nó lưu đầu tiên vào submaches là "dan" , vì khi chay sẽ có lúc 2 đoạn khớp với pattern trong đó có "dan...dan", khi chạy lần tiếp theo do "Dan" đứng trước "dan", nên nó sẽ lưu "Dan" vào submactes nữa, vì vậy khi dùng Instr defaut thì sẽ hiểu "Dan" và "dan" là khác nhau.
Nhưng code thực ra không đúng.
Nếu bạn có vd. text3 = "Dan;com;dan" thì text2 sẽ rỗng trong khi phải có text2 = "Dan". Tại sao?
Vì .Execute(text) sẽ trả về tập rỗng do Execute được thực hiện cho text = " Dan com ", tức cho text ở dòng text = .Replace(text, "$1 ") chứ không phải cho text = " Dan com dan " ở dòng Do While. Vậy ta sửa thành
Cám ơn anh, em không chú ý đến chỗ này thật, vì đã chạy replace rồi nên cái execute là của cái text mới chứ không phải cái text cũ.
Nhưng code trên vẫn chưa đúng vì mới giải quyết xong vấn đề .Execute(text). Còn vấn đề InStr thì chưa chính xác.
Bạn thử với text3 = "Dan;An;com;an;pho;dan" thì text2 không có "An". Vì sao?
Sau Do While thứ 1 có text = " Dan An com an Pho " và text2 = " | Dan"
Trong vòng thứ 2 thì Trim(subl.submatches(1)) = "An", sẽ tìm thấy trong text2 nên điều kiên
Vâng, chỗ này thì em hiểu rồi ạ, chỗ này em bị các anh chị khác trong diễn đàn nhắc nhở khi so sanh text rồi, cám ơn anh!!
 
Lần chỉnh sửa cuối:
Upvote 0
Thực ra pattern củas tôi vẫn chưa chính xác. Phải là
Mã:
.Pattern = "(\b\w+;)((\w+;)*)\1"
Nhưng có lẽ
Mã:
.Pattern = "(\b\w+;)(.*)\1"
còn hay hơn
Cái pattern
Mã:
.Pattern = "(\b\w+;)(.*)\1"
khi bỏ \b thì nó giống với cái pattern em đã nói ở trên:
Mã:
 .Pattern = "((\w+;).*)\2"
Thêm \b vào thì tức là tới đầu vị trí của mỗi chuỗi thì em hiểu nó sẽ loại được trường hợp "n;dan;". Nhưng (\b\w+ ; )và (\w+ ; ) là khác nhau như thế nào , (\w+ ; ) theo em hiểu là nó sẽ lấy đến khi nào không có phần tử thuộc w nữa thì thôi, em nghĩ nó cũng giống (\b\w+ ; ). Anh giải thích dùm em chỗ này, cám ơn anh!!!
(Xin lỗi thêm khoảng trắng vào mấy cái pattern vì hiện mặt cười)
 
Upvote 0
Em vẫn có chỗ thắc mác thêm anh ạ, như chỗ này:

Lúc trước làm thì em có chạy thử cái pattern này:
Mã:
Pattern = "((\w+;).*)\2"
text = .Replace(text, "$1")
(xin lỗi vì phải cho vào đây vì hiện mặt cười). Em dùng debug để kiểm tra khi đoạn text còn là "Dien;Dan;giai;phap;phap;excel;Excel;2017;2017;" thì nó nhận diện đoạn phù hợp nhất với Pattern là "n;Dan;", "phap;phap;","excel;Excel;","2017;2017;" .
, sao nó lại lụm thằng "n;Dan;", bởi vậy em mới thắc mắc ở bài trên, anh giải thích cho em chỗ này, cám ơn anh!!
pattern này của bạn cũng phạm lỗi như pattern cũ của tôi. Tôi phát hiện ra sự thiếu chính xác trong pattern của mình khi phân tích và nêu các tính chất của pattern.
Trước tiên nói về pattern của bạn
Mã:
.Pattern = "((\w+;).*)\2"

\1 = \w+;.*
"ông anh sinh đôi" của \2 = \w+;
Suy ra:
1. "ông anh sinh đôi" của \2 và cả \2 là chuỗi ký tự thuộc [a-z0-9] và kết thúc bằng dấu chấm phẩy? Chỉ thế thôi. Còn pattern không bắt buộc trước "ông anh sinh đôi" của \2 và \2 phải là "biên giới giữa text và không text". Vậy thì vẫn có trường hợp trước "ông anh sinh đôi" của \2 hoặc trước \2 không có "biên giới giữa text và không text", tức "ông anh sinh đôi" của \2 hoặc \2 chỉ là một phần của từ nào đấy, mà pattern vẫn thỏa. Tất cả những ký tự còn lại của đoạn khớp sẽ được chuyển vào tài khoản của .*
Ví dụ:
a. Trước \2 không có "biên giới giữa text và không text" nhưng trước "ông anh sinh đôi" của \2 có "biên giới giữa text và không text". Tức "ông anh sinh đôi" của \2 là cả từ trong khi \2 chỉ là một phần của từ: text = "om;em;luc;an;com;"
Rõ ràng không có từ nào lặp nhưng toàn bộ text là đoạn khớp. Regexp sẽ "đẩy" om; vào \2 và chuyển em;luc;an;c vào tài khoản của .*

b. Trước \2 có "biên giới giữa text và không text" nhưng trước "ông anh sinh đôi" của \2 không có "biên giới giữa text và không text". Tức \2 là cả từ trong khi "ông anh sinh đôi" của \2 chỉ là một phần của từ: text = "hom;qua;di;bia;om;"
Đoạn khớp là om;qua;di;bia;om;
om;
-> \2 và qua;di;bia; vào tài khoản của .*

Để có pettern đúng thì phải thêm điều kiện là trước "ông anh sinh đôi" của \2 và trước \2 phải là "biên giới giữa text và không text". Tức \w+ và \2 phải là cả từ chứ không là một phần của từ
Mã:
.Pattern = .Pattern = "((\b\w+;).*\b)\2"
---------------
Về code cũ của tôi
Mã:
"(\w+;)((\w+;)*)\1"
pattern đảm bảo tính chất: trước \1 có "biên giới giữa text và không text" (đó chính là dấu chấm phẩy). Chỉ thế thôi. Từ pattern không suy ra là trước "ông anh sinh đôi" của \1 phải là "biên giới giữa text và không text". Vậy tôi chỉ phải sửa sao cho trước "ông anh sinh đôi" của \1 phải là "biên giới giữa text và không text"
Mã:
.Pattern = "(\b\w+;)((\w+;)*)\1"
-------------
Tôi nhầm vì
Mã:
.Pattern = "(\b\w+;)(.*)\1"
không bắt buộc trước \1 phải là "biên giới giữa text và không text". Vậy pattern đó không chính xác.

Vậy phải là
Mã:
.Pattern = "(\b\w+;)(.*\b)\1"
------------
Tóm lại hiện ta có 3 pattern
Mã:
.Pattern =  "((\b\w+;).*\b)\2"
.Pattern = "(\b\w+;)((\w+;)*)\1"
.Pattern = "(\b\w+;)(.*\b)\1"
 
Upvote 0
pattern này của bạn cũng phạm lỗi như pattern cũ của tôi. Tôi phát hiện ra sự thiếu chính xác trong pattern của mình khi phân tích và nêu các tính chất của pattern.
Trước tiên nói về pattern của bạn
Mã:
.Pattern = "((\w+;).*)\2"

\1 = \w+;.*
"ông anh sinh đôi" của \2 = \w+;
Suy ra:
1. "ông anh sinh đôi" của \2 và cả \2 là chuỗi ký tự thuộc [a-z0-9] và kết thúc bằng dấu chấm phẩy? Chỉ thế thôi. Còn pattern không bắt buộc trước "ông anh sinh đôi" của \2 và \2 phải là "biên giới giữa text và không text". Vậy thì vẫn có trường hợp trước "ông anh sinh đôi" của \2 hoặc trước \2 không có "biên giới giữa text và không text", tức "ông anh sinh đôi" của \2 hoặc \2 chỉ là một phần của từ nào đấy, mà pattern vẫn thỏa. Tất cả những ký tự còn lại của đoạn khớp sẽ được chuyển vào tài khoản của .*
Ví dụ:
a. Trước \2 không có "biên giới giữa text và không text" nhưng trước "ông anh sinh đôi" của \2 có "biên giới giữa text và không text". Tức "ông anh sinh đôi" của \2 là cả từ trong khi \2 chỉ là một phần của từ: text = "om;em;luc;an;com;"
Rõ ràng không có từ nào lặp nhưng toàn bộ text là đoạn khớp. Regexp sẽ "đẩy" om; vào \2 và chuyển em;luc;an;c vào tài khoản của .*

b. Trước \2 có "biên giới giữa text và không text" nhưng trước "ông anh sinh đôi" của \2 không có "biên giới giữa text và không text". Tức \2 là cả từ trong khi "ông anh sinh đôi" của \2 chỉ là một phần của từ: text = "hom;qua;di;bia;om;"
Đoạn khớp là om;qua;di;bia;om;
om;
-> \2 và qua;di;bia; vào tài khoản của .*

Để có pettern đúng thì phải thêm điều kiện là trước "ông anh sinh đôi" của \2 và trước \2 phải là "biên giới giữa text và không text". Tức \w+ và \2 phải là cả từ chứ không là một phần của từ
Mã:
.Pattern = .Pattern = "((\b\w+;).*\b)\2"
---------------
Về code cũ của tôi
Mã:
"(\w+;)((\w+;)*)\1"
pattern đảm bảo tính chất: trước \1 có "biên giới giữa text và không text" (đó chính là dấu chấm phẩy). Chỉ thế thôi. Từ pattern không suy ra là trước "ông anh sinh đôi" của \1 phải là "biên giới giữa text và không text". Vậy tôi chỉ phải sửa sao cho trước "ông anh sinh đôi" của \1 phải là "biên giới giữa text và không text"
Mã:
.Pattern = "(\b\w+;)((\w+;)*)\1"
-------------
Tôi nhầm vì
Mã:
.Pattern = "(\b\w+;)(.*)\1"
không bắt buộc trước \1 phải là "biên giới giữa text và không text". Vậy pattern đó không chính xác.

Vậy phải là
Mã:
.Pattern = "(\b\w+;)(.*\b)\1"
------------
Tóm lại hiện ta có 3 pattern
Mã:
.Pattern =  "((\b\w+;).*\b)\2"
.Pattern = "(\b\w+;)((\w+;)*)\1"
.Pattern = "(\b\w+;)(.*\b)\1"
Anh giải thích dễ hiểu, lúc trước còn mơ hồ thằng "biên giới giữa text và không text" nhưng giờ đã hiểu ý nghĩ của nó rồi, trong pattern thì nên có nó để tránh trường hợp sai sót . Nhưng em nghĩ thay bằng "biên giới giữa hai nước" thì sẽ hợp lý cho mọi trường hợp hơn :p:p:p.Cám ơn anh!!!
 
Upvote 0
"biên giới giữa text và không text" (đó chính là dấu chấm phẩy)
Nói tắt quá sợ hiểu lầm.
\b không là ký tự nào cả. Nó chỉ là "vị trí" giữa ký tự thuộc [a-zA-Z0-9_]) và ký tự [^A-Za-z0-9_]. Tức trong trường hợp trên là "vị trí" giữa 2 ký tự ";" (dấu chấm phẩy) và "\w"

Tương tự như "^" và "$" không là ký tự mà chỉ là "vị trí" đầu và cuối mỗi text (cả đầu và cuối mỗi dòng nếu MultiLine = TRUE)
 
Lần chỉnh sửa cuối:
Upvote 0
Nói tắt quá sợ hiểu lầm.
\b không là ký tự nào cả. Nó chỉ là "vị trí" giữa ký tự [a-z0-9] và [a-z0-9]. Tức trong trường hợp trên là "vị trí" giữa 2 ký tự ";" (dấu chấm phẩy) và "\w"

Tương tự như "^" và "$" không là ký tự mà chỉ là "vị trí" đầu và cuối mỗi text (cả đầu và cuối mỗi dòng nếu MultiLine = TRUE)
Nói giữa thì nghe hơi mơ hồ anh nhỉ, em có đọc file kí tự của anh siwtom, cũng nói là đoạn giữa, nhưng không hiểu , nhưng có ví dụ cũng chỉ hiểu được chút, ví dụ đoạn text: "\saad55asd00dsd-" thì nó chính là "\s\w+-" ví dụ có pattern "\b\w+-" hoặc "\w+\b" nếu hiểu đoạn giữa của nó là khúc nào ???? nếu như anh nói "đường biên giới" thì em sẽ hiểu là "\b\w+-" thì đường biên giới của nó sẽ là \s và "\w+\b" là "-".
 
Upvote 0
\s là 1 ký tự của tập [ \f\n\r\t\v]
\b không là ký tự, chỉ là "vị trí" giữa ký tự thuộc [a-zA-Z0-9_]) và ký tự [^A-Za-z0-9_]. Cũng có nghĩa là vị trí ^ hoặc $ nếu ký tự đầu tiên hoặc cuối cùng trong chuỗi thuộc [A-Za-z0-9_]
Thế bạn hiểu ^ và $ thế nào? Chúng cũng không là ký tự gì cả mà là "vị trí" đầu và cuối chuỗi (cả đầu và cuối mỗi dòng nếu MultiLine = TRUE). Thì \b nó cũng thế, cũng chỉ là "vị trí" thôi chứ có là ký tự gì đâu?

Vài vd. cho dễ hiểu. Tôi có chuỗi text = "Mai17 Hoa21 Nga39 Hanh14"

1. Thế bạn cho ^ nó là ký tự nào? Là "M"? Làm gì có chuyện đó. Thế $ nó là ký tự nào? Là "4"? Làm gì có chuyện đó.

"^" là "vị trí" đầu chuỗi. Hay nói nôm na thì "trước ký tự đầu tiên của chuỗi" có một vị trí vô hình, ta cứ tưởng tượng là chỗ mà ký tự đầu tiên "tiếp xúc" với "thế giới" bên ngoài. Và "chỗ đó" người ta gọi là "^". Về "$" cũng tương tự.

2. Nếu bạn vẫn chưa hiểu về \b thì hãy hiểu như sau:
"\b\w" có nghĩa là hoặc ký tự đại diện bởi \w là ký tự đầu tiên của chuỗi (của dòng) - lúc này \b trùng với ^, có cùng nghĩa với ^ - hoặc trước ký tự đó là ký tự thuộc [^A-Za-z0-9_]

"\w\b" có nghĩa là hoặc ký tự đại diện bởi \w là ký tự cuối cùng của chuỗi (của dòng) - lúc này \b trùng với $, có cùng nghĩa với $ - hoặc sau ký tự đó là ký tự thuộc [^A-Za-z0-9_]

3. Ta làm bài toán cực đơn giản. Xóa từ đầu tiên trong chuỗi. Tất nhiên có nhiều cách nhưng ta xét các cách cụ thể nhằm mục đích giải thích vài chuyện.
Nếu bạn có .Pattern = "\s\w+?\s" thì sẽ chỉ tìm thấy các từ thứ 2, 3. Bởi trước từ đầu tiên và sau từ cuối cùng không có ký tự nào là "dấu cách", TAB, vbCr, vbLf ... Chú ý: \s là ký tự thuộc [ \f\n\r\t\v].
Nếu có .Pattern = "^\w+?\s" thì tìm thấy và chỉ tìm thấy từ đầu tiên thôi. Bạn thấy rõ ràng trước "M" không có ký tự nào. Chỉ có "vị trí", "nơi "tiếp xúc" gọi là "^"

4. Bạn có text = "Mai tieu thu, Hong mat nau, My Ha noi choi than voi Hoa (Manh la ban cua ca hai)"
Bài toán: Tìm tất cả các tên có chữ cái đầu là "M"
Bạn không thể dùng "\s\M\w*?\s" được vì trước "Mai" không có ký tự nào, vì trước "Manh" là ký tự "(" không thuộc tập [ \f\n\r\t\v]. Tức bạn chỉ tìm thấy " My "
Nếu .Pattren = "\bM\w*?\b" thì bạn tìm thấy hết. Vì trước "Mai" là "^" cùng nghĩa với "\b", trước "My" là "dấu cách", vậy giữa "dấu cách" (thuộc tập [^A-Za-z0-9_]) và "M" (thuộc tập [A-Za-z0-9_]) có "\b", trước "Manh" là ký tự "(", vậy giữa ký tự "(" (thuộc tập [^A-Za-z0-9_]) và "M" (thuộc tập [A-Za-z0-9_]) có "\b". Tức bạn tìm thấy hết.

Bạn thấy sự khác nhau giữa \s và \b chưa?

Ngoài ra vì \s là ký tự nên bạn "nhìn" thấy ký tự này trong đoạn khớp. Tức đoạn khớp không phải là "My" mà là " My ". Trong khi dùng \b thì bạn chả có thêm ký tự nào trong đoạn khớp, vì \b không là ký tự. Nó chỉ là vị trí mà người ta qui ước với nhau là cái chỗ đó chỗ đó gọi là \b. Thế thôi.

Tóm lại cứ hiểu nôm na là \s là ký tự cụ thể của một tập ký tự cụ thể, tức có thể cân, đo, đong, đếm và nhìn thấy được. Trong khi đó ^, $, \b là "khái niệm"

Thôi không quan trọng từ ngữ nữa. Dù là "khái niệm", "vị trí", "biên giới" thì cứ hiểu:
Nếu tìm thấy đoạn khớp mà:
- pattern = "\b\w..." thì trong text nguồn trước đoạn khớp đó không thể cũng là ký tự \w
- pattern = "...\w\b" thì trong text nguồn sau đoạn khớp đó không thể cũng là ký tự \w
- pattern = "\b\W..." thì trong text nguồn trước đoạn khớp đó không thể cũng là ký tự \W
- pattern = "...\W\b" thì trong text nguồn sau đoạn khớp đó không thể cũng là ký tự \W
 
Upvote 0
\s là 1 ký tự của tập [ \f\n\r\t\v]
\b không là ký tự, chỉ là "vị trí" giữa ký tự thuộc [a-zA-Z0-9_]) và ký tự [^A-Za-z0-9_]. Cũng có nghĩa là vị trí ^ hoặc $ nếu ký tự đầu tiên hoặc cuối cùng trong chuỗi thuộc [A-Za-z0-9_]
Thế bạn hiểu ^ và $ thế nào? Chúng cũng không là ký tự gì cả mà là "vị trí" đầu và cuối chuỗi (cả đầu và cuối mỗi dòng nếu MultiLine = TRUE). Thì \b nó cũng thế, cũng chỉ là "vị trí" thôi chứ có là ký tự gì đâu?

Vài vd. cho dễ hiểu. Tôi có chuỗi text = "Mai17 Hoa21 Nga39 Hanh14"

1. Thế bạn cho ^ nó là ký tự nào? Là "M"? Làm gì có chuyện đó. Thế $ nó là ký tự nào? Là "4"? Làm gì có chuyện đó.

"^" là "vị trí" đầu chuỗi. Hay nói nôm na thì "trước ký tự đầu tiên của chuỗi" có một vị trí vô hình, ta cứ tưởng tượng là chỗ mà ký tự đầu tiên "tiếp xúc" với "thế giới" bên ngoài. Và "chỗ đó" người ta gọi là "^". Về "$" cũng tương tự.

2. Nếu bạn vẫn chưa hiểu về \b thì hãy hiểu như sau:
"\b\w" có nghĩa là hoặc ký tự đại diện bởi \w là ký tự đầu tiên của chuỗi (của dòng) - lúc này \b trùng với ^, có cùng nghĩa với ^ - hoặc trước ký tự đó là ký tự thuộc [^A-Za-z0-9_]

"\w\b" có nghĩa là hoặc ký tự đại diện bởi \w là ký tự cuối cùng của chuỗi (của dòng) - lúc này \b trùng với $, có cùng nghĩa với $ - hoặc sau ký tự đó là ký tự thuộc [^A-Za-z0-9_]

3. Ta làm bài toán cực đơn giản. Xóa từ đầu tiên trong chuỗi. Tất nhiên có nhiều cách nhưng ta xét các cách cụ thể nhằm mục đích giải thích vài chuyện.
Nếu bạn có .Pattern = "\s\w+?\s" thì sẽ chỉ tìm thấy các từ thứ 2, 3. Bởi trước từ đầu tiên và sau từ cuối cùng không có ký tự nào là "dấu cách", TAB, vbCr, vbLf ... Chú ý: \s là ký tự thuộc [ \f\n\r\t\v].
Nếu có .Pattern = "^\w+?\s" thì tìm thấy và chỉ tìm thấy từ đầu tiên thôi. Bạn thấy rõ ràng trước "M" không có ký tự nào. Chỉ có "vị trí", "nơi "tiếp xúc" gọi là "^"

4. Bạn có text = "Mai tieu thu, Hong mat nau, My Ha noi choi than voi Hoa (Manh la ban cua ca hai)"
Bài toán: Tìm tất cả các tên có chữ cái đầu là "M"
Bạn không thể dùng "\s\M\w*?\s" được vì trước "Mai" không có ký tự nào, vì trước "Manh" là ký tự "(" không thuộc tập [ \f\n\r\t\v]. Tức bạn chỉ tìm thấy " My "
Nếu .Pattren = "\bM\w*?\b" thì bạn tìm thấy hết. Vì trước "Mai" là "^" cùng nghĩa với "\b", trước "My" là "dấu cách", vậy giữa "dấu cách" (thuộc tập [^A-Za-z0-9_]) và "M" (thuộc tập [A-Za-z0-9_]) có "\b", trước "Manh" là ký tự "(", vậy giữa ký tự "(" (thuộc tập [^A-Za-z0-9_]) và "M" (thuộc tập [A-Za-z0-9_]) có "\b". Tức bạn tìm thấy hết.

Bạn thấy sự khác nhau giữa \s và \b chưa?

Ngoài ra vì \s là ký tự nên bạn "nhìn" thấy ký tự này trong đoạn khớp. Tức đoạn khớp không phải là "My" mà là " My ". Trong khi dùng \b thì bạn chả có thêm ký tự nào trong đoạn khớp, vì \b không là ký tự. Nó chỉ là vị trí mà người ta qui ước với nhau là cái chỗ đó chỗ đó gọi là \b. Thế thôi.

Tóm lại cứ hiểu nôm na là \s là ký tự cụ thể của một tập ký tự cụ thể, tức có thể cân, đo, đong, đếm và nhìn thấy được. Trong khi đó ^, $, \b là "khái niệm"

Thôi không quan trọng từ ngữ nữa. Dù là "khái niệm", "vị trí", "biên giới" thì cứ hiểu:
Nếu tìm thấy đoạn khớp mà:
- pattern = "\b\w..." thì trong text nguồn trước đoạn khớp đó không thể cũng là ký tự \w
- pattern = "...\w\b" thì trong text nguồn sau đoạn khớp đó không thể cũng là ký tự \w
- pattern = "\b\W..." thì trong text nguồn trước đoạn khớp đó không thể cũng là ký tự \W
- pattern = "...\W\b" thì trong text nguồn sau đoạn khớp đó không thể cũng là ký tự \W
Cái ^ và $ em đã hiểu từ khi xem cái bảng kí tự rồi , còn cái \b thì lúc đấy chưa nắm rõ năm nhưng theo mấy bài trên anh nói thì đã hiểu rồi ạ, thằng ^ và $ kiểu như dạng bắt buộc phải thực hiện từ đầu chuỗi hoặc cuối chuỗi , còn thằng \b thì rộng hơn chút nó là khoảng biên giới của các đoạn trong chuỗi, vì vậy trong một số trường hợp ^ và $ và \b có thể là như nhau. Còn \s em dùng nó như khoảng trắng thôi ạ, dù biết nó còn đại diện cho nhiều kí tự khác, do chưa dùng tới nên chỉ cần vậy là đủ. Cách kết hợp pattern này cũng không phải dễ, mỗi người có thể hiểu theo nhiểu cách khác nhau nhưng miễn sao hiểu đúng là được, đôi khi hiểu diễn giải ra thì thấy không hợp lý , do em cũng mới tìm hiểu đây nên còn nhiều cái chưa rõ nên được anh giải thích nên năm thêm được nhiều cái lắm ạ.
Em có chút thắc mắc trong cái ví dụ trên
text = "Mai tieu thu, Hong mat nau, My Ha noi choi than voi Hoa (Manh la ban cua ca hai)"
Anh dùng pattern="\bM\w*?\b" , dấu ? có bị dư không anh em nghĩ viết vầy "\bM\w*\b" cũng được, vì đã có * rồi sao lại thêm ?, * là xuất hiện >=0 lần, ? có thể xuất hiện hoặc không (0 hoặc 1 lần)
Nếu chỉ tách tên có chữ M như ví dụ của anh thì em nghĩ không cần dùng \b cũng được, có thể dùng các pattern này: "M\w*" hoặc "M\S*" (cài này có thể sai nếu các kí tự tập \S nằm cuối và kế là khoảng trắng)
Cám ơn anh đã nhiệt tình giải thích!!!
 
Lần chỉnh sửa cuối:
Upvote 0
Em có chút thắc mắc trong cái ví dụ trên
text = "Mai tieu thu, Hong mat nau, My Ha noi choi than voi Hoa (Manh la ban cua ca hai)"
Anh dùng pattern="\bM\w*?\b" , dấu ? có bị dư không anh em nghĩ viết vầy "\bM\w*\b" cũng được, vì đã có * rồi sao lại thêm ?, * là xuất hiện >=0 lần, ? có thể xuất hiện hoặc không (0 hoặc 1 lần)
Đúng là thừa. Tôi hì hục viết trong notepad, cũng không suy nghĩ nhiều.
Nếu chỉ tách tên có chữ M như ví dụ của anh thì em nghĩ không cần dùng \b cũng được, có thể dùng các pattern này: "M\w*" hoặc "M\S*"
Tôi không lặp lại thôi chứ xuyên suốt toàn bộ bài viết là tinh thần: "Tất nhiên có nhiều cách nhưng ta xét các cách cụ thể nhằm mục đích giải thích vài chuyện." đã viết ở điểm 3.
Ngoài ra tôi lấy vd. cụ thể như thế nhưng pattern phải là cho chuỗi tuỳ ý một chút.

Đúng là thừa \b cuối. Tức phải là Pattern = "\bM\w*"

Nếu
text = "Mai tieu thu, Hong mat nau, My Ha noi choi than voi Hoa (Manh la ban cua ca hai), con Ang dep trai thich Ai xinh gai"
và tìm các tên bắt đầu bằng A (hoặc a)
thì với pattern = "A\w*" hoặc pattern = "A\S*"

là sai rồi còn gì? Vd. như kết quả "ai" (từ Mai), "at" (từ mat), "au" (từ nau), "a" (từ Ha), "an" (từ than), "anh" (từ Manh) đâu có phải là kết quả mong đợi?

Còn nếu với pattern = "\bA\w*" thì chỉ tìm thấy "Ang", "Ai" thôi.

Tất nhiên tất cả chỉ là vd. đơn giản chỉ dùng với mục đích để giải thích và tìm hiểu về các mẫu pattern mà thôi. Trong thực tế ta phải phân tích: chuỗi nguồn có dạng thế nào, yêu cầu phải làm gì. Lúc đó mới có thể viết pattern.

Thực ra bài viết chỉ với mục đích giải thích cho bạn về ^, $, \s, \b chứ mục đích không phải là tìm các pattern ngắn gọn nhất, hay nhất, đẹp nhất. Vì thế tôi không suy nghĩ nhiều, không tập trung vào việc tìm pattern ngắn nhất, hay nhất. Chỉ lấy vd. đơn giản để giải thích về ^, $, \s, \b thôi. Khi bạn đã hiểu về chúng thì bạn có thể tự viết pattern cho mình, sửa pattern của mình, của tôi, của người khác sao cho ngắn gọn hơn.
 
Lần chỉnh sửa cuối:
Upvote 0
Đúng là thừa. Tôi hì hục viết trong notepad, cũng không suy nghĩ nhiều.

Tôi không lặp lại thôi chứ xuyên suốt toàn bộ bài viết là tinh thần: "Tất nhiên có nhiều cách nhưng ta xét các cách cụ thể nhằm mục đích giải thích vài chuyện." đã viết ở điểm 3.
Ngoài ra tôi lấy vd. cụ thể như thế nhưng pattern phải là cho chuỗi tuỳ ý một chút.

Đúng là thừa \b cuối. Tức phải là Pattern = "\bM\w*"

Nếu
text = "Mai tieu thu, Hong mat nau, My Ha noi choi than voi Hoa (Manh la ban cua ca hai), con Ang dep trai thich Ai xinh gai"
và tìm các tên bắt đầu bằng A (hoặc a)
thì với pattern = "A\w*" hoặc pattern = "A\S*"

là sai rồi còn gì? Vd. như kết quả "ai" (từ Mai), "at" (từ mat), "au" (từ nau), "a" (từ Ha), "an" (từ than), "anh" (từ Manh) đâu có phải là kết quả mong đợi?

Còn nếu với pattern = "\bA\w*" thì chỉ tìm thấy "Ang", "Ai" thôi.

Tất nhiên tất cả chỉ là vd. đơn giản chỉ dùng với mục đích để giải thích và tìm hiểu về các mẫu pattern mà thôi. Trong thực tế ta phải phân tích: chuỗi nguồn có dạng thế nào, yêu cầu phải làm gì. Lúc đó mới có thể viết pattern.

Thực ra bài viết chỉ với mục đích giải thích cho bạn về ^, $, \s, \b chứ mục đích không phải là tìm các pattern ngắn gọn nhất, hay nhất, đẹp nhất. Vì thế tôi không suy nghĩ nhiều, không tập trung vào việc tìm pattern ngắn nhất, hay nhất. Chỉ lấy vd. đơn giản để giải thích về ^, $, \s, \b thôi. Khi bạn đã hiểu về chúng thì bạn có thể tự viết pattern cho mình, sửa pattern của mình, của tôi, của người khác sao cho ngắn gọn hơn.
Vâng, cảm ơn anh nhiều ạ!!!
 
Upvote 0

Bài viết mới nhất

Back
Top Bottom