Showing posts with label regular expression. Show all posts
Showing posts with label regular expression. Show all posts

Sunday, November 22, 2009

Regular Expression มหัศจรรย์แห่งการจัดการ Text (ตอนที่ 2 จบ)

ข้อเขียนที่แล้วได้แนะนำการใช้งานและประโยชน์ของ Regular Expression สำหรับตอนนี้เราจะมาพูดถึงไวยกรณ์ของ Regular Expression บน Oracle กันนะครับ


source_string คือ Text ที่เราต้องการจัดการ ส่วนมากจะเป็นชื่อคอลัมน์ในตาราง และเป็นคอลัมน์ที่มีประเภทข้อมูลเป็น Char, Varchar2, Nchar, Nvarchar2, Clob, NClob

pattern คือ รูปแบบทีเป็นไวยกรณ์ของ Regular Expression ที่เอาไว้จัดการ Text ใน source_string

match_parameter คือตัวที่กำหนดพฤติกรรมในการจับคู่ตาม pattern ที่กำหนด ตัวอย่างเช่น
'i' หมายถึงการจับคู่ตาม pattern ไม่สนใจเรื่องตัวอักษรใหญ่หรือเล็ก (Case Insensitive)
'c' หมายถึงการจับคู่ตาม pattern ที่จะต้องตรงกันตามอักษรตัวเล็ก-ใหญ่ (Case Sensitive)
'n' ทำให้สัญญลักษณ์จุด (Period)ใน Regular Expression ซึ่งโดยปกติหมายถึงตัวอักษรใด ๆ ก็ได้หนึ่งตัว กลายเป็นหมายถึงตัวขึ้นบรรทัดใหม่ (New Line Character)
'm' ทำให้เครื่องหมาย ^ และ $ เป็นจุดเริ่มต้นและจุดสิ้นสุดบรรทัดใน source_string ซึ่งหมายถึงทำให้มอง source_string เป็นหลาย ๆ บรรทัดได้ ถ้าไม่ได้กำหนดตัวนี้ source_string จะถูกมองเห็นเป็นบรรทัดเดียว และ ^ กับ $ จะหมายถึงเป็นจุดเริ่มต้นกับสิ้นสุดของ Text ทั้งหมดใน source_string

ถ้าเราระบุ match_parameter ที่ขัดแย้งกันเช่น 'ic' จะใช้ค่าตัวหลังสุดคือ 'c' ซึ่งหมายถึง Case Sensitive ตัวอักษรจะต้องเป็นตัวเล็ก-ใหญ่ตรงกัน หรือถ้าเราไม่ระบุ match_parameter เลย ค่าดีฟอลต์จะมีผลดังนี้
- การจับคู่จะเป็น Case Sensitive หรือไม่ขึ้นอยู่กับตัวแปร NLS_SORT
- จุด (Period) จะหมายถึงตัวใด ๆ หนึ่งตัว ไม่ได้แทนตัวขึ้นบรรทัดใหม่
- source_string จะถูกมองเป็นบรรทัดเดียว และเครื่องหมาย ^ และ $ จะหมายถึงจุดเริ่มต้นและสิ้นสุด Text ทั้งหมดใน source_string

ตัวอย่างที่ 1
คิวรีข้างล่างนี้จะแสดงชื่อและนามสกุลของพนักงาน ซึ่งเราต้องการคนที่มีชื่อประมาณว่า "สตีเฟ่น" ซึ่งภาษาอังกฤษอาจจะเขียนเป็น Steven หรือ Stephen ก็ได้(มีชื่อขึ้นต้นด้วย Ste และลงท้ายด้วย en และตรงกลางเป็น v หรือไม่ก็ ph)

SQL select first_name, last_name
from employees
where REGEXP_LIKE (first_name, '^Ste(v|ph)en$');

FIRST_NAME LAST_NAME
---------------------- ------------------------------------------
Steven King
Steven Markle
Stephen Stiles

เครื่องหมาย ^ คื่อให้เริ่มตรวจสอบ(ด้วย pattern)ตั้งแต่อักขระตัวแรกของ source_string โดยที่อักษรตัวแรกจะต้องเป็นตัว S, ตัวที่สองสามเป็น te ตัวถัดมาอาจจะเป็น v หรือ ph ก็ได้ และจบท้ายด้วย en เครื่องหมาย $ แสดงจุดสิ้นสุดของ Text ที่เราต้องการตรวจสอบ เมื่อใช้ en$ หมายถึงว่า Text ที่จะเข้าเงื่อนไขจะต้องลงท้ายด้วย en แต่ถ้าเราใส่แค่ en เฉย ๆ ก็หมาย ความว่าตัวอักษรตัวที่ห้า-หก หรือตัวที่หก-เจ็ด จะเป็น en (ขึ้นอยู่กับว่าจะเป็น Stev หรือ Steph)ส่วนหลังจากนั้นจะเป็นอะไรก็ได้

ตัวอย่างที่ 2
คิวรีข้างล่างนี้แสดงนามสกุลของพนักงาน ที่มีสระเหมือนกันติดกันสองตัว (คอลัมน์ last_name มีตัวอักษรที่เป็นสระ (a, e, i, o หรือ u) ซ้ำกันสองตัวที่อยู่ติดกันด้วย โดยไม่สนใจว่าตัวเล็ก หรือตัวใหญ่

SQL> select last_name
from employees
where REGEXP_LIKE (last_name,'([aeiou])\1','i');

LAST_NAME
-------------------------
Bloom
De Haan
Feeney
Gee
Greenberg
Greene
Khoo
Lee

ตัว Bracket "[]" ครอบตัวอักษรใด ๆ ที่เป็นไปได้ ในกรณีนี้คือตัวใด ๆ ระหว่าง a,e,i,o หรือ u ส่วนเครื่องหมาย Backslash "\" หมายถึงซ้ำตัวที่อยู่ในวงเล็บ "()" ข้างหน้า และตัว i ข้างหลังสุดคือการจับคู่ไม่สนใจว่าจะเป็นตัวใหญ่หรือเล็ก เราจะพบว่า LAST_NAME ที่ปรากฎทุก row จะมีสระซ้ำกันสองตัว

สรุปสัญญลักษณ์ต่าง ๆ ที่ใช้ใน Regular Expression (ส่วนที่เป็น Pattern)
\ (Backslash) เป็นเครื่องหมาย Backslash ตรงตัวหรือทำให้ตัวอักษรซึ่งเป็น Operator (เช่นเครื่องหมายคูณ, หาร, บวก และลบ) กลายเป็นตัวอักษรธรรมดาตัวหนึ่ง เช่น \* จะหมายถึง ตัวดอกจันหนึ่งตัว ไม่ใช่เครื่องหมายคูณเป็นต้น

* แทนการเกิดขึ้น 0 ครั้งขึ้นไป

+ แทนการเกิดขึ้น1 ครั้งเป็นต้นไป

? แทนการเกิดขึ้น 0 หรือ 1 ครั้ง

| ใช้เป็นตัวคั่นเพื่อแสดงตัวเลือก

^ แสดงว่าเป็นจุดเริ่มของข้อความ ถ้าใช้ร่วมกับ match_parameter 'm' จะใช้เป็นจุดเริ่มต้นของบรรทัดใด ๆ ใน source_string

$ แสดงว่าเป็นจุดสิ้นสุดของข้อความ ถ้าใช้ร่วมกับ match_parameter 'm' จะใช้เป็นจุดสิ้นสุดของบรรทัดใด ๆ ใน source_string

. แทนตัวอักษรใด ๆ หนึ่งตัวยกเว้น NULL และแทนตัวขึ้นบรรทัดใหม่ (New Line) ถ้า match_parameter เป็น 'n'

[ ] ใน Bracket นี้จะเป็นลิสต์ของตัวอักษร ซึ่งตัวใดตัวหนึ่งในลิสต์สามารถจับคู่ได้กับใน source_string ถ้าต้องการให้เป็นตรงกันข้ามคือต้องการ source_string ที่ไม่มีในลิสต์จะต้องใส่ ^ เข้าไปเช่นจากคิวรีข้างบนถ้าเปลี่ยน [aeiou] เป็น [^aeiou] จะได้ข้อมูล last_name ที่เป็นตรงกันข้ามคือแสดงทุกเรคคอร์ดใน employees ยกเว้น 8 เรคคอร์ดนี้

( ) เครื่องหมายกลุ่ม, สัญญลักษณ์ทั้งหลายในนี้จะถือเป็นหนึ่งตัว

{m} แทนการเกิดขึ้น m ครั้งเท่านั้น

{m,} แทนการเกิดขึ้น m ครั้งขึ้นไป

{m,n} แทนการเกิดขึ้น m ถึง n ครั้ง

\n แทนการเกิดขึ้นซ้ำครั้งที่ n ของตัว ( ) ที่อยู่ข้างหน้าตัวมัน เช่น ([aeiou])\1 หมายถึงการเกิดขึ้นซ้ำของตัว a, e,
i, o หรือ u ครั้งที่หนึ่ง เช่น aa, ee, ii, oo หรือ uu ตัว n สามารถเป็นได้จาก 1 ถึง 9

[. .] แสดงตัวอักษรหนึ่งตัว ซึ่งอาจจะประกอบด้วยอักขระมากกว่าหนึ่งตัวก็ได้ (เช่น [.ch.] ในภาษาสเปน) ใช้ในกรณีที่เราต้องการแทนตัวอักษรตัวใดๆ ในลำดับ เช่นในภาษาอังกฤษ จาก a ไปจนถึง c เราจะใช้ [a-c] แต่ในบางภาษาตัวอักษรแต่ละตัวอาจจะไม่ได้ประกอบด้วยอักขระเพียงตัวเดียวดังเช่น ch ในภาษาสเปน เราก็เลยต้องใช้ [..] ช่วยเช่นจาก a ถึง ch เราก็สามารถใช้ [a-[.ch.]] แทน

[: :] แสดงคลาสของตัวอักษร (เช่น [:alpha:]) โดยจับคู่กับตัวอักษรใด ๆ ในคลาสของตัวอักษรที่ระบุ
[:alnum:] ตัวอักษรและตัวเลขทุกตัว (alphanumeric)
[:alpha:] ตัวอักษรทุกตัว
[:digit:] ตัวเลขทุกตัว
[:blank:] ตัวช่องว่าง (Space) ทุกตัว
[:space:] ตัวช่องว่างที่ไม่สามารถพิมพ์ได้ (Nonprinting)
[:cntrl:] แสดงตัวอักษร Control ทุกตัว (ตัวอักษรที่พิมพ์ไม่ได้ - Nonprinting)
[:punct:] ตัวอักขระพิเศษทุกตัว
[:lower:] ตัวอักษรที่เป็นตัวเล็กทุกตัว
[:upper:] ตัวอักษรที่เป็นตัวใหญ่ทุกตัว
[:graph:] ตัวอักษรที่อยู่ในคลาส [:punct:], [:upper:], [:lower:] และ [:digit:] ทุกตัว
[:print:] ตัวอักษรทุกตัวที่พิมพ์ได้
[:xdigit:] ตัวอักษรที่เป็นเลขฐานสิบหกทุกตัว
คลาสเหล่านี้จะต้องใช้ภายใน Bracket เท่านั้นเช่น [[:alnum:]] หรือ [[:alpha:][:digit:]] เป็นต้น

[==] จับคู่กับตัวอักษรที่มีตัวอักษรฐาน (Base Character) ตรงกัน เช่น [=a=] จะจับคู่กับตัวอักษรที่มีตัวอักษรฐานเป็น a (เช่น Á และ Ä เป็นต้น) จะต้องใช้ภายใน Bracket เท่านั้น

ลองเล่นดูนะครับ เล่นไป ๆ จะพบว่ามันไม่ยากอย่างที่คิดเลย ขอให้สนุกกับ Regular Expression นะครับ!

บทความที่เกี่ยวเนื่องกัน
Regular Expression มหัศจรรย์แห่งการจัดการ Text (ตอนที่ 1)

Sunday, November 15, 2009

Regular Expression มหัศจรรย์แห่งการจัดการ Text (ตอนที่ 1)

สวัสดีครับ สำหรับตอนนี้ผมตั้งชื่อไว้เสียโก้หรู ไม่ใช่เพราะจะเลียนแบบโฆษณาหรอกครับ แต่ด้วยรู้สึกว่า Regular Expression เป็นเครื่องมือสำหรับนักฐานข้อมูลที่มหัศจรรย์จริง ๆ และควรจะมีมาตั้งนานแล้ว (Regular Expression เดิมมีอยู่บน Unix,Oracle เพิ่งเริ่มจะมีเมื่อ Version 10g นี้เอง) เราเริ่มกันเลยนะครับ

Regular Expression เป็นเรื่องเกี่ยวกับรูปแบบการเรียงตัวของตัวอักษร (Pattern) ซึ่ง Regular Expression ทำให้เราสามารถจับ Pattern ในข้อมูลที่เป็น Text ยาว ๆ ได้ ซึ่งการที่เราสามารถจับ Pattern ใน Text ได้นี้ทำให้เราสามารถค้นหาโดยใช้เงื่อนไข รวมทั้งสามารถจัดการกับข้อมูลได้อย่างมีประสิทธิภาพ

ตัวอย่างการหารูปแบบการเรียงตัวของตัวอักษรที่พบบ่อยคือข้อมูล "ที่อยู่" ที่เป็นข้อความติดต่อกัน เช่น
"11/999 ม.4 ถ.พุทธมณฑลสาย 3 แขวงบางไผ่ เขตบางแค กรุงเทพฯ 10160"

ถ้าข้อมูลในตารางที่อยู่มีข้อมูลที่มีลักษณะเหมือน ๆ กันกับข้อมูลข้างบนนี้ เราอาจจะบอกถึงรูปแบบคร่าว ๆ ได้ว่า
1. เลขที่บ้านจะอยู่ข้างหน้าเสมอ และจะต้องมีตัวเลขอย่างน้อยหนึ่งตัว อาจจะมีเครื่องหมาย "/" ด้วยก็ได้
2. "ม." หมายถึง หมู่ที่
3. "ถ." หมายถึง ถนน
4. "แขวง", "เขต" ความหมายตรงตัว
5. รหัสไปรษณีย์ เป็นเลขห้าหลักอยู่หลังสุด

สมมติว่าเดิมข้อมูลในตารางของเรา (cust_addr) ไม่ได้ตัดแบ่งที่อยู่ออกเป็นคอลัมน์ และเราต้องการตัดข้อมูลที่อยู่ข้างบนนี้ออกเป็นคอลัมน์ ๆ ดังข้างล่างนี้
1. เลขที่ = '11/999'
2. หมู่ = '4'
3. ถนน = 'พุทธมณฑลสาย 3'
4. แขวง/ตำบล = 'บางไผ่'
5. เขต/อำเภอ = 'บางแค'
6. จังหวัด = 'กรุงเทพฯ'
7. รหัสไปรษณีย์ = '10160'

สมมติอีกว่าเรามีข้อมูลในตาราง cust_addr ดังข้างล่างนี้

SQL> select * from cust_addr;

CUST_ID CUST_ADDR
------- ------------------------------------------------------------------
3 501/121 ถนนจันทร์ แขวงทุ่งวัดดอน อำเภอยานนาวา กรุงเทพฯ 10120 โทร (02)285-2834
2 12/45 ถนนเพชรเกษม จังหวัดนครปฐม โทร 0 24311235
1 15/120 ม.3 ถ.พุทธมณฑลสาย 3 แขวงบางไผ่ เขตบางแค กรุงเทพฯ 10160

คิวรีข้างล่างเป็นการค้นหาเรคคอร์ดที่มีหมายเลขโทรศัพท์อยู่ด้วย โดยใช้ REGEXP_LIKE ยังไม่ต้องกังวลเรื่องตัวอักษรแปลก ๆ นะครับ เราจะมาดูรายละเอียดกันทีหลัง

SQL> select cust_addr from cust_addr where regexp_like (cust_addr,'\(?[0-9]{2,3}\)?[[:digit:]]{3,4}\-?[[:digit:]]{3,4}');

CUST_ADDR
-----------------------------------------------------------------
501/121 ถนนจันทร์ แขวงทุ่งวัดดอน อำเภอยานนาวา กรุงเทพฯ 10120 โทร (02)285-2834
12/45 ถนนเพชรเกษม จังหวัดนครปฐม โทร 0 24311235

เราจะได้แค่สองเรคคอร์ดเนื่องจากเรคคอร์ดที่ cust_id = 1 ไม่มีหมายเลขโทรศัพท์

เราสามารถใช้ Regular Expression ในการตัดเอาเฉพาะส่วนที่ต้องการจากข้อความยาว ๆ มาก็ได้ โดยใช้ REGEXP_SUBSTR เช่น

SQL> select cust_addr,regexp_substr(cust_addr,'\(?[0-9]{2,3}\)?[[:digit:]]{3,4}\-?[[:digit:]]{3,4}') as tel_no from cust_addr

CUST_ADDR TEL_NO
------------------------------------------------------------------ ------------
501/121 ถนนจันทร์ แขวงทุ่งวัดดอน อำเภอยานนาวา กรุงเทพฯ 10120 โทร (02)285-2834 (02)285-2834
12/45 ถนนเพชรเกษม จังหวัดนครปฐม โทร 0 24311235 24311235
15/120 ม.3 ถ.พุทธมณฑลสาย 3 แขวงบางไผ่ เขตบางแค กรุงเทพฯ 10160

เป้าหมายของเราคือแยกข้อมูลที่อยู่ออกเป็นคอลัมน์ ๆ ในขั้นแรกเราจะสร้างตารางที่จะใช้เก็บที่อยู่ขึ้นมาอีกตาราง โดยแยกที่อยู่ออกเป็นคอลัมน์ ๆ ดังตาราง cust_addr2 ข้างล่าง

SQL> desc cust_addr2;

Name Type Nullable Default Comments
-------- ------------ -------- ------- --------
CUST_ID NUMBER Y
ADDR_NO VARCHAR2(20) Y
MOO VARCHAR2(5) Y
ROAD VARCHAR2(50) Y
TAMBON VARCHAR2(50) Y
AMPHUR VARCHAR2(50) Y
PROVINCE VARCHAR2(50) Y
POSTCODE VARCHAR2(5) Y

เราจะใช้ REGEXP_SUBSTR ในการตัดข้อความที่อยู่ออกเป็นส่วนๆ เริ่มกันที่บ้านเลขที่่ก่อนนะครับ อย่าเพิ่งกังวลกับตัวอักษรแปลก ๆ นะครับ

SQL> select regexp_substr(cust_addr,'^[[:digit:]]{1,4}/?[[:digit:]]{1,4}') as addr_no from cust_addr;

ADDR_NO
-----------------
501/121
12/45
15/120

คราวนี้เราจะตัดเอาถนน (ROAD) ออกมาอีกคอลัมน์นะครับ

SQL> select regexp_substr(cust_addr,'^[[:digit:]]{1,4}/?[[:digit:]]{1,4}') as addr_no
, regexp_substr(cust_addr,'(ถนน|ถ\.)[[:alpha:]]+[[:blank:]]?[[:digit:]]{0,3}') as road
from cust_addr;

ADDR_NO ROAD
---------- ----------------
501/121 ถนนจันทร์
12/45 ถนนเพชรเกษม
15/120 ถ.พุทธมณฑลสาย 3

ถ้าต้องการตัดทุกคอลัมน์จะได้ดังนี้ครับ

SQL> select
2 cust_id
3 , regexp_substr(cust_addr,'^[[:digit:]]{1,4}/?[[:digit:]]{1,4}') as addr_no
4 , regexp_substr(cust_addr,'(ม\.|หมู่)[0-9]{1,2}') as moo
5 , regexp_substr(cust_addr,'(ถนน|ถ\.)[[:alpha:]]+[[:blank:]]?[[:digit:]]{0,3}') as road
6 , regexp_substr(cust_addr,'(แขวง|ตำบล)[[:alpha:]]+') as tambon
7 , regexp_substr(cust_addr,'(เขต|อำเภอ)[[:alpha:]]+') as amphur
8 , regexp_substr(cust_addr,'(จ\.|จังหวัด)[[:alpha:]]+|กรุงเทพฯ') as province
9 , regexp_substr(cust_addr,'[[:digit:]]{5}$') as postcode
10 , regexp_substr(cust_addr,'\(?[0-9]{1,3}\)?[[:blank:]]?[[:digit:]]{3,4}\-?[[:digit:]]{3,4}') tel_no
11 from cust_addr;

CUST_ID ADDR_NO MOO ROAD TAMBON AMPHUR PROVINCE POSTCODE TEL_NO
------- ------- --- -------------- ---------- ---------- -------- -------- --------
3 501/121 ถนนจันทร์ แขวงทุ่งวัดดอน อำเภอยานนาวา กรุงเทพฯ (02)285-2834
2 12/45 ถนนเพชรเกษม จังหวัดนครปฐม 11235 0 24311235
1 15/120 ม.3 ถ.พุทธมณฑลสาย 3 แขวงบางไผ่ เขตบางแค กรุงเทพฯ 10160

เราใช้คำสั่ง insert into ... select เพื่อ Insert ข้อมูลจากตาราง cust_addr ที่แปลงแล้วเข้าไปในตาราง cust_addr2

SQL> insert into cust_addr2
select
cust_id
, regexp_substr(cust_addr,'^[[:digit:]]{1,4}/?[[:digit:]]{1,4}') as addr_no
, regexp_substr(cust_addr,'(ม\.|หมู่)[0-9]{1,2}') as moo
, regexp_substr(cust_addr,'(ถนน|ถ\.)[[:alpha:]]+[[:blank:]]?[[:digit:]]{0,3}') as road
, regexp_substr(cust_addr,'(แขวง|ตำบล)[[:alpha:]]+') as tambon
, regexp_substr(cust_addr,'(เขต|อำเภอ)[[:alpha:]]+') as amphur
, regexp_substr(cust_addr,'(จ\.|จังหวัด)[[:alpha:]]+|กรุงเทพฯ') as province
, regexp_substr(cust_addr,'[[:digit:]]{5}$') as postcode
, regexp_substr(cust_addr,'\(?[0-9]{1,3}\)?[[:blank:]]?[[:digit:]]{3,4}\-?[[:digit:]]{3,4}') tel_no
from cust_addr;

3 rows inserted

SQL> select * from cust_addr2;

CUST_ID ADDR_NO MOO ROAD TAMBON AMPHUR PROVINCE POSTCODE TEL_NO
------- ------- --- -------------- ---------- ---------- -------- -------- --------
3 501/121 ถนนจันทร์ แขวงทุ่งวัดดอน อำเภอยานนาวา กรุงเทพฯ (02)285-2834
2 12/45 ถนนเพชรเกษม จังหวัดนครปฐม 11235 0 24311235
1 15/120 ม.3 ถ.พุทธมณฑลสาย 3 แขวงบางไผ่ เขตบางแค กรุงเทพฯ 10160

เราจะเห็นว่ายังมีคำที่ไม่ต้องการในแต่ละคอลัมน์อีกเช่น "แขวง" หรือ "ถนน" ฯลฯ เราจะใช้ REGEXP_REPLACE ในการตัดเอาตัวอักษรที่ไม่ต้องการนี้ออก

SQL> update cust_addr2
set moo = regexp_replace(moo,'ม\.|หมู')
, road = regexp_replace(road,'ถนน|ถ\.')
, tambon = regexp_replace(tambon,'ตำบล|แขวง')
, amphur = regexp_replace(amphur,'อำเภอ|เขต')
, province = regexp_replace(province,'จังหวัด');

3 rows updated

SQL> select * from cust_addr2;

CUST_ID ADDR_NO MOO ROAD TAMBON AMPHUR PROVINCE POSTCODE TEL_NO
------- ------- --- -------------- ---------- ---------- -------- -------- --------
3 501/121 จันทร์ ทุ่งวัดดอน ยานนาวา กรุงเทพฯ (02)285-2834
2 12/45 เพชรเกษม นครปฐม 11235 0 24311235
1 15/120 3 พุทธมณฑลสาย 3 บางไผ่ บางแค กรุงเทพฯ 10160

จะเห็นว่า Regular Expression ช่วยเราได้มากในการจัดการกับข้อความที่มีรูปแบบหรือ "Pattern" บางอย่างในเนื้อข้อมูล อย่างเช่นข้อมูล "ที่อยู่"ได้ บทความตอนต่อไปเราจะนำเอาไวยกรณ์ ของ regular expression มาคุยกันต่อ ขอขอบคุณที่ติดตามแล้วพบกันครั้งหน้านะครับ

บทความที่เกี่ยวเนื่องกัน
Regular Expression มหัศจรรย์แห่งการจัดการ Text (ตอนที่ 2 จบ)