ใน การเขียนโปรแกรมที่ต้องยุ่งกับ 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 ได้แบบข้างบนนี่ เราจะแบ่งออกเป็น
- ชื่ออีเมลซึ่งเป็น ตัวอักษร A-Z, a-z หรือ 0-9
- @
- ชื่อเว็บไซต์ซึ่งก็เป็น ตัวอักษร A-Z, a-z หรือ 0-9
- . (dot)
- com หรือ net หรือพวก co.th
การบอกว่า token นี้จะประกอบด้วยตัวอักษรอะไรบ้างสามารถกำหนอกได้ดังนี้
Metacharacter | Description |
. | ตัวอักษรอะไรก็ได้ |
[ ] | เรียกว่า bracket expression หมายถึง กลุ่มของตัวอักษรในนี้เท่านั้นที่ต้องการ สามารถใช้ - ช่วยในกรณีที่อักษรที่ต้องการเป็น range ได้
|
[^ ] | เหมือนเคสที่แล้ว แค่เปลี่ยนเป็น ไม่เอาตัวอักษรในนี้แทน |
^ | ต้องขึ้นประโยคด้วยคำนี้ ห้ามมีอะไรนำหน้า (อย่าสับสนกับ ^ ที่อยู่ใน [] คนละตัวกันน) |
$ | ต้องจบประโยคด้วยคำนี้ ห้ามมีอะไรต่อท้าย |
( ) | หมายถึงการจัดกลุ่มว่า 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 ตัว
|
โอเค หลังจากเรารู้จักทั้ง 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 */
ขอบคุณคัรบ