Regex pattern คือเทมเพลตที่นำกลับมาใช้ซ้ำได้สำหรับจับคู่ลำดับอักขระเฉพาะในข้อความ ลองนึกภาพมันเป็นเครื่องมือ "ค้นหา" ที่ทรงพลังกว่าปกติมาก และใช้ได้กับแทบทุกภาษาโปรแกรมมิ่ง ไม่ว่าจะเป็นการ validate อีเมล ตรวจสอบเบอร์โทรศัพท์จากฟอร์ม หรือ parse log file - regular expression แค่ไม่กี่ตัวก็จัดการงานได้ถึง 90% แล้ว คู่มือนี้รวบรวม pattern ที่ใช้งานได้จริงบ่อยที่สุด พร้อมอธิบายการทำงานของแต่ละส่วนและวิธีปรับใช้ครับ
สารบัญ
ทบทวน Regex Syntax อย่างรวดเร็ว
ก่อนดู pattern จริง มาทบทวน building block พื้นฐานที่จะเห็นซ้ำ ๆ กันก่อนครับ แม้จะเคยใช้ regex มาแล้ว การมีตารางอ้างอิงไว้ในที่เดียวก็ช่วยได้มากทีเดียว
| Token | ความหมาย | ตัวอย่างที่ match |
|---|---|---|
.
|
อักขระใด ๆ ยกเว้น newline |
a.c
match กับ
abc
,
a1c
|
\d
|
ตัวเลขใด ๆ (0-9) |
\d\d
match กับ
42
|
\w
|
อักขระคำ (ตัวอักษร, ตัวเลข, underscore) |
\w+
match กับ
hello_world
|
\s
|
whitespace (เว้นวรรค, tab, newline) |
\s+
match กับช่องว่างหลายช่อง
|
^
/
$
|
จุดเริ่มต้น / จุดสิ้นสุดของ string |
^\d+$
match กับ
123
เท่านั้น
|
{n,m}
|
ซ้ำระหว่าง n ถึง m ครั้ง |
\d{2,4}
match กับ
12
ถึง
1234
|
[abc]
|
character class - อักขระใดก็ได้ใน a, b, c |
[aeiou]
match กับสระทุกตัว
|
(?:...)
|
non-capturing group | จัดกลุ่มโดยไม่เก็บ backreference |
(?=...)
|
positive lookahead | ตรวจสอบสิ่งที่ตามมาโดยไม่ consume อักขระนั้น |
คู่มือ regular expressions ของ MDN Web Docs เป็นแหล่งอ้างอิงที่ดีที่สุดสำหรับ regex syntax ใน JavaScript และ pattern ส่วนใหญ่ด้านล่างสามารถนำไปใช้กับ Python, PHP, Java และ Ruby ได้เลย โดยมีความแตกต่างเรื่อง flag เพียงเล็กน้อยครับ
การ Validate อีเมล
อีเมลเป็น use case คลาสสิกของ regex - และยังเป็นส่วนที่นักพัฒนาส่วนใหญ่ทำผิดพลาดบ่อยที่สุดด้วย เพราะพยายาม validate ให้เข้มงวดเกินไป
สเปค RFC 5322
อนุญาตให้มีที่อยู่อีเมลแบบ
"very unusual"@example.com
ซึ่งแทบไม่มี regex ตัวไหนรองรับได้ครบ สำหรับงาน input validation จริง ๆ ใช้ pattern แบบ pragmatic นี้ได้เลยครับ:
^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$
แต่ละส่วนทำงานอย่างไร:
-
[a-zA-Z0-9._%+\-]+- ส่วน local part (ก่อน @); รองรับจุด, เครื่องหมายบวก, ยัติภังค์, underscore -
@- เครื่องหมาย @ ตามตัวอักษร -
[a-zA-Z0-9.\-]+- ชื่อโดเมน รวมถึง subdomain -
\.[a-zA-Z]{2,}- TLD ที่มีความยาวอย่างน้อย 2 ตัวอักษร (.io, .com, .museum)
URL และที่อยู่เว็บ
การ match URL ครอบคลุมตั้งแต่การดึงลิงก์จากข้อความธรรมดา ไปจนถึงการ validate ช่องกรอก URL ที่ผู้ใช้ป้อนเข้ามา
https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&\/=]*)
-
https?- match ทั้งhttpและhttps -
(?:www\.)?- www prefix แบบ optional -
[-a-zA-Z0-9@:%._+~#=]{1,256}- อักขระของ hostname ได้สูงสุด 256 ตัว -
\.[a-zA-Z0-9()]{1,6}- TLD -
\b(?:[-a-zA-Z0-9()@:%_+.~#?&\/=]*)- path, query string และ fragment แบบ optional
ถ้าต้องการแค่ validate (ไม่ต้องดึงข้อมูล) ให้ครอบด้วย anchor
^
และ
$
ครับ
เบอร์โทรศัพท์
เบอร์โทรศัพท์เป็นเรื่องยุ่งยากเป็นพิเศษ เพราะรูปแบบการเขียนแตกต่างกันมากตามแต่ละประเทศและนิสัยผู้ใช้ สอง pattern นี้ครอบคลุมสถานการณ์ส่วนใหญ่ได้ครับ:
รูปแบบ US/Canada (NANP)
^(\+1[-.\s]?)?(\(?\d{3}\)?[-.\s]?)?\d{3}[-.\s]?\d{4}$
Match กับ:
555-867-5309
,
(555) 867 5309
,
+1.555.867.5309
,
5558675309
รูปแบบสากล (E.164)
^\+[1-9]\d{6,14}$
E.164 คือรูปแบบที่ telephony API ส่วนใหญ่ใช้ (เช่น Twilio, AWS SNS) โดยเริ่มต้นด้วย
+
ตามด้วยรหัสประเทศ ไม่มีช่องว่างหรือเครื่องหมายวรรคตอน
วันที่และเวลา
การ match รูปแบบวันที่พบบ่อยใน log parser, form validator และ data pipeline รูปแบบที่ใช้ขึ้นอยู่กับแหล่งที่มาของข้อมูลครับ
ISO 8601 (YYYY-MM-DD)
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
รูปแบบ US (MM/DD/YYYY)
^(0[1-9]|1[0-2])\/(0[1-9]|[12]\d|3[01])\/\d{4}$
เวลาแบบ 24 ชั่วโมง (HH:MM หรือ HH:MM:SS)
^([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?$
โปรดทราบว่า pattern เหล่านี้ validate เฉพาะรูปแบบ ไม่ใช่ตรรกะปฏิทิน ตัวอย่างเช่น
2024-02-31
จะผ่านการตรวจสอบทั้งที่วันที่ 31 กุมภาพันธ์ไม่มีอยู่จริง ถ้าต้องการ validate วันที่อย่างเข้มงวด ให้ parse ด้วย date library ของภาษาที่ใช้หลังจาก regex check ครับ
การ Validate ความแข็งแกร่งของรหัสผ่าน
กฎรหัสผ่านมักกำหนดให้มีอักขระหลายประเภทและความยาวขั้นต่ำ การใช้ lookahead ช่วยให้เขียน pattern ได้กระชับโดยไม่ต้องแยกตรวจสอบหลายรอบครับ
ความยาวขั้นต่ำ 8 ตัวอักษร, มีตัวพิมพ์ใหญ่, ตัวพิมพ์เล็ก และตัวเลขอย่างละอย่างน้อย 1 ตัว
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$
แข็งแกร่ง: 8 ตัวขึ้นไป, มีตัวพิมพ์ใหญ่, ตัวพิมพ์เล็ก, ตัวเลข และอักขระพิเศษ
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]).{8,}$
แต่ละ
(?=.*[...])
คือ lookahead ที่สแกนทั้ง string เพื่อหาอักขระที่ตรงเงื่อนไขอย่างน้อย 1 ตัว ส่วน
.{8,}
ตอนท้ายบังคับความยาวขั้นต่ำ สามารถเปลี่ยน
{8,}
เป็น
{12,}
เพื่อกำหนดความยาวขั้นต่ำ 12 ตัวอักษร ซึ่งสอดคล้องกับ
แนวทาง NIST SP 800-63B
ครับ
IP Address
IPv4
^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$
Pattern นี้ปฏิเสธค่าอย่าง
999.0.0.1
ได้อย่างถูกต้อง โดย match แต่ละ octet เป็น 0-255 อย่างชัดเจนครับ
IPv6 (แบบย่อ)
^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$
รองรับรูปแบบ 8 กลุ่มแบบเต็ม สำหรับ compressed notation เช่น
::1
สำหรับ loopback นั้น pattern จะซับซ้อนขึ้นมาก - ในกรณีนั้นการ parse ด้วย network library น่าเชื่อถือกว่าใช้ regex ครับ
HTML และ Markup
มี pattern เฉพาะทางบางตัวที่มีประโยชน์จริง ๆ ในส่วนนี้ คำแนะนำทั่วไปที่ว่า "อย่า parse HTML ด้วย regex" ยังคงใช้ได้สำหรับเอกสารเต็ม - ใช้ DOM parser ที่เหมาะสมอย่าง BeautifulSoup หรือ DOMParser แทน แต่สำหรับงานเฉพาะเจาะจงที่มีขอบเขตชัดเจน regex ใช้งานได้ดีครับ
ลบ HTML tag ทั้งหมด
<[^>]*>
ดึงเนื้อหาจาก tag เฉพาะ (เช่น <title>)
([^<]*)<\/title>
capture group ที่ 1 จะเก็บข้อความ title ไว้ครับ
Match รหัสสี hex ของ HTML
#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})\b
Match ทั้งรูปแบบย่อ 3 หลัก (
#fff
) และรูปแบบเต็ม 6 หลัก (
#ffffff
) ครับ
Pattern อรรถประโยชน์ทั่วไป
pattern เหล่านี้ผุดขึ้นมาบ่อยในโปรเจกต์หลากหลายประเภทครับ
Slug (string ที่เหมาะกับ URL)
^[a-z0-9]+(?:-[a-z0-9]+)*$
Match กับ string อย่าง
my-blog-post-2024
ไม่มีตัวพิมพ์ใหญ่, ไม่มียัติภังค์นำหน้าหรือท้าย, ไม่มียัติภังค์ซ้อนกัน
หมายเลขบัตรเครดิต (รูปแบบพื้นฐาน ไม่มีช่องว่าง)
^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})$
-
ขึ้นต้นด้วย
4- Visa (13 หรือ 16 หลัก) -
ขึ้นต้นด้วย
51-55- Mastercard (16 หลัก) -
ขึ้นต้นด้วย
34หรือ37- Amex (15 หลัก) -
ขึ้นต้นด้วย
6011หรือ65- Discover (16 หลัก)
จัดการ whitespace (ยุบช่องว่างหลายช่องเป็นช่องเดียว)
\s{2,}
แทนที่ผลลัพธ์ที่ match ด้วยช่องว่างเดี่ยว เพื่อทำความสะอาด input ของผู้ใช้หรือข้อความที่ scrape มาครับ
เฉพาะตัวเลข
^\d+$
เฉพาะตัวอักษรและตัวเลข (alphanumeric)
^[a-zA-Z0-9]+$
Match บรรทัดที่มีคำเฉพาะ (ไม่สนใจตัวพิมพ์เล็ก/ใหญ่ด้วย flag)
^.*\bword\b.*$
word boundary
\b
ป้องกันไม่ให้ match กับ
word
ที่อยู่ภายใน
password
ครับ
ดึงหมายเลขเวอร์ชัน (semver)
\bv?(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.]+))?(?:\+([a-zA-Z0-9.]+))?\b
ดึง major, minor, patch, pre-release label และ build metadata จาก string อย่าง
v2.14.0-beta.1+build.42
ครับ
Flag และเทคนิคการใช้งาน
Regex pattern ทำงานต่างกันขึ้นอยู่กับ flag ที่ใช้ นี่คือ flag ที่จำเป็นบ่อยที่สุดครับ:
| Flag | JS | Python | ผลลัพธ์ |
|---|---|---|---|
| ไม่สนใจตัวพิมพ์เล็ก/ใหญ่ |
i
|
re.IGNORECASE
|
ถือว่าตัวพิมพ์ใหญ่และเล็กเหมือนกัน |
| Global (หาทุกตัว) |
g
|
re.findall()
|
คืนค่าทุก match ไม่ใช่แค่ตัวแรก |
| Multiline |
m
|
re.MULTILINE
|
^
และ
$
match ขอบเขตบรรทัด ไม่ใช่ขอบเขต string
|
| Dotall |
s
|
re.DOTALL
|
.
match กับ newline ด้วย
|
นิสัยเหล่านี้จะช่วยประหยัดเวลา debug ได้มากครับ:
- ทดสอบกับ edge case เสมอ - string ว่าง, ความยาวสูงสุด, อักขระ Unicode และ string ที่เกือบจะถูกต้องแต่ไม่ถูก
-
ใช้ non-capturing group
(?:...)เมื่อไม่จำเป็นต้องใช้เนื้อหาที่ match - เร็วกว่าและกระชับกว่า capturing group -
ใส่ anchor ใน validation pattern เสมอ
ด้วย
^และ$เพื่อป้องกัน substring ที่ดูถูกต้องภายใน string ที่ไม่ถูกต้องหลุดผ่านไปได้ -
ระวัง catastrophic backtracking
- quantifier ซ้อนกันอย่าง
(a+)+อาจทำให้ regex engine ค้างได้เมื่อรับ input ที่ถูกสร้างมาเพื่อโจมตี ควรใช้ quantifier ที่เรียบง่ายและเฉพาะเจาะจงครับ - ใช้เครื่องมือทดสอบ regex ขณะสร้าง pattern regex101.com แสดงผลการ match แบบ real-time, อธิบาย token แต่ละตัว และให้สลับระหว่าง PCRE, JavaScript, Python และ flavor อื่น ๆ ได้ครับ
ทดสอบและ validate regex pattern โดยไม่ต้องเดาสุ่ม
การสร้าง regex pattern ที่เชื่อถือได้สำหรับ input validation ทำได้เร็วขึ้นมากเมื่อมีเครื่องมือที่ใช่อยู่ในมือ ลองใช้เครื่องมือสำหรับนักพัฒนาฟรีของเราเพื่อทำความสะอาด ตรวจสอบ และแปลงข้อความด้วย regex pattern และอื่น ๆ อีกมากมายครับ
ลองใช้เครื่องมือฟรีของเรา →
greedy quantifier (เช่น
.*
) จะ match มากที่สุดเท่าที่ทำได้แล้วค่อย backtrack ส่วน lazy quantifier (เช่น
.*?
) จะ match น้อยที่สุดเท่าที่เป็นไปได้ ตัวอย่างเช่น กับ string
bold
นั้น pattern
<.*>
จะ match ทั้ง string ในขณะที่
<.*?>
จะ match แค่
เท่านั้น ใช้ lazy quantifier เมื่อต้องการดึงเนื้อหาระหว่าง delimiter ครับ
ส่วนใหญ่ใช่ แต่มีความแตกต่างอยู่บ้าง module
re
ของ Python ใช้ syntax แบบ PCRE และรองรับ named group ด้วย
(?P
ส่วน JavaScript ใช้ syntax ของ flag ที่ต่างออกไปเล็กน้อย และไม่รองรับ lookbehind ใน engine รุ่นเก่า (ก่อน ES2018) สำหรับงานข้ามภาษา ควรยึดกับ subset ร่วมกัน ได้แก่ character class, quantifier, anchor และ group พื้นฐานครับ
Regex ใช้สำหรับ format validation ใน production ได้ดีครับ - web framework แทบทุกตัวก็ใช้มันอยู่แล้ว ความเสี่ยงที่แท้จริงมาจาก pattern ที่เขียนไม่ดีซึ่งเปิดช่องให้เกิดการโจมตีแบบ ReDoS (regex denial-of-service) ผ่าน catastrophic backtracking ควรหลีกเลี่ยง quantifier ซ้อนกัน ทำให้ pattern เฉพาะเจาะจง และกำหนดขีดจำกัดความยาว input ที่สมเหตุสมผลก่อนที่ regex จะทำงานด้วยครับ
ใน engine ทั่วไปส่วนใหญ่ทั้งสองให้ผลเหมือนกันสำหรับ input แบบ ASCII ความแตกต่างจะปรากฏเมื่อใช้กับ Unicode:
\d
ใน engine บางตัว (เช่น Python 3 ที่เปิด Unicode mode) จะ match ตัวเลขจาก script อื่นด้วย เช่น ตัวเลขอารบิก-อินดิก ถ้าต้องการเฉพาะตัวเลข ASCII 0-9 อย่างชัดเจน ให้ใช้
[0-9]
แทน สำหรับ web form validation ทั่วไป ความแตกต่างนี้มักไม่มีผลครับ
ต้องการสองอย่างครับ: dotall flag (เพื่อให้
.
match กับ newline ด้วย) และอาจต้องใช้ multiline flag ด้วย (เพื่อให้
^
และ
$
ยึดกับแต่ละบรรทัดแทนที่จะเป็นทั้ง string) ใน JavaScript ใช้
/pattern/ms
ส่วนใน Python ใช้ร่วมกันด้วย
re.DOTALL | re.MULTILINE
ถ้าไม่มี dotall นั้น
.
จะหยุดที่ line break และ pattern จะไม่ข้ามบรรทัดได้ครับ