รูปแบบ Regex ที่นักพัฒนาทุกคนควรบุ๊กมาร์กไว้ครับ

รูปแบบ regex ทั่วไปพร้อม syntax การจับคู่รูปแบบที่ไฮไลต์บนพื้นหลังตัวแก้ไขสีเข้ม

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)
Regex เพียงอย่างเดียวไม่สามารถยืนยันได้ว่าที่อยู่อีเมลนั้นมีอยู่จริงหรือรับส่งได้ ควรส่งอีเมลยืนยันเสมอสำหรับทุกกรณีที่สำคัญครับ

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) โดยเริ่มต้นด้วย + ตามด้วยรหัสประเทศ ไม่มีช่องว่างหรือเครื่องหมายวรรคตอน

สำหรับงานที่ต้องการมากกว่าแค่ตรวจสอบรูปแบบ เช่น ยืนยันว่าเป็นเบอร์มือถือที่ใช้งานได้จริงในประเทศนั้น ๆ ควรใช้ library เฉพาะทางอย่าง libphonenumber (library โอเพนซอร์สของ Google รองรับ Java, JavaScript, Python และอื่น ๆ) ครับ

วันที่และเวลา

การ 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 หลัก)
ห้ามเก็บหมายเลขบัตรดิบเด็ดขาด pattern นี้ใช้สำหรับแสดง feedback รูปแบบฝั่ง client เท่านั้น การ validate บัตรจริงต้องผ่าน payment processor ที่เป็น PCI-compliant อย่าง Stripe หรือ Braintree เท่านั้นครับ

จัดการ 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 อื่น ๆ ได้ครับ
เครื่องมือ pattern matching และ input validation ด้วย regex

ทดสอบและ 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 จะไม่ข้ามบรรทัดได้ครับ