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 จบ)

No comments:

Post a Comment