1. Tải bản cài đặt AutoIT mới nhất

    Chào Khách. Nếu bạn mới tham gia và chưa cài đặt AutoIT.
    Vui lòng vào topic trên để tải bản AutoIT mới nhất nhé
    Dismiss Notice
  2. Quy định và nội quy

    Chào Khách. Vui lòng đọc kỹ nội quy và quy định của diễn đàn
    Để tránh bị ban một cách đáng tiếc nhé!
    Dismiss Notice
  3. Hướng dẫn chèn mã AutoIT trong diễn đàn

    Chào Khách. Vui lòng xem qua bài viết này
    Để biết cách chèn mã AutoIT trong diễn đàn bạn nhé :)
    Dismiss Notice

Hướng dẫn Regexp - Regular Expression & Ứng dụng - Chi tiết dễ hiểu, cập nhật hàng tuần - LHTP

Thảo luận trong 'Hướng dẫn - Bài tập nâng cao' bắt đầu bởi Huân Hoàng, 20/1/17.

  1. Huân Hoàng

    Huân Hoàng Administrator Thành viên BQT Administrator Super Moderator
    • 93/113

    Tham gia ngày:
    29/9/15
    Bài viết:
    642
    Đã được thích:
    1,181
    I/ LỜI GIỚI THIỆU

    - Chắc hẳn ai đã và đang viết code cũng phải biết đến Regexp (Biểu thức chính quy), một kỹ thuật phân tích sự trùng khớp của một tập hợp các chuỗi nào đó, để có thể giải quyết vấn đề được đặt ra. Đây là một chủ đề khó nuốt với nhiều bạn, nhất là những bạn mới tập tễnh bước vào lĩnh vực lập trình.

    - Nay bạn
    Lê Hoàng Thiên Phúc (FB: https://www.facebook.com/mmo.acc1), thành viên tiêu biểu của nhóm lập trình AutoIT Việt (https://www.facebook.com/groups/autoitscript/)đã tổng hợp từ kiến thức và kinh nghiệm của mình để viết bài hướng dẫn này, sẽ giúp các bạn có thêm một sự lựa chọn để tiếp cận dễ dàng hơn Regexp trong vô vàn những TUT về regexp trên mạng.

    - Mạch bài viết sẽ là chọn một vấn đề rồi giới thiệu từ từ về RegExp thông qua quá trình giải quyết vấn đề đó. Đây có lẽ là một hướng đi đúng đắn, bởi Regexp là cái gì đó khá trừu tượng mà những lý thuyết khô cứng sẽ không mang lại hiệu quả tốt nhất.

    - Bài viết này sẽ được cập nhật dựa thường xuyên theo tiến độ của tác giả, bạn có thể theo dõi thông qua Blog sau: https://inkd-blog.blogspot.com/


    :autoit:

    II/ BÀI HƯỚNG DẪN

    (19/01/2017)
    Phần 1
    Giới thiệu về RegExp và một số metacharacter

    Bắt đầu với một vấn đề đơn giản: Thay thế tất cả từ cat bằng dog trong một chuỗi. Giải pháp đầu tiên trong đầu bạn là gì? StringReplace($string, "cat", "dog")? Vấn đề này có vẻ đơn giản quá nhỉ! Nhưng, không. Sau khi chạy thử với chuỗi free cat food category nhận về được kết quả là free dog food dogegory, bạn sẽ bắt đầu hoang mang về độ chính xác mà đoạn code trên mang lại. cat trong category cũng bị thay thế, kết quả này không ổn tí nào! May mắn thay, vẫn còn một hàm khác để thay thế, đó là StringRegExpReplace. Để dùng hàm này thì phải biết RegExp. Vậy giờ phải làm gì? Tất nhiên là tìm hiểu RegExp rồi :v

    RegExp (hoặc regex) là viết tắt của Regular Expression, dịch ra tiếng Việt là "biểu thức chính quy". Một đoạn RegExp là một pattern (khuôn mẫu) của chuỗi cần tìm. Ví dụ với một số nguyên nhị phân, pattern của nó sẽ là "[01]+". Nếu bạn nhìn vào không hiểu gì cũng đừng lo, vì đã học đâu mà hiểu :))


    StringRegExp và StringRegExpReplace
    AutoIt có 2 hàm sử dụng RegExp là StringRegExp StringRegExpReplace. Cái tên cũng nói lên chức năng của nó rồi, StringRegExp dùng để tách chuỗi, còn StringRegExpReplace thì thay thế chuỗi, cả hai đều tìm kiếm dựa trên pattern. Cách dùng cũng khá đơn giản:

    StringRegExp($input_string, $pattern, $flag)
    StringRegExpReplace($input_string, $pattern, $replacement)​

    Còn tham số phụ đằng sau nữa, nhưng để cho đơn giản mình sẽ bỏ qua không đề cập đến. Ở 2 dòng code trên:
    • $input_string là chuỗi nguồn. Ví dụ với vấn đề ở đầu bài thì $input_string là free cat food category.
    • $pattern là cái RegExp pattern mình sẽ dùng để tìm kiếm/thay thế.
    • $flag để chỉnh chế độ hoạt động của StringRegExp. Có 5 giá trị lần lượt từ 0 đến 4, nhưng ở đây mình sẽ ưu tiên dùng flag 3 để tách chuỗi, dùng flag 0 để kiểm tra chuỗi. Các flag còn lại mình sẽ không đề cập đến.
    • $replacement là chuỗi dùng để thay thế đoạn được match.
    • StringRegExp trả về một mảng nếu match thành công.
    • StringRegExpReplace trả về chuỗi sau khi đã replace
    Để tiện việc kiểm tra giá trị, khi dùng StringRegExp mình sẽ cho hiển thị kết quả bằng hàm _ArrayDisplay, còn StringRegExpReplace thì mình sẽ dùng MsgBox để hiển thị kết quả.


    Hai anchor thông dụng: Start of string & end of string
    Quay trở lại với vấn đề ở đầu bài, StringReplace không phân biệt được một từ hoàn chỉnh với một phần của từ nên thất bại. Vậy thêm một khoảng trắng trước và sau từ cat thì thế nào nhỉ? Thử nào:

    Mã (AutoIt):
    $string = 'free cat food category'
    $result = StringReplace($string, ' cat ', ' dog ')
    MsgBox(0, '', $result)
    Wow, chạy mượt như vừa gội Sunsilk, cơ mà trật lất cmnr =)) Nếu cat đứng ở đầu hoặc ở cuối chuỗi thì đoạn code trên sẽ bỏ qua không thay thế. Bây giờ thử chuyển sang StringRegExpReplace xem nào.

    Mã (AutoIt):
    $string = 'free cat food category'
    $result = StringRegExpReplace($string, ' cat ', ' dog ')
    MsgBox(0, '', $result)
    Kết quả vẫn chẳng có gì thay đổi, nhưng từ đây chúng ta sẽ có thể sử dụng RegExp (hàm StringReplace không hỗ trợ tìm kiếm bằng RegExp). Nhìn lại vấn đề vừa rồi nào! Từ cat đứng đầu hoặc cuối chuỗi thì lỗi. Chúng ta sẽ thay đổi lại một tí: một từ sẽ đứng đầu dòng hoặc có 1 khoảng trắng trước nó, đồng thời từ đó sẽ đứng cuối chuỗi hoặc có 1 khoảng trắng theo sau. Vậy giờ làm sao để cho máy nó biết mà phân biệt vị trí của từ? Đây, đến giờ RegExp tỏa sáng rồi. RegExp có dành ra một số ký tự để dùng với ý nghĩa đặc biệt, trong đó có ^$. Hai ký tự này gọi là anchor (móc), nó không biểu diễn một ký tự nào cả, nhưng nó biểu diễn vị trí (nếu vẫn thấy rối, cứ từ từ đọc tiếp đến chỗ ví dụ).
    • Ký tự ^ match vị trí trước ký tự đầu tiên của chuỗi.
    • Ký tự $ thì match vị trí sau ký tự cuối cùng của chuỗi (thực ra nó hơi rắc rối hơn tí, nhưng mà phần chi tiết đó để sau nói).
    Ví dụ với chuỗi abc, các bạn cứ tưởng tượng như giữa 2 ký tự liên tiếp nhau, trước ký tự đầu, sau ký tự cuối có một chỗ trống, chỗ đó là chỗ để anchor match đấy. Với ^ thì nó là chỗ trống (tưởng tượng) trước a, với $ thì nó là chỗ trống sau c.

    Lựa chọn thay thế (Alternation)
    Đến đây nếu bạn hiểu được mấy câu mình lảm nhảm ở trên thì chắc sẽ xác định được pattern cần dùng rồi, đó là: ^cat$ (trường hợp chuỗi chỉ có đúng 1 từ), ^cat (từ nằm ở đầu chuỗi), cat$ (từ ở cuối chuỗi), cat (từ nằm giữa chuỗi). Nhưng mà StringRegExpReplace nhận có 1 pattern, làm sao nhồi hết đống này vào được? Viết 4 dòng StringRegExpReplace ra thì nó ngáo không khác gì copy paste code thay vì chạy vòng lặp vậy. Trường hợp như vầy không hiếm, nên tất nhiên là phải có giải pháp rồi. Giới thiệu với các bạn một ký tự đặc biệt khác là |. Ký tự này để biểu diễn một lựa chọn thay thế, dùng để nối 4 cái pattern trên lại thì chuẩn cơm mẹ nấu rồi. Áp dụng ngay và luôn:
    Mã (AutoIt):
    $string = 'cat free cat food category cat'
    $result = StringRegExpReplace($string, '^cat$|^cat | cat$| cat ', ' dog ')
    MsgBox(0, '', $result)
    Gom nhóm (Group)
    Sau khi sửa pattern, output đoạn code trên trông vẫn chuẩn chưa có gì bất thường, nhưng mà cái pattern dài dòng quá, tìm cách rút gọn nó thôi! Ở trước chỉ có thể là vị trí đầu chuỗi hoặc một khoảng trắng, ở sau thì cũng tương tự: chỉ có thể là khoảng trắng hoặc vị trí cuối chuỗi. Nhưng nếu viết ^| cat |$ thì nó lại thành ^ hoặc cat hoặc $ rồi, không được. Vâng, xin giới thiệu với các bạn một thành viên nữa của hội những ký tự đặc biệt trong RegExp: cặp ngoặc tròn ( ). Cặp ngoặc này dùng để nhóm các ký tự (và cả anchor) lại với nhau thành một khối. Với cặp ngoặc này chúng ta có thể viết lại pattern dài dòng ở trên thành (^| )cat( |$). Chạy thêm nháy nữa để xem kết quả có bị sai sau khi sửa pattern không.
    Mã (AutoIt):
    $string = 'cat free cat food category cat'
    $result = StringRegExpReplace($string, '(^| )cat( |$)', ' dog ')
    MsgBox(0, '', $result)
    Tham chiếu ngược (Backreference)
    Ngon lành cành đào, pattern ngắn đi và kết quả không bị thay đổi. Nhưng mà, ô kìa, có vài khoảng trắng đi lạc đâu vô đầu với cuối chuỗi rồi :)) Nhắc lại một tí cho nhớ: ^$ là 2 anchor, anchor chỉ match vị trí chứ không đụng chạm gì đến ký tự xung quanh nó. Nhưng mà trong chuỗi replace thì luôn thêm 1 khoảng trắng vào trước và sau chữ dog, nên kết quả là đầu và cuối chuỗi bây giờ lại có khoảng trắng. Vấn đề này có vẻ nhỏ, StringStripWS phát là xong ngay, nhưng mà đang học RegExp, làm người ai làm thế :v Để không còn khoảng trắng dư ở đầu và cuối chuỗi nữa thì bên replacement chỉ được thêm khoảng trắng vào nếu bên pattern match được khoảng trắng. Anchor là để chỉ vị trí, không phải ký tự, nên nếu match 1 anchor thì kết quả nhận về là một chuỗi rỗng, liên quan không ta? Hình như có hơi hơi rồi :v Nếu ở đầu dòng thì nó sẽ match ^ thay vì khoảng trắng, nếu ở cuối chuỗi thì nó match $ thay vì khoảng trắng. Hai vị trí đó cũng là hai vị trí mà chúng ta không cần thêm khoảng trắng vào. À nãy ở đoạn trước quên nói, nhóm được tạo bởi ( ) là một capturing-group, có nghĩa là nó sẽ lưu cái kết quả match được của group đó lại (tất nhiên là để xài rồi) :v Truy cập cái giá trị đã được match gọi là tham chiếu ngược. Tham chiếu ngược có thể được xài trong pattern hoặc trong replacement. Ở trường hợp này thì rất rõ ràng, replacement cần truy cập giá trị match được. Mỗi một capturing-group sẽ cho một kết quả nếu match thành công, các kết quả này được đánh số từ 1, để truy cập thì viết \ theo sau là số thứ tự của cái group cần lấy. Để truy cập giá trị (^| ) match được chúng ta viết \1, với cặp ngoặc kế tiếp ( |$) thì viết là \2. Đến đây thì gợi ý cho bước kế tiếp để sửa chữa code đã có rồi: Thay khoảng trắng ở replacement thành \1\2. Sau đó nữa thì... Sửa xong rồi, chạy thôi chứ sau đó gì nữa :v Ở đoạn code dưới mình nối thêm 2 ký tự | vào chuỗi kết quả để dễ quan sát hơn (MsgBox có bỏ trống một khoảng ở 2 bên lề nên nhìn khoảng trắng hơi khó)
    Mã (AutoIt):
    $string = 'cat free cat food category cat'
    $result = StringRegExpReplace($string, '(^| )cat( |$)', ' dog ')
    MsgBox(0, '', '|' & $result & '|')
    Chế độ không phân biệt hoa thường (Case-insensitive mode)
    Một lưu ý bên lề là RegExp phân biệt hoa thường, nên cat sẽ không match CAT. Để regex có thể bỏ qua vấn đề chữ hoa chữ thường, chúng ta cần thêm (?i) vào đầu pattern.

    Metacharacter
    - Từ nãy đến giờ mình đã giới thiệu các ký tự có ý nghĩa đặc biệt như ^, $, ... Các ký tự như vậy gọi là metacharacter. Ký tự \ cũng là một metacharacter. Để biểu diễn một ký tự ^ bạn cần viết \^, nó sẽ hiểu là "ký tự ^" thay vì "vị trí đầu chuỗi", nói cách khác thì thêm \ vào trước metacharacter sẽ làm mất đi ý nghĩa đặc biệt của nó.
    - Lưu ý quan trọng: Nếu muốn biểu diễn một ký tự \ bạn cần viết \\ ngay cả trong $replacement.

    Tóm tắt phần 1
    • Anchor match vị trí trong chuỗi: ^ match đầu chuỗi, $ match cuối chuỗi.
    • | để đưa ra lựa chọn thay thế. a|b sẽ match ký tự a, hoặc ký tự b.
    • ( ) để tạo một capturing-group, các ký tự bên trong được nhóm thành một nhóm, kết quả sau khi match của nhóm này sẽ được lưu lại.
    • Kết quả match được đánh số từ 1, \1 để truy cập kết quả thứ 1, \2 để truy cập kết quả thứ 2, tương tự cho các kết quả còn lại (nếu có).
    • RegExp phân biệt chữ hoa và chữ thường. Thêm option (?i) vào để chuyển sang chế độ không phân biệt hoa thường.
    • Để bỏ đi ý nghĩa đặc biệt của metacharacter, thêm \ vào trước nó.
    Tổng kết phần 1
    Phần 1 chỉ giới thiệu đến các bạn được một phần rất nhỏ của RegExp, nếu chỉ đọc tới đây thì các bạn vẫn chưa thể viết những pattern thật sự hiệu quả. Ở những bài viết sau mình sẽ giới thiệu thêm những thứ khác hấp dẫn hơn nữa. Hy vọng sau loạt bài này các bạn sẽ có thêm một công cụ mới để xử lý chuỗi cách hiệu quả. Hẹn gặp lại các bạn vào tuần sau.



    ---------------------***----------------------​
    (20/01/2017)
    Phần 2
    Character types & quantifiers

    Thử tưởng tượng một ngày nào đó bạn có một đoạn văn bản dài gấp vài trăm lần như thế này:
    Mã (Text):
    0942607853 - Bửu,Cần Thơ
    0914838746 Liên, Ninh Bình
    0974668082 Lâm, Cà Mau
    0926813634 Hào, Hà Nội
    0965667320 - Long, Đồng Tháp
    01213566732 - Duy, Sóc Trăng
    Và bạn cần phải lọc tên, số điện thoại và cả địa chỉ ra để sử dụng... Thằng cha nào gửi cái đoạn này chắc hẳn rất thích làm khó người khác, viết chẳng thống nhất gì hết, chỗ thì dùng dấu gạch để phân cách, chỗ thì một đống khoảng trắng, chỗ thì lại chỉ có đúng 1 khoảng trắng. Nhưng mà nếu bạn đang cần thì ngoài rủa thầm trong bụng ra còn làm gì được nữa đâu. Vấn đề chính là bạn phải lấy được dữ liệu bạn cần ra từ cái đống hỗn độn này. Hôm nay mình sẽ dùng vấn đề này làm ví dụ để giới thiệu thêm về những khả năng của RegExp cho các bạn.

    Lưu ý: Mình sẽ dùng từ metacharacter (một ký tự có ý nghĩa đặc biệt) và metasequence (một dãy các ký tự có ý nghĩa đặc biệt khi đi cùng nhau) với ý nghĩa như nhau để giữ bài viết đơn giản.

    Vấn đề 1: Số điện thoại là một dãy số
    - Điều này chắc hẳn ai cũng nhìn ra, và dĩ nhiên, RegExp có sẵn một metacharacter để dùng trong trường hợp này: \d. Thằng \d sẽ match 1 chữ số hệ thập phân. Một số điện thoại có 10 hoặc 11 chữ số. Pattern để lấy số điện thoại sẽ là gì? \d\d\d\d\d\d\d\d\d\d|\d\d\d\d\d\d\d\d\d\d\d chăng? Chỉ mỗi việc đếm xem có bao nhiêu cái \d thôi đủ mệt rồi. Nhưng không sao, RegExp có quantifier để mô tả số lần xuất hiện mà, tại nãy chưa nói thôi :))
    - Để biểu diễn sự xuất hiện liên tục từ x đến y lần, chúng ta viết {x,y} với x, y là 2 số nguyên cụ thể. Nếu bạn chỉ muốn giới hạn số lần xuất hiện tối thiểu, có thể bỏ trống chỗ y (tức là {x,}). Quantifier không đi một mình, mà nó theo sau một metacharacter, một ký tự bình thường, hoặc một group. Kết hợp 2 cái trên lại được pattern \d{10,11}, ngắn hơn, dễ đọc hơn và cho ra cùng kết quả. Tới đây tạm dừng để thử xem kết quả có như ý không.
    Mã (AutoIt):
    #include <Array.au3>
    $string = "0942607853 - Bửu,Cần Thơ" & @CRLF & " 0914838746 Liên, Ninh Bình" & @CRLF & "0974668082 Lâm, Cà Mau" & @CRLF & "0926813634 Hào, Hà Nội" & @CRLF & "0965667320 - Long, Đồng Tháp" & @CRLF & " 01213566732 -Duy, Sóc Trăng"
    $result = StringRegExp($string, '\d{10,11}', 3)
    _ArrayDisplay($result)
    ; \d{10,11} nghĩa là "10 hoặc 11 chữ số thập phân"
    ; Lưu ý: KHÔNG có khoảng trắng sau dấu phẩy.
    Output chuẩn rồi, duyệt. Qua phần kế: tách tên ra khỏi chuỗi.

    Vấn đề 2: Sau số điện thoại là khoảng trắng, có thể có dấu gạch
    Đến đây, bạn cần phải bỏ qua phần phân cách, gồm có một hoặc nhiều khoảng trắng, theo sau nó có thể là một dấu gạch nối, rồi sau đó lại có thể có thêm một lượng bất kỳ khoảng trắng. Dựa vào đoạn mô tả trên và những gì đã được giới thiệu ở đoạn trước, bạn sẽ có phần kế tiếp của pattern là: {1,}-{0,1} {0,}. Để không bị dính phần phân cách vào kết quả, dùng ( ) để nhóm và lưu phần cần match lại. Test thử đoạn code:
    Mã (AutoIt):
    #include <Array.au3>
    $string = "0942607853 - Bửu,Cần Thơ" & @CRLF & " 0914838746 Liên, Ninh Bình" & @CRLF & "0974668082 Lâm, Cà Mau" & @CRLF & "0926813634 Hào, Hà Nội" & @CRLF & "0965667320 - Long, Đồng Tháp" & @CRLF & " 01213566732 -Duy, Sóc Trăng"
    $result = StringRegExp($string, '(\d{10,11}) {1,}-{0,1} {0,}', 3)
    _ArrayDisplay($result)
    Pattern này hoạt động được, nhưng vẫn còn có thể rút gọn được. 1 hoặc nhiều lần xuất hiện, 0 hoặc nhiều lần xuất hiện, có thể xuất hiện hoặc không. cả 3 cái này đều thường xuyên được sử dụng. RegExp có riêng 3 metacharacter cho 3 cái trên, lần lượt là + (xuất hiện 1 hoặc nhiều lần), * (xuất hiện 0 hoặc nhiều lần), và ? (có thể có hoặc không). Với 3 metacharacter này pattern của chúng ta có thể rút gọn lại còn (\d{10,11}) +-? *.

    Vấn đề 3: Từ có thể có số hoặc dấu gạch chân _
    Nghe có vẻ sai sai, nhưng đây là những gì mà RegExp thực sự cung cấp cho chúng ta. Metacharacter \w match 1 chữ cái, một số, hoặc dấu gạch chân, nói chung là "word" character (ký tự từ). Mặc dù nó có thể match cả số và dấu gạch chân, nhưng ở đây khi kiểm tra qua dữ liệu thấy không có số và dấu gạch chân nào, lười quá nên thôi cứ dùng luôn cho nhanh gọn. Tên gồm 1 hoặc nhiều ký tự (cho thím nào đang thắc mắc, Ý là tên tiếng Việt có 1 ký tự). Phần này cần lấy chứ không bỏ, nên sẽ bao nó lại trong một group riêng. Pattern cho đoạn này: (\w+)
    Mã (AutoIt):
    #include <Array.au3>
    $string = "0942607853 - Bửu,Cần Thơ" & @CRLF & " 0914838746 Liên, Ninh Bình" & @CRLF & "0974668082 Lâm, Cà Mau" & @CRLF & "0926813634 Hào, Hà Nội" & @CRLF & "0965667320 - Long, Đồng Tháp" & @CRLF & " 01213566732 -Duy, Sóc Trăng"
    $result = StringRegExp($string, '(\d{10,11}) +-? *(\w+)', 3)
    _ArrayDisplay($result)
    Thay vào code rồi chạy thử thì ôi thôi, nó bỏ qua gần hết mấy chữ có dấu tiếng Việt. Nát, nát hết rồi :v \w match một ký tự từ, nhưng mặc định thì Unicode Category Properties (UCP) không được hỗ trợ, nên chữ cái mà \w match được chỉ giới hạn trong các chữ cái tiếng Anh mà thôi. Vấn đề này giải quyết rất đơn giản, chưa bật thì giờ bật lên thôi :v Thêm (*UCP) vào đầu pattern (cũng giống như lúc chuyển sang case-insensitive mode vậy), chấm hết.
    Mã (AutoIt):
    #include <Array.au3>
    $string = "0942607853 - Bửu,Cần Thơ" & @CRLF & " 0914838746 Liên, Ninh Bình" & @CRLF & "0974668082 Lâm, Cà Mau" & @CRLF & "0926813634 Hào, Hà Nội" & @CRLF & "0965667320 - Long, Đồng Tháp" & @CRLF & " 01213566732 -Duy, Sóc Trăng"
    $result = StringRegExp($string, '(*UCP)(\d{10,11}) +-? *(\w+)', 3)
    _ArrayDisplay($result)
    Hết bị vướng chữ cái có dấu rồi, xét tiếp đoạn cuối thôi!

    Dấu Chấm, match (gần) hết mọi loại ký tự
    Vẫn như cũ, bỏ qua phần phân cách (dấu phẩy, 0 hoặc nhiều khoảng trắng), nhưng lần này sau khi bỏ qua thì sẽ lấy hết tất cả những gì còn lại trên dòng. Để match một ký tự bất kỳ ngoại trừ ký tự xuống dòng, dùng .. Vâng, là một dấu chấm các bác ạ, cái này lên mấy group trên Facebook thấy chấm mút đầy ra :)) Dấu phẩy, 0 hoặc nhiều khoảng trắng thì viết là , *, còn phần còn lại thì chỉ việc .+ là xong. Bao phần cần lấy lại trong ( ) nữa và tận hưởng thành quả.
    Mã (AutoIt):
    #include <Array.au3>
    $string = "0942607853 - Bửu,Cần Thơ" & @CRLF & " 0914838746 Liên, Ninh Bình" & @CRLF & "0974668082 Lâm, Cà Mau" & @CRLF & "0926813634 Hào, Hà Nội" & @CRLF & "0965667320 - Long, Đồng Tháp" & @CRLF & " 01213566732 -Duy, Sóc Trăng"
    $result = StringRegExp($string, '(*UCP)(\d{10,11}) +-? *(\w+), *(.+)', 3)
    _ArrayDisplay($result)

    Tóm tắt phần 2
    • \d để match 1 chữ số thập phân
    • {x,y} để thể hiện ký tự/metacharacter/group ngay trước nó xuất hiện từ x đến y lần.
    • {x,} để thế hiện ký tự/metacharacter/group ngay trước nó xuất hiện ít nhất x lần.
    • + để thể hiện ký tự/metacharacter/group ngay trước nó xuất hiện ít nhất 1 lần.
    • * để thể hiện ký tự/metacharacter/group ngay trước nó xuất hiện ít nhất 0 lần.
    • ? để thể hiện ký tự/metacharacter/group ngay trước nó có thể xuất hiện hoặc không.
    • \w để match 1 ký tự từ (chữ cái, chữ số, hoặc dấu _).
    • (*UCP) để bật Unicode Category Properties (UCP)
    • . để match ký tự bất kỳ ngoại trừ ký tự xuống dòng.
    Tổng kết phần 2
    Trên đây là những metacharacter vô cùng phổ biến, biết cách sử dụng được chúng là bạn đã có thể khai thác thêm một phần lớn sức mạnh của RegExp. Ở những bài viết sau sẽ đề cập những thứ ít xuất hiện hơn (nhưng không kém phần hữu ích), đồng thời cũng sẽ đưa ra những vấn đề phức tạp hơn có thể giải quyết bằng RegExp. Hẹn gặp lại các bạn ở bài viết kế.



    ---------------------***----------------------​
    (27/01/2017)
    Phần 3
    Character class & ungreedy quantifier


    Chào các bạn, tuần này mình lười nghĩ quá nên thôi lấy luôn đoạn HTML trên trang thông báo của trường để viết vậy :v Bài viết này sẽ từng bước giải quyết vấn đề lấy link và ngày đăng của từng mẩu thông báo.

    Bài viết này chủ yếu là nói về RegExp nên phần code không liên quan mình sẽ cung cấp sẵn luôn. À sẵn nói luôn, lấy link với ngày đăng về rồi thì có thể truy cập từng link để lấy nội dung thông báo. Việc lấy link thế này có ích lợi gì hay không là phải tùy vào việc bạn dùng nó ra sao.

    Mã (AutoIt):
    #include <Array.au3>
    Local $pageSource = BinaryToString(InetRead('http://ctu.edu.vn/notice.php'), 4)
    Local $pattern = '' Local $result = StringRegExp($pageSource, $pattern, 3)
    _ArrayDisplay($result)
    Lề: Mấy bác đừng có bật cái flag FORCERELOAD của InetRead nhé, trang này của trường em :'(

    Lấy link thông báo
    Để chắc chắn pattern không match nhầm sang phần khác của trang web, bạn cần phải xem xét cả phần cần lấy và phần xung quanh nó nữa. Riêng trường hợp này thì khá dễ do trên cả trang này chỉ có mấy cái link thông báo có thuộc tính (attribute) href là notice_det.php thôi. Mình sẽ bắt đầu từ đây. href="(notice_det\.php\?mn=\d+&id=\d+)". Pattern này chỉ sử dụng những cái đã được giới thiệu ở các bài trước. Gán đoạn này vào $pattern và chạy code để kiểm tra.

    Mã (AutoIt):
    #include <Array.au3>
    Local $pageSource = BinaryToString(InetRead('http://ctu.edu.vn/notice.php'), 4)
    Local $pattern = 'href="(notice_det\.php\?mn=\d+&id=\d+)"'
    Local $result = StringRegExp($pageSource, $pattern, 3)
    _ArrayDisplay($result)
    Chạy quá mượt luôn, không có vấn đề gì hết, qua tới phần kế thôi: lấy ngày đăng thông báo.

    Bỏ qua phần dữ liệu chen giữa
    - Ngày đăng nằm trong tag font, tag này nằm ngay sau tag a. Đoạn pattern ở trên chỉ mới lấy phần href của tag a thôi, để tới phần ngày đăng cần phải bỏ qua phần còn lại của tag a. Để bỏ qua phần này thì bạn chỉ cần viết nó vào pattern nhưng không đặt nó trong () là được. Phần cần bỏ qua là những ký tự liên tiếp nhau và khác <, biểu diễn trong RegExp là [^<]+. [^<] là một character class, dùng để biểu diễn một tập các ký tự có thể match. Character class có thể biểu diễn theo 2 cách: chỉ ra tập ký tự có thể match (biểu diễn tập ký tự này giữa [ và ], hoặc chỉ ra tập ký tự không thể match (biểu diễn tập ký tự này giữa [^ và ]. [^<] có nghĩa là một ký tự không phải < (nếu muốn loại bỏ ký tự nào khác thì bạn vẫn có thể thêm vào giữa [^ và ], chẳng hạn [^<>] sẽ match một ký tự bất kỳ ngoại trừ < và >).
    - Một số ký tự có ý nghĩa đặc biệt khi được viết trong character class sẽ mất đi ý nghĩa đặc biệt của nó. Ví dụ như [.+?] là một character class match dấu chấm, dấu cộng, hoặc dấu chấm hỏi. Cả 3 metacharacter này đều chỉ được coi là ký tự bình thường trong character class, nên không cần phải dùng \ để escape nữa.
    - Có thể dùng ký tự - để biểu diễn một phạm vi thay vì chỉ rõ từng ký tự, ví dụ [a-z] là những ký tự từ a tới z.
    - Những ký tự như ^ - [ ] cần escape bằng \ để dùng nó như một ký tự.
    • Riêng với ^ thì chỉ cần nó không đứng ngay sau [ là không cần phải escape.
    • Ngược lại, với - thì chỉ cần nó đứng ngay sau [ là không cần escape.
    - Bạn có thể chọn cách escape hoặc đặt nó vào vị trí làm mất ý nghĩa đặc biệt, cái này tùy các bạn chọn, cách nào cũng được.
    - Có một điều thú vị là các metacharacter \d, \w, ... vẫn có thể dùng trong character class (và cả hai đều là kiểu viết gọn của những character class). Ví dụ như [\da-fA-F] sẽ match 1 hexdigit (chữ số trong hệ 16).
    - Mà thôi, quay lại vấn đề, [^<] match 1 ký tự khác <, nhưng dữ liệu của chúng ta có nhiều hơn 1 ký tự như vậy, nên phải thêm + vào để nó match hết phần đó luôn. Tiếp theo là loại bỏ phần kết thúc tag a và đầu tag font, cái này chỉ cần viết y nguyên vào pattern là xong. Giữa 2 tag có thể có khoảng trắng (thường là xuống dòng kèm vài khoảng trắng), để bỏ qua luôn những khoảng trắng này chúng ta dùng \s* (\s match 1 khoảng trắng chẳng hạn như tab, xuống dòng, ký tự space có mã ASCII 32).

    Mã (AutoIt):
    #include <Array.au3>
    Local $pageSource = BinaryToString(InetRead('http://ctu.edu.vn/notice.php'), 4)
    Local $pattern = 'href="(notice_det\.php\?mn=\d+&id=\d+)">[^<]+</a>\s*<font class="date-color">'
    Local $result = StringRegExp($pageSource, $pattern, 3)
    _ArrayDisplay($result)
    Lưu ý: Ở bước này thì chạy thử chỉ giúp chúng ta xác định xem pattern có bị sai lệch hay không, chứ dữ liệu lọc ra được (nếu pattern vẫn còn đúng) vẫn chưa có thêm phần muốn lấy.

    Lấy ngày đăng
    Tới đây cái cần lấy đã ở trước mặt rồi, chắc là sẽ có bạn hấp tấp quất luôn (.+)</font> vào cuối pattern. Rất gọn gàng, rất đẹp, nhưng kết quả thì sai bét! Tất cả những quantifier mình đã giới thiệu đều sẽ match nhiều ký tự nhất có thể, nên viết như ở trên thì nó sẽ lấy từ sau <font class="date-color"> đầu tiên đến trước </font> cuối cùng! Những quantifier này được gọi là greedy quantifier. Để nó match ít nhất có thể thì cần thêm ? vào ngay sau, lúc này cái quantifier có thêm dấu ? sẽ được gọi là ungreedy quantifier và nó sẽ match ít nhất có thể. Áp dụng ngay vào pattern trên:

    Mã (AutoIt):
    #include <Array.au3>
    Local $pageSource = BinaryToString(InetRead('http://ctu.edu.vn/notice.php'), 4)
    Local $pattern = 'href="(notice_det\.php\?mn=\d+&id=\d+)">[^<]+</a>\s*<font class="date-color">(.+?)</font>'
    Local $result = StringRegExp($pageSource, $pattern, 3)
    _ArrayDisplay($result)
    - Cách làm khác: Thay vì dùng <font class="date-color">(.+?)</font> có thể dùng <font class="date-color">([^<]+)

    - Lề: Nếu cần thiết, có thể thêm \( \) vào pattern để loại bỏ cặp ngoặc xung quanh ngày đăng.

    Tóm tắt phần 3
    • Character class được viết trong cặp ngoặc vuông [ ], dùng để mô tả các ký tự có thể được match bởi chính class đó. Thêm ^ vào ngay sau [ để biểu diễn các ký tự không thể match.
    • \s để match 1 ký tự khoảng trắng (tab, newline, space).
    • Các quantifier mặc định luôn là greedy (match nhiều nhất có thể), thêm ? vào sau để chuyển nó thành ungreedy quantifier.
    Tổng kết phần 3
    Đến đây thì link và ngày đăng của các thông báo ở trang đầu đều đã được lấy về hết rồi, muốn dùng nó thế nào thì dùng thôi! Đoạn code ở bài viết này chỉ là ví dụ, nhưng theo mình thấy thì có rất nhiều trang web có thể áp dụng cách lọc dữ liệu này. Tổng hợp kiến thức từ 2 bài viết trước với bài viết này là đã đủ để bạn có thể giải quyết rất rất nhiều trường hợp cần lọc/kiểm tra dữ liệu rồi. Năm mới cũng sắp đến rồi, chúc các bạn năm mới vui vẻ. :v
     
  2. meocon

    meocon Thành viên mới
    • 3/6

    Tham gia ngày:
    5/12/16
    Bài viết:
    16
    Đã được thích:
    20
    Bài viết khá lý thú, mong bạn tiếp tục chia sẻ cho mọi người cùng học tập và tham khảo. :autoit:
     
    kamsamita1 thích bài này.
  3. essered

    essered Thành viên
    • 18/23

    Tham gia ngày:
    11/9/16
    Bài viết:
    47
    Đã được thích:
    45
    bài viết hay & dí dỏm, đợi tiếp theo
     
    kamsamita1 thích bài này.
  4. Cô Giáo Thảo

    Cô Giáo Thảo Thành viên
    • 18/23

    Tham gia ngày:
    4/1/16
    Bài viết:
    57
    Đã được thích:
    91
    Nơi ở:
    ĐH Sư Phạm Hà Nội
    Hay lắm, 2 điểm về chỗ :p
     
    kamsamita1 thích bài này.
  5. Huân Hoàng

    Huân Hoàng Administrator Thành viên BQT Administrator Super Moderator
    • 93/113

    Tham gia ngày:
    29/9/15
    Bài viết:
    642
    Đã được thích:
    1,181
    Voãi, đậu phộng Cô giáo =.= :finger:
     
    kamsamita1 thích bài này.
  6. Huân Hoàng

    Huân Hoàng Administrator Thành viên BQT Administrator Super Moderator
    • 93/113

    Tham gia ngày:
    29/9/15
    Bài viết:
    642
    Đã được thích:
    1,181
    Update phần 2 :p
     
    kamsamita1 and nguyenhanh like this.
  7. tuoitre

    tuoitre Thành viên năng động
    • 28/34

    Tham gia ngày:
    15/9/15
    Bài viết:
    135
    Đã được thích:
    89
    haizzz.. đã bắt đầu khó nhớ rùi ^.^ ... thanks các bác..:autoit:
     
    kamsamita1 thích bài này.
  8. unknown

    unknown Thành viên mới
    • 3/6

    Tham gia ngày:
    7/9/15
    Bài viết:
    9
    Đã được thích:
    18
    @tuoitre Không nhớ thì đọc phần tóm tắt thôi, chỗ nào trong phần tóm tắt không rõ nghĩa thì lội ngược lên đọc ở trên. Tính ra nội dung chính một bài cũng đâu tới 20 câu đâu.
    @Huân Hoàng Bỏ pinned post đi bác, cái box này có nhiêu bài đâu, pin chi cho mất công :v
     
    kamsamita1 and tuoitre like this.
  9. emga9xkc

    emga9xkc Thành viên năng động
    • 43/45

    Tham gia ngày:
    7/3/16
    Bài viết:
    232
    Đã được thích:
    205
    :D hay quá
     
    kamsamita1 thích bài này.
  10. tuoitre

    tuoitre Thành viên năng động
    • 28/34

    Tham gia ngày:
    15/9/15
    Bài viết:
    135
    Đã được thích:
    89
    Mã (Text):
    Để truy cập giá trị (^| ) match được chúng ta viết \1, với cặp ngoặc kế tiếp ( |$) thì viết là \2. Đến đây thì gợi ý cho bước kế tiếp để sửa chữa code đã có rồi: Thay khoảng trắng ở replacement thành \1 và \2. Sau đó nữa thì...
    Bác viết lý thuyết mà chưa biểu diễn cho em ạ .. muốn xem cái \1 thì mình xem thế nào hả bác ??
     
    kamsamita1 thích bài này.
  11. unknown

    unknown Thành viên mới
    • 3/6

    Tham gia ngày:
    7/9/15
    Bài viết:
    9
    Đã được thích:
    18
    @tuoitre Muốn xem thì replace nó rồi hiển thị ra, ví dụ cái trong ảnh bên dưới.
    Có 2 group giống nhau là (a|b), group này match 'a' hoặc 'b', kết quả nó match được sẽ được đánh số tăng dần từ 1.
    (a|b) match được 'a' trong 'abc', số thứ tự của nó là 1.
    (b|c) match được 'b' trong 'abc', số thứ tự là 2.

    Có thể hiểu đơn giản thế này, phần text mà cả cái pattern lớn match được sẽ bị xóa, xong phần replacement sẽ được điền vào.
    Chuỗi ban đầu: 'abc'
    Match xong, xóa 3 ký tự: ''
    Thêm cái replacement vào, rồi lần lượt thay mấy cái \1 \2 bằng chuỗi mà group có số thứ tự tương ứng match được:
    '\2\1c' -> 'b\1c' -> 'bac'
    [​IMG]

    Muốn biết cái group mình muốn lấy có số thứ tự là bao nhiêu cũng không khó, làm như trong ảnh dưới là nó tự hiện kết quả match được ra, nhớ là AutoIt đánh số phần tử mảng từ 0, còn backreference trong RegExp nó đếm từ 1. Theo hình dưới thì [0] là a, [1] là b, thì suy ra được \1 là a, \2 là b, chỉ vậy thôi.
    [​IMG]

    À quên nói, backreference xài ngay trong pattern cũng được, chẳng hạn như (a|b)\1 sẽ match 2 chữ a hoặc 2 chữ b, nhưng mà nếu đang xài StringRegExp flag 3 thì chỉ có 1 chữ a (hoặc chữ b) được trả về thôi, tại vì cái a|b nằm trong group, còn \1 nằm ngoài, flag 3 nó chỉ trả về kết quả của mấy cái mà capturing-group match được thôi. Mấy cái group nãy giờ đều là capturing-group, chỉ có mở ngoặc xong tới cái pattern con rồi tới đóng ngoặc, mấy group khác thì sau khi mở ngoặc còn có một số ký tự đặc biệt nữa mới tới pattern con.
     
    kamsamita1 and tuoitre like this.
  12. tuoitre

    tuoitre Thành viên năng động
    • 28/34

    Tham gia ngày:
    15/9/15
    Bài viết:
    135
    Đã được thích:
    89
    Chúc các bác năm mới vui vẻ trẻ khỏe, đẻ nhiều ra project =))... cám ơn các bác..sau tết em ngâm cứu tiếp ạ..
     
    kamsamita1 thích bài này.
  13. Huân Hoàng

    Huân Hoàng Administrator Thành viên BQT Administrator Super Moderator
    • 93/113

    Tham gia ngày:
    29/9/15
    Bài viết:
    642
    Đã được thích:
    1,181
    Nghỉ Tết hơi lâu, nay iêm hoạt động lại trên 4rum ợ, Update phần 3 :p
     
    kamsamita1 thích bài này.
  14. tuoitre

    tuoitre Thành viên năng động
    • 28/34

    Tham gia ngày:
    15/9/15
    Bài viết:
    135
    Đã được thích:
    89
    bác cho em hỏi với.file điện thoại của em có dạng thế này :
    Mã (Text):

    ,xxx xxxx xxxx, số cách giữa
    xyz,999N,......,xxx-xxx-xxxx, số có gạch giữa
    ,+x(xxx) xxxxxxx, số nước ngoài
    ,+x(xxx) xxx-xxxx, số nước ngoài
    ,00 1 (xxx) xxx-xxxx, số nước ngoài
    ,xxxxxxxxxx, 10 số
    ,xxxxxxxxxxx,  11 số
    ,+84 xx xx xxx , 10 hoặc 11 số việt nam
    , chữ không phải số ,
     
    thường thì stringregexp($array,',(\d{3,4}.*?\d{4,7}),',1), nhưng mà nó lại dính cả đoạn tên tuổi có gắn số, tớ làm mãi mà không tách được số điện thoại ra :v giúp tớ với.
     
  15. Huân Hoàng

    Huân Hoàng Administrator Thành viên BQT Administrator Super Moderator
    • 93/113

    Tham gia ngày:
    29/9/15
    Bài viết:
    642
    Đã được thích:
    1,181
    xxx xxxx xxxx, số có cách giữa
    xxx-xxxx-xxxx, số có gạch giữa
    +XX xx xx xxx, số có +mã nước (+84 xx xx xxx)
    => 3 thằng này có điểm chung là gồm 1 cụm số rồi đến dấu cách (- hoặc space) và lặp lại đến khi hoàn chỉnh số đt, thằng cuối có thêm dấu + => ta có:
    • cụm từ 2 số trở lên : \d{2,}
    • dấu cách bằng - hoặc space : [\-\s]
    • lặp lại "cụm từ 2 số trở lên và dấu cách" cho đến khi hoàn chỉnh số đt : (\d{2,}[\-\s])+
    • trường hợp thứ 3 có thêm dấu + phía trước : \+?(\d{2,}[\-\s])+
    => $sdt = StringRegExp($list, '(\+?(\d{2,}[\s\-])+)', 3)

    Vì [\s\-] ở cuối số đt không có nên thêm dấu ? đằng sau để ứng với : có hoặc không có [\s\-] đều được => $sdt = StringRegExp($list, '(\+?(\d{2,}[\s\-]?)+)', 3)

    Tuy nhiên nếu chỉ để vậy thì nó dư trường hợp (vì có 2 cặp đóng mở ngoặc đơn như in đậm ở trên), cặp đóng mở ngoặc bên trong là để nhóm từng cụm ("cụm từ 2 số trở lên và dấu cách") nhưng nó cũng hiểu là lấy giá trị của cụm đó, ví dụ nếu trong $list có số 09 12 12 4312 thì nó trả về mảng 2 phần tử là 09 12 12 4312 và 4312
    => thêm ?: vào cặp đóng mở ngoặc bên trong để không lấy giá trị cụm nó chứa:
    $sdt = StringRegExp($list, '(\+?(?:\d{2,}[\s\-]?)+)', 3)


    xxxxxxxxxx 10 số
    xxxxxxxxxxx 11 số
    Cái này thì khá đơn giản:
    $sdt = StringRegExp($list, '(\d{10,11})', 3)

    Để kết hợp nhiều trường hợp thì dùng dạng '(TH1|TH2|TH3|...)'
    => $sdt = StringRegExp($list, '(\+?(?:\d{2,}[\s\-]?)+|\d{10,11})', 3)

    Làm tương tự với mấy trường hợp còn lại: +x(xxx) xxxxxxx ... :v
     
    tuoitre thích bài này.
  16. tuoitre

    tuoitre Thành viên năng động
    • 28/34

    Tham gia ngày:
    15/9/15
    Bài viết:
    135
    Đã được thích:
    89
    phải làm nhiều mới vỡ ra được bác ạ ;) hí hí.. cơ mà của bác vẫn bị dính thằng này này bác ơi
    xyz,999,......,xxx-xxx-xxxx, số có gạch giữa <<< nó lại lấy được cả thằng 999,
    tuy trên ví dụ của em lại có chữ n :v , cơ mà em soi lại thấy vẫn có ông trong danh bạ có số 1688 hay 901,902 ở phần tên :v nó lấy luôn, làm sao để ko lấy số đó hả bác ?
     
  17. Huân Hoàng

    Huân Hoàng Administrator Thành viên BQT Administrator Super Moderator
    • 93/113

    Tham gia ngày:
    29/9/15
    Bài viết:
    642
    Đã được thích:
    1,181
    chỗ này: (\d{2,}[\-\s]?)+ => lấy từ 1 cụm trở lên => bắt luôn 999, 1688... => đổi thành chỉ lấy khi có từ 2 cụm trở lên: (\d{2,}[\-\s]?){2,}
     
  18. Huân Hoàng

    Huân Hoàng Administrator Thành viên BQT Administrator Super Moderator
    • 93/113

    Tham gia ngày:
    29/9/15
    Bài viết:
    642
    Đã được thích:
    1,181
    Nếu thým không quan trọng việc lấy số có dấu cách (- hoặc space) hay không thì sẽ đơn giản hơn nhiều, trước khi StringRegExp thì StringReplace toàn bộ danh bạ dấu - và space thành empty hết: xxx xxxx xxxx => xxxxxxxxxxx ; xyz,999N,......,xxx-xxx-xxxx => xyz,999N,......,xxxxxxxxxx ;+x(xxx) xxxxxxx => +x(xxx)xxxxxxx
    => StringRegexp với \d từ 6, 7 số trở lên (kèm thêm có hay không có dấu ngoặc và dấu +) => luôn luôn ra số đt chính xác chắc :D:D:D
     
    tuoitre thích bài này.
  19. tuoitre

    tuoitre Thành viên năng động
    • 28/34

    Tham gia ngày:
    15/9/15
    Bài viết:
    135
    Đã được thích:
    89
    hôm trước em rep mà hình như diễn đàn bị lỗi nên mất bài ;(
    em thích cách này ..đơn giản mà dễ, nhưng rồi phải replace cả file csv danh bạ lúc đầu nữa..để em so sánh số nào chưa có, số nào có rồi.
     

Chia sẻ trang này

Đang tải...