Regular Expression เช็ก/จัด/ตัด/แบ่งstring แค่บอกมาว่าอยากได้อะไร


ใน การเขียนโปรแกรมที่ต้องยุ่งกับ string มีหลายครั้งเลยที่เราต้องมีการตรวจเช็กว่า string ที่มีน่ะ ไม่ว่าจะมาจากการกรอกข้อมูลของผู้ใช้ อ่านจากไฟล์ หรือสร้างขึ้นมาเองก็เถอะพวกนั้นน่ะ มีรูปแบบ (pattern) ที่ถูกต้อง

เช่น

  • username ของผู้ใช้ต้องประกอบด้วยตัวอักษร a-z หรือตัวเลข ตั้งแต่ 4-20 ตัวอักษร
  • ให้ผู้ใช้กรอกอีเมล แล้วอยากเช็กว่า string ที่เขาใส่มาน่ะเป็นอีเมลจริงรึเปล่า หรือว่าใส่มั่วมากันแน่
  • อยาเช็กว่า 158.24.36.220 เป็น IP Address ที่ถูกformatรึเปล่า

เอาล่ะ สำหรับโปรแกรมเมอร์มือใหม่ก็อาจจะบอกว่าการตรวจ 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 นี้จะประกอบด้วยตัวอักษรอะไรบ้างสามารถกำหนอกได้ดังนี้

Metacharacter Description
 . ตัวอักษรอะไรก็ได้
 [  ] เรียกว่า 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 Classes Description
\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 มีตัวอักษรได้กี่ตัว มีทั้งหมดตามนี้

Metacharacter min-max Description
 ? 0 - 1 token นี้มีได้ 0 ตัว หรือ 1 ตัว แปลง่ายว่า "มี" หรือ "ไม่มี" ก็ได้
 * 0 - ∞ token นี้ "มี" หรือ "ไม่มี" ก็ได้ แล้วจะมีกี่ตัวก็ได้ด้วยนะ
 + 1 - ∞ token นี้มีกี่ตัวก็ได้ แต่อย่างมีอย่างน้อย 1 ตัว
 {min,max} min-max token นี้มีได้ตั้งแต่ 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) ให้ดูง่ายขึ้นหน่อยซิ

function check_is_username(str){
    return /[A-Za-z0-9]{4,20}/.test(str);
}

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

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

check_is_username("ta");
//ผลคือ false -> OK ถูกแล้ว

check_is_username("nartra");
//ผลคือ true -> OK ถูกแล้ว

check_is_username("nartra$123");
//ผลคือ true -> มันควรจะได้ false สิ เพราะมี $ ... ทำไมกัน?

check_is_username("123thisisthenewusernamela");
//ผลคือ true -> มันควรจะได้ false สิ เพราะยาวเกิน 20 ตัว ... ทำไมกัน?

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

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

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

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

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

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

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

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

function check_is_username(str){
    return /^[A-Za-z0-9]{4,20}$/.test(str);
}

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

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

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

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

101,Fundamental Programming: with C (3),math
102,Object Oriented Programming: with Java (3),math|advance
225,Business-English (2),

รูปแบบข้อมูลเขียนอยู่ใน 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() นั้นใช้สำหรับเช็กอย่างเดียว ตัดคำไม่ได้

var subject = "102,Object Oriented Programming: with Java (3),math|advance";
/([0-9]{3}),(.+)\(([0-9])\),([a-z]+(\|[a-z]+)*)?/.exec(subject);
/*
ผลจากการตัด
[
    "102,Object Oriented Prog...h Java (3),math|advance", 
    "102", 
    "Object Oriented Programming: with Java ", 
    "3", 
    "math|advance", 
    "|advance"
]
*/

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

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

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

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

PHP

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

<?php

$subject = "101,Fundamental Programming with C (3),math|sci";
$pattern = '/([0-9]{3}),(.+)\(([0-9])\),([a-z]+(\|[a-z]+)*)?/';
preg_match($pattern, $subject, $matches);
print_r($matches);

/*
ผลที่ได้
Array
(
    [0] => 101,Fundamental Programming with C (3),math|sci
    [1] => 101
    [2] => Fundamental Programming with C 
    [3] => 3
    [4] => math|sci
    [5] => |sci
)
*/

 Java

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

String pattern = "([0-9]{3}),(.+)\\(([0-9])\\),([a-z]+(\\|[a-z]+)*)?";
String text = "102,Object Oriented Programming: with Java (3),math|advance";

Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(text);

if(m.find()) {
	System.out.println(m.group(0));
	System.out.println(m.group(1));
	System.out.println(m.group(2));
	System.out.println(m.group(3));
	System.out.println(m.group(4));
}

/*
102,Object Oriented Programming: with Java (3),math|advance
102
Object Oriented Programming: with Java 
3
math|advance
*/

 

8676 Total Views 9 Views Today
Ta

Ta

สิ่งมีชีวิตตัวอ้วนๆ กลมๆ เคลื่อนที่ไปไหนโดยการกลิ้ง .. ถนัดการดำรงชีวิตโดยไม่โดนแสงแดด
ปัจจุบันเป็น Senior Software Engineer อยู่ที่ Centrillion Technology
งานอดิเรกคือ เขียนโปรแกรม อ่านหนังสือ เขียนบทความ วาดรูป และ เล่นแบดมินตัน

2 Responses

  1. 1 เมษายน 2020

    […] [ก-๙] สำหรับภาษาไทยซึ่งมีทั้ง พยัญชนะ สระ ตัวเลขไทยมากมาย ให้เริ่มด้วย “ก” ถึง “๙” จะได้ครบทุกตัวพอดี […]

ใส่ความเห็น

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องที่ต้องการถูกทำเครื่องหมาย *