• April 27, 2018

    การตรวจ format (หรือที่เรียกว่า pattern matching) พวกนี้ก็เขียนโปรแกรมให้เช็กได้อยู่แล้วนี่นา เช่นจะเช็ก username ก็อาจจะเขียนโค้ดประมาณนี้ออกมา

    function check_is_username(str){
        if(str.length() < 4 && str.length() > 20){
            return false;
        }
        
        for(i = 0; i < str.length(); i++){
            if( ! inRange(str.charAt(i), 'a', 'z') && ! isNumber( str.charAt(i) ) ){
                return false;
            }
        }
     
        return true;
    }

    .. แต่ถ้าเงื่อนไขมันซบซ้อนขึ้นล่ะ เช่นอยากจะเช็ก pattern ของอีเมลโดยมีกฎดังนี้

    “อีเมลจะต้องขึ้นด้วยตัวอักษรภาษาอังกฤษ A-Z จะตัวเล็กหรือตัวใหญ่ก็ได้ หรือจะเป็นตัวเลข – _ . กี่ตัวก็ได้ ตามด้วย @ ต่อด้วยชื่อเว็บไซต์ที่ต้องลงท้ายด้วย .com .net หรืออาจจะเป็นโดเมนประเทศเช่น .co.th .ac.uk เป็นต้น … แล้วอยากได้ชื่ออีเมลและเมลที่ใช้เช่น gmail ด้วยนะ”

    จะเห็นว่าเงื่อนไขที่ให้มาซับซ้อนมากถ้าจะมาเขียนโปรแกรมสไตล์เดิมๆ ที่เช็กตัวอักษรไล่ไปทีละตัวก็ทำได้นะ แต่น่าจะยากและใช้เวลาเยอะแน่ๆ กว่าจะเขียนเสร็จ แถมถ้าเขียนเสร็จแล้วมีการเปลี่ยนกฎอีเมลขึ้นมาล่ะ ก็ต้องมานั่งรื้อโค้ดกันใหม่

    วันนี้เราจะมาเสนอผู้ช่วยที่ทำให้การทำ string matching ของคุณง่ายขึ้นมากๆ โดยบอกมาแค่ว่า “คุณอยากได้อะไร” ก็พอ

    RegExp

    หรือ RegEx (อ่านว่า เร็ก-เอ็กซ์ ) ที่ย่อมาจาก Regular Expression ภาษาไทยเรียกว่า นิพจน์ปรกติ (ห๊ะ!?) เป็นรูปแบบการเขียนโปรแกรมในสไตล์ Declarative (อ่านเพิ่มเติมได้ใน Programming paradigm – การเขียนโปรแกรมก็มี “กระบวนท่า (ทัศน์)” นะ) ซึ่งมีหลายภาษาที่รองรับฟีเจอร์นี้ตั้งแต่ C/C++ Java Python Ruby PHP JavaScript .NET เอาง่ายๆ ว่าภาษาโปรแกรมดังๆ ใช้ RegExp ได้ทั้งนั้นแหละ ดังนั้นจะเรียนรู้มันไว้ก็ไม่เสียหลายหรอก

    หลักการเบื้องต้น

    Token / Metacharacter

    สำหรับ RegExp จะมองส่วนต่างๆ ของ pattern เป็นส่วนเล็กๆ ที่เรียกว่า token

     thisismyemail  @   gmail .  com 

    จากตัวอย่างเรื่องอีเมล เราสามารถแบ่งส่วนต่างๆ เป็น token ได้แบบข้างบนนี่ เราจะแบ่งออกเป็น

    1. ชื่ออีเมลซึ่งเป็น ตัวอักษร A-Z, a-z หรือ 0-9
    2. @
    3. ชื่อเว็บไซต์ซึ่งก็เป็น ตัวอักษร A-Z, a-z หรือ 0-9
    4. . (dot)
    5. com หรือ net หรือพวก co.th

    การบอกว่า token นี้จะประกอบด้วยตัวอักษรอะไรบ้างสามารถกำหนอกได้ดังนี้

    MetacharacterDescription
     .ตัวอักษรอะไรก็ได้
     [  ]เรียกว่า bracket expression หมายถึง กลุ่มของตัวอักษรในนี้เท่านั้นที่ต้องการ สามารถใช้ – ช่วยในกรณีที่อักษรที่ต้องการเป็น range ได้

    • [abc] แมทช์กับ “a”, “b”, “c”
    • [a-z] แมทช์กับ “a” ถึง “z” เลย
    • [abcx-z] หรือ [a-cx-z] แมทช์กับ “a”, “b”, “c”, “x”, “y”, “z”
    • [A-Za-z] แมทช์กับ “a” ถึง “z” และแมทช์กับอักษรตัวใหญ่ด้วย “A” ถึง “Z”
    • [0-9] แมทช์กับตัวเลข 0 ถึง 9
    • [_.-] แมทช์กับ “_”, “.”, “-“
    • [ก-๙] สำหรับภาษาไทยซึ่งมีทั้ง พยัญชนะ สระ ตัวเลขไทยมากมาย ให้เริ่มด้วย “ก” ถึง “๙” จะได้ครบทุกตัวพอดี
     [^  ]เหมือนเคสที่แล้ว แค่เปลี่ยนเป็น ไม่เอาตัวอักษรในนี้แทน
     ^ต้องขึ้นประโยคด้วยคำนี้ ห้ามมีอะไรนำหน้า (อย่าสับสนกับ ^ ที่อยู่ใน [] คนละตัวกันน)
     $ต้องจบประโยคด้วยคำนี้ ห้ามมีอะไรต่อท้าย
    ( )หมายถึงการจัดกลุ่มว่า token พวกนี้เป็นกลุ่มเดียวกัน
     |OR หรือ – ใช้บอกว่าตัวนี้ หรือตัวนี้ก็ได้ เช่น a|b

     Character Classes

    ต่อจากหัวข้อที่แล้ว … มีหลายๆ เคสของ RegExp ที่มักจะมีการเขียนบ่อยๆ เช่น [A-Za-z] ซึ่งใช้บ่อยมากสุดๆ จึงมีการทำเป็น short-hand ขึ้นมาให้ใช้ง่ายขึ้น

    Character ClassesDescription
    \w[A-Za-z0-9_]
    \W[^A-Za-z0-9_]
    \a[A-Za-z]
    \s[ \t] (space กับ tab สังเกตว่ามีอักษร 2 ตัวนะ คือ ช่องว่าง กับ \t)
    \_s[ \t\r\n\v\f] (space กับ tab และ whitespace ทุกตัว)
    \S[^ \t\r\n\v\f]
    \d[0-9] (digits ตัวเลข)
    \D[^0-9]
    \l[a-z] (lowercase character)
    \u[A-Z] (uppercase character)
    \x[A-Fa-f0-9] (Hexadecimal digits)

    Quantification

    หรือตัวบอกจำนวนว่าในแต่ละ token มีตัวอักษรได้กี่ตัว มีทั้งหมดตามนี้

    Metacharactermin-maxDescription
     ?0 – 1token นี้มีได้ 0 ตัว หรือ 1 ตัว แปลง่ายว่า “มี” หรือ “ไม่มี” ก็ได้
     *0 – ∞token นี้ “มี” หรือ “ไม่มี” ก็ได้ แล้วจะมีกี่ตัวก็ได้ด้วยนะ
     +1 – ∞token นี้มีกี่ตัวก็ได้ แต่อย่างมีอย่างน้อย 1 ตัว
     {min,max}minmaxtoken นี้มีได้ตั้งแต่ min ตัวถึง max ตัว

    • {4} – บอกว่า token นี้มีได้ 4 ตัวอักษรเท่านั้น (4คือตัวเลขสมมุตินะ จะเป็นเลขอะไรก็ได้) (คือ 4 ตัวเท่านั้นนะ ห้ามมาก ห้ามน้อยกว่านี้)
    • {4,8} – บอกว่า token นี้มีได้ตั้งแต่ 4 – 8 ตัวอักษร (4 กับ 8 คือตัวเลขสมมุตินะ จะเป็นเลขอะไรก็ได้) (คือมีได้ตั้งแต่ 4, 5, 6, 7, 8 ตัว)
    • {4,} – บอกว่า token นี้มีได้ตั้งแต่ 4 – ∞ ตัวอักษร (4คือตัวเลขสมมุตินะ จะเป็นเลขอะไรก็ได้) (คล้ายๆ เคสที่แล้วแต่ไม่บอก)

    โอเค หลังจากเรารู้จักทั้ง token และ quantification แล้วลองมาดูวิธีผสมพวกมันเพื่อตั้งกฎกันดู เอาโจทย์เดิมคือ function check_is_username(str) ละกัน

    **ใน ตัวอย่างต่อไปนี้จะใช้ภาษา JavaScript เป็นหลักนะ เพราะเป็นภาษาที่เขียน RegExp ได้ยุ่งยากน้อยที่สุดล่ะ .. ส่วนตอนท้ายบทความจะแถมวิธีการเขียนในภาษา Java และ PHP ให้อีกที

    อันดับ แรกสุด เราต้องรู้กฎก่อนว่า username ของเรามีข้อจำกัดอะไรบ้าง ในที่นี้คือโจทย์กำหนดว่า “username ของผู้ใช้ต้องประกอบด้วยตัวอักษร a-z หรือตัวเลข ตั้งแต่ 4-20 ตัวอักษร”

    1. เริ่มจาก token

    [A-Za-z0-9]

    2. ตามด้วยการบอกว่า token ที่เราเพิ่งกำหนดไปน่ะ มีได้ยาวกี่ตัวกัน

    [A-Za-z0-9]{4,20}

    มาถึงตอนนี้ลองเอาไปปรับปรุงโค้ด function check_is_username(str) ให้ดูง่ายขึ้นหน่อยซิ

    อธิบายเพิ่มเติมกันหน่อย ใน JavaScript การบอกว่าโค้ดส่วนไหนเป็น RegExp เราจะใช้ / / ครอบเอาไว้ ไม่ใช่ ” ” แบบตัวที่เป็น string … ส่วนการเช็กว่า string ของเราตรงกับ pattern ของ RegExp ที่เรากำหนดรึเปล่าจะใช้คำสั่ง .test()

    จะเห็นว่า (ถ้าเขียนเป็น) ง่ายกว่าแบบแรกเยอะมาก แถมอ่านรู้เรื่อง แก้ไขง่ายกว่าด้วย .. แต่ไหนลองมาเทสดูซิว่ามันใช้ได้หรือเปล่า

    2 เคสสุดท้ายมันไม่ตรงกับเงื่อนไงที่เรากำหนดนี่ ทำไมถึงได้ true ?

    เราไม่ได้เขียนผิดหรอกนะ เพราะ RegExp นั้นมอง string ของเราเป็นแบบนี้

    • nartra$123 – ส่วนหน้าเป็นตัวอักษร 6 ตัวซึ่งตรงกับเงื่อนไข เลยให้ผ่าน
    • 123thisisthenewusernamela – มีส่วนของตัวอักษรที่ยาวไม่เกิน 20 ตัวตามเงื่อนไข เลยให้ผ่าน

    ด้วยเหตุผลนี้แหละ ที่ทำให้ผลการทดสอบไม่เป็นไปตามที่คาดไว้ เพราะการเขียน RegExp ปกติ มันจะไม่เทียบ string ทั้งตัว แต่เอาแค่ส่วนที่มันแมทช์กับเงื่อนไขได้ก็พอแล้ว จะต้นคำ กลางคำ หรือปลายคำก็ได้ทั้งนั้น

    งั้นถ้าเราต้องการเช็ก string ทั้งตัวก็ต้องเพิ่มเงื่อนไขเข้าไปอีก…

    3. ถ้าต้องการให้เช็ก string ทั้งตัว ไม่ใช่แค่ส่วนใดส่วนหนึ่ง อย่าลืมใส่ ^ กับ $ ด้วยล่ะ

    ^[A-Za-z0-9]{4,20}$

    แล้วก็แก้โค้ดเป็น

    คราวนี้ก็จะได้ตามที่ต้องการล่ะ

    ตัด/แบ่ง string ด้วย group

    จากตัวอย่างที่ผ่านมา เราต้องการเช็ก username หรือ email แค่ว่ามันตรง format มั้ย คำตอบที่ต้องการก็แค่ true/false

    เช่นมีข้อมูลวิชาเรียนอยู่ แบบนี้…

    รูปแบบข้อมูลเขียนอยู่ใน format ต่อไปนี้

    รหัสวิชา,ชื่อวิชา(หน่วยกิต),วิชาที่เกี่ยวข้อง(มีหลายตัวได้ คั่นด้วย | )

    ถ้าเจอโจทย์แบบนี้ สิ่งที่เราต้องการทำไม่ใช่แค่เช็กว่าตรง format หรือเปล่าแล้ว แค่เราต้องการตัด string ออกมาเป็นส่วนต่างๆ เช่นข้อมูลบรรทัดแรก 101,Fundamental Programming with C (3),math|sci ต้องการแบ่ง string ออกมาเป็น..

    • id = 101
    • subject = Fundamental Programming with C
    • credit = 3
    • relate = math กับ sci

    ในกรณีนี้เราสามารถใช้ RegExp ช่วยได้เช่นกัน แต่ไม่ใช่ใช้เช็กว่าตรง format มั้ย แต่ใช้ในการตัด string ด้วย group

    ก่อนอื่นมาเขียน RegExp ของ format ข้อมูลวิชาเรียนอันนี้ก่อน

    1. เริ่มด้วยรหัสวิชา ซึ่งเป็นเลข 3 ตัว ตามด้วย ,

    [0-9]{3},

    2. แล้วก็ชื่อวิชาที่เป็นตัวอักษรอะไรก็ได้ อย่างน้อย 1 ตัว

    [0-9]{3},.+

    3. จากนั้นเป็นหน่วยกิตที่เป็นเลข 1 ตัวอยู่ในวงเล็บ (วงเล็บเป็นสัญลักษณ์พิเศษใน RegExp เลยต้องเติม \ นำหน้าด้วย เช่นเดียวกับภาษาตระกูล C)

    [0-9]{3},.+\([0-9]{1}\),

    4. สุดท้ายคือวิชาที่เกี่ยวข้อง กำหนดให้เป็นตัวอักษร (อย่างน้อย 1 ตัวขึ้นไป)

    [0-9]{3},.+\([0-9]\),[a-z]+

    5. แต่มันก็อาจจะมีวิชาที่เกี่ยวข้องได้หลายตัว คั่นด้วย | ( | เป็นสัญลักษณ์พิเศษใน RegExp เลยต้องเติม \ นำหน้าด้วย เช่นเดียวกับภาษาตระกูล C)

    [0-9]{3},.+\([0-9]\),[a-z]+(\|[a-z]+)*

    6. แต่สังเกตดีๆ ว่าวิชาที่เกี่ยวข้องอาจจะไม่มีก็ได้

    [0-9]{3},.+\([0-9]\),([a-z]+(\|[a-z]+)*)?

    เอาล่ะ ได้ RegExp มาแล้ว แต่แค่นี้ก็ทำได้แค่เช็กนะ ยังตัดออกมาเป็นส่วนๆ ไม่ได้

    ใส่ group ให้ส่วนที่ต้องการตัดซะ

    ขั้นตอนต่อไปให้ใช้ ( ) ครอบส่วนที่เราต้องการตัดออกมาทั้งหมด

    ([0-9]{3}),(.+)\(([0-9])\),([a-z]+(\|[a-z]+)*)?

    ครอบเฉพาะส่วนที่ต้องการนะ ยกเว้นอันสุดท้ายเพราะว่ามันดันมี ( ) ครอบอยู่แล้ว จึงไม่ต้องทำอะไร

    ส่วนคำสั่งที่จะใช้จะเปลี่ยนนิดหน่อยเป็น .exec() แทน เพราะ .test() นั้นใช้สำหรับเช็กอย่างเดียว ตัดคำไม่ได้

    ผลที่ได้จะต่างจาก .test() คือให้ลิสต์ของสิ่งที่แมทช์ได้ออกมาตามลำดับของ ( ) ที่เราใส่ลงไป โดย คำตอบแรก (index=0) จะเป็น string เต็มๆ ทั้งตัวเสมอ

    และในเคสนี้ เราได้คำตอบสุดท้ายเกินมา “|advance” เพราะใน RegExp ของเรามีวงเล็บอยู่ตรงนั้นพอดี เป็นวงเล็บตัวที่ 5 ซึ่งเวลาจะเอาไปใช้ก็ไม่ต้องสนใจมันก็ได้

    สรุป … RegExp นั้นใช้ได้ทั้งเช็กว่า string ตรง format รึเปล่า หรือแม้แต่จะใช้ในการตัดคำก็ยังได้ด้วยการเขียนแค่บรรทัดเดียวโดยไม่ต้องเขียนโปรแกรมเลย

    ตัวอย่างการใช้งานในภาษาอื่น

    PHP

    สำหรับภาษา PHP เราจะใช้ฟังก์ชัน preg_match() ในการแมทช์ pattern ส่วน string ผลจากการตัดจะถูกเขียนคำตอบลง parameter ที่ 3

     Java

    สำหรับ Java จะยุ่งยากนิดหน่อยตามสไตล์ภาษา OOP คือต้องสร้างอ๊อปเจ็ค Pattern ขึ้นมาก่อนด้วยคำสั่ง .compile() จากนั้นเอาไปแมทช์กับ string ที่ต้องการตัด ผลการตัดจะให้ออกมาในรูปของอ๊อปเจ็ค Matcher ตามตัวอย่างข้างล่างนี่



เวอไนน์ไอคอร์ส

ประหยัดเวลากว่า 100 เท่า!






เวอไนน์เว็บไซต์⚡️
สร้างเว็บไซต์ ดูแลเว็บไซต์

Categories


Uncategorized