10 เทคนิค Regex ที่นักพัฒนาทุกคนควรรู้
Regular expression คือเครื่องมือที่ตอนแรกดูเหมือนเข้าใจยาก แล้วจู่ๆ ก็เข้าใจ พอเข้าใจแล้วคุณจะเห็นมันได้ทุกที่: การตรวจสอบ input, การ parse log, ไปป์ไลน์ search-and-replace, การจัด routing ของ URL แต่นักพัฒนาส่วนใหญ่ใช้แค่ฟีเจอร์ไม่กี่ตัว — character class, quantifier, anchor — และไม่ได้แตะส่วนที่เหลือของสเปกเลย
คู่มือนี้ครอบคลุม 10 ฟีเจอร์ regex ที่ก้าวข้ามพื้นฐาน แต่ละข้อแก้ปัญหาจริงที่แพตเทิร์นพื้นๆ จัดการได้ไม่สวย ตัวอย่างทั้งหมดใช้ไวยากรณ์ JavaScript ซึ่งใช้ได้กับสภาพแวดล้อมใดๆ ที่เข้ากันได้กับ ECMAScript
คุณสามารถทดสอบทุกแพตเทิร์นในบทความนี้ได้ด้วย Toova Regex Tester โดยไม่ต้องเขียนโค้ดตั้งค่าใดๆ
1. Lookahead: จับคู่โดยไม่กินตัวอักษร
Lookahead ยืนยันว่าแพตเทิร์นต้อง (หรือต้องไม่) ตามหลังตำแหน่งปัจจุบัน โดยไม่ทำให้ engine ขยับผ่านตัวอักษรเหล่านั้น ข้อความที่จับคู่ไม่รวมสิ่งที่ lookahead ตรวจสอบ
ไวยากรณ์ Positive lookahead: (?=...)
// Positive lookahead: จับคู่ "foo" เมื่อมี "bar" ตามหลังเท่านั้น
const re1 = /foo(?=bar)/;
re1.test('foobar'); // true
re1.test('foobaz'); // false
ไวยากรณ์ Negative lookahead: (?!...)
// Negative lookahead: จับคู่ "foo" ที่ไม่มี "bar" ตามหลัง
const re2 = /foo(?!bar)/;
re2.test('foobaz'); // true
re2.test('foobar'); // false
การใช้งานจริง: จับคู่ตัวเลขราคาเฉพาะเมื่อมีสัญลักษณ์สกุลเงินตามหลัง โดยไม่รวมสัญลักษณ์ในค่าที่จับ หรือใช้ตรวจสอบว่ารหัสผ่านมีตัวเลขอย่างน้อยหนึ่งตัวด้วย (?=.*\d) โดยไม่ระบุว่าตัวเลขต้องอยู่ตรงไหน
Lookahead มีความกว้างเป็นศูนย์ — ไม่กินตัวอักษรใดๆ คุณสามารถวาง lookahead หลายตัวที่ตำแหน่งเดียวกันเพื่อบังคับเงื่อนไขอิสระหลายข้อพร้อมกันได้
2. Lookbehind: ตรวจสอบสิ่งที่อยู่ข้างหน้า
Lookbehind คือภาพสะท้อนของ lookahead: ตรวจสอบข้อความที่นำหน้าตำแหน่งปัจจุบันโดยไม่รวมในการจับคู่
ไวยากรณ์ Positive lookbehind: (?<=...)
// Positive lookbehind: จับคู่ "bar" เมื่อมี "foo" นำหน้าเท่านั้น
const re3 = /(?<=foo)bar/;
re3.test('foobar'); // true
re3.test('bazbar'); // false
ไวยากรณ์ Negative lookbehind: (?<!...)
// Negative lookbehind: จับคู่ "bar" ที่ไม่มี "foo" นำหน้า
const re4 = /(?<!foo)bar/;
re4.test('bazbar'); // true
re4.test('foobar'); // false
Lookbehind เข้ามาใน ECMAScript 2018 และรองรับใน browser สมัยใหม่ทุกตัวรวมถึง Node.js 10+ การใช้งานทั่วไป: ดึงค่าจากคู่ key-value เช่น name=Alice โดยจับทุกอย่างหลัง name= โดยไม่รวม key ในการจับ
หมายเหตุ: ต่างจาก lookahead, นิพจน์ lookbehind ใน JavaScript ไม่สามารถมีแพตเทิร์นที่มีความยาวแปรผันได้ — นิพจน์ lookbehind ต้องมีความยาวคงที่หรือมีขอบเขตสูงสุดที่กำหนด
3. Named Capture Group: แพตเทิร์นที่อธิบายตัวเอง
Capture group มาตรฐานอ้างอิงด้วยเลข: $1, $2 และอื่นๆ เมื่อเพิ่มหรือลบกลุ่ม การอ้างอิงทุกจุดท้ายน้ำพังหมด Named group แก้ปัญหานี้โดยให้คุณติดป้ายชื่อให้แต่ละกลุ่ม
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const m = '2026-05-10'.match(dateRe);
console.log(m.groups.year); // "2026"
console.log(m.groups.month); // "05"
console.log(m.groups.day); // "10"
Named group ใช้ใน replacement string ได้ผ่าน $<name> ด้วย:
// Back-reference โดยใช้ named group ในแพตเทิร์น
const quoteRe = /(?<q>['"]).*?\k<q>/;
quoteRe.test('"hello"'); // true
quoteRe.test('"hello''); // false
ชื่อกลุ่มต้องเป็น identifier ที่ถูกต้องของ JavaScript ใช้ชื่อที่บอกความหมายที่กลุ่มจับ — year, port, protocol — แล้วแพตเทิร์นของคุณจะเกือบอธิบายตัวเองได้ คุณยังใช้ \k<name> ภายในแพตเทิร์นเพื่อ back-reference named group ได้ดังตัวอย่างด้านบน
4. Non-Greedy Quantifier: จับคู่ให้น้อยที่สุด
โดยค่าเริ่มต้น quantifier (*, +, ?) เป็นแบบ greedy — จับคู่ตัวอักษรให้มากที่สุดเท่าที่ทำได้ การเติม ? หลัง quantifier ทำให้กลายเป็น non-greedy (เรียกอีกชื่อว่า lazy หรือ reluctant) จับคู่ตัวอักษรให้น้อยที่สุดเท่าที่ทำได้
const html = '<a>click</a>';
// Greedy (ค่าเริ่มต้น) — จับคู่ข้อความยาวที่สุดเท่าที่เป็นไปได้
/<.+>/.exec(html)?.[0]; // '<a>click</a>'
// Non-greedy — จับคู่ข้อความสั้นที่สุดเท่าที่เป็นไปได้
/<.+?>/.exec(html)?.[0]; // '<a>' เรื่องนี้สำคัญเมื่อ parse HTML, XML หรือฟอร์แมตใดที่ตัวคั่นเดียวกันปรากฏหลายครั้ง เวอร์ชัน greedy จะกลืนทุกอย่างจากแท็กเปิดแรกถึงแท็กปิดสุดท้ายทั่วทั้งสตริง เวอร์ชัน non-greedy หยุดที่การจับปิดที่ถูกต้องตัวแรก
เช่นเดียวกันใช้ได้กับ +? (หนึ่งหรือมากกว่า, lazy) และ ?? (ศูนย์หรือหนึ่ง, lazy) Non-greedy quantifier ไม่เปลี่ยน "สิ่งที่จับได้" — มันเปลี่ยน "การจับคู่ที่ถูกต้องตัวไหนถูกเลือก" เมื่อมีตัวเลือกหลายตัว
5. หลีกเลี่ยง Catastrophic Backtracking
Backtracking คือวิธีที่ regex engine กู้ตัวเมื่อจับคู่ไม่สำเร็จ — ลองเส้นทางอื่นผ่านแพตเทิร์น โดยทั่วไปเรื่องนี้มองไม่เห็นและรวดเร็ว แต่แพตเทิร์นบางตัวอาจทำให้ engine สำรวจเส้นทางจำนวนเพิ่มแบบทวีคูณ จน Node.js process ล้มแม้กับ input สตริงที่ขนาดธรรมดา
แพตเทิร์นอันตรายแบบคลาสสิกคือ quantifier ซ้อนกันเช่น (a+)+ ใช้กับสตริงอย่าง aaaaab engine จะลองทุกวิธีที่เป็นไปได้ในการแบ่งตัวอักษร a ระหว่างกลุ่มในและกลุ่มนอก ก่อนจะสรุปว่าไม่ตรง
Atomic group ((?>...)) ป้องกันเรื่องนี้โดยบอก engine ว่าห้ามย้อนกลับเข้าไปในกลุ่มเมื่อจับคู่แล้ว JavaScript ไม่รองรับ atomic group โดยตรง แต่สามารถจำลองพฤติกรรม possessive ได้ด้วย lookahead:
// ไม่มี atomic group — engine ย้อนกลับ (backtrack) เข้าไปใน (\d+)
// มี atomic group — เมื่อ (\d+) จับคู่แล้ว ห้ามย้อนกลับ
// JavaScript ไม่รองรับ atomic group โดยตรง
// แต่สามารถจำลองได้ด้วยทริก lookahead:
const re5 = /(?=(\d+))\1(?!\d)/; // จำลอง possessive \d++ หลักการที่ปลอดภัยกว่า: หลีกเลี่ยงการใส่ quantifier ซ้อนใน quantifier อื่นโดยตรง เว้นแต่มีเหตุผลเฉพาะ เขียนแพตเทิร์นใหม่ให้แม่นยำขึ้นว่าสิ่งที่จับคืออะไร คุณยังใช้ เครื่องมือ Text Diff เปรียบเทียบ output ของแพตเทิร์นที่เทียบเท่ากันสองตัวเคียงข้างกันขณะ refactor ได้
6. การดำเนินการเซ็ตของ Character Class (แฟล็ก Unicode v)
ECMAScript 2024 ได้เพิ่มแฟล็ก v ซึ่งเปิดใช้ set operations ภายใน character class เรื่องนี้ทำให้คุณแสดง "ตัวอักษรทุกตัวยกเว้นสระ" หรือ "ตัวอักษรพิมพ์ใหญ่ที่เป็น ASCII ด้วย" ได้เป็นนิยามคลาสที่สะอาด แทนการแยกตัวเลือกที่ยุ่งเหยิง
// การลบ character class แบบ POSIX ไม่มีใน JS
// แต่โหมด Unicode sets (แฟล็ก `v`) เพิ่ม set operations:
const lettersNoVowels = /[a-z--[aeiou]]/v;
lettersNoVowels.test('b'); // true
lettersNoVowels.test('e'); // false
แฟล็ก v รองรับสามการดำเนินการภายใน character class:
- Subtraction:
[A--B]— ตัวอักษรที่อยู่ใน A แต่ไม่อยู่ใน B - Intersection:
[A&&B]— ตัวอักษรที่อยู่ทั้งใน A และ B - Union:
[AB]— ตัวอักษรที่อยู่ใน A หรือ B (เหมือน character class มาตรฐาน)
Node.js 20+ และ browser แบบ evergreen ทุกตัวรองรับแฟล็ก v เป็น superset ของแฟล็ก u — อย่ารวมทั้งสองเข้าด้วยกัน ใช้ v ลำพังเมื่อต้องการคุณสมบัติของมัน
7. Word Boundary: จับคู่คำเต็ม
Anchor \b จับคู่ตำแหน่งระหว่างตัวอักษรในคำ (\w) กับตัวอักษรที่ไม่ใช่ในคำ ไม่กินตัวอักษร — เพียงยืนยันตำแหน่ง ตรงข้ามคือ \B จับคู่ตำแหน่งใดที่ ไม่ใช่ ขอบเขตคำ
const sentence = 'cat concatenate';
// ไม่มี \b — พบ "cat" ภายใน "concatenate" ด้วย
/cat/g.exec(sentence); // จับคู่ "cat" ใน "cat" และใน "concatenate"
// มี \b — จับเฉพาะคำเต็ม "cat"
/\bcat\b/g.exec(sentence); // จับเฉพาะ "cat" ที่ยืนเดี่ยว // \B เป็นตรงข้าม: จับคู่ภายในคำ ไม่ใช่ที่ขอบเขตคำ
/\Bcat\B/.test('concatenate'); // true — "cat" อยู่ภายในคำ
Word boundary จำเป็นเมื่อค้นหา identifier ในโค้ดหรือร้อยแก้ว หากไม่มี การค้นหาตัวแปรชื่อ id ก็จะโดน indexOf, invalid และ grid ด้วย ใช้ \bterm\b เพื่อจำกัดการจับเฉพาะที่ยืนเดี่ยว
ข้อควรระวังสำคัญหนึ่งข้อ: \b ใช้นิยาม "ตัวอักษรในคำ" ของ JavaScript ([a-zA-Z0-9_]) ตัวอักษรที่มีเครื่องหมายและตัวอักษรที่ไม่ใช่ละตินถูกถือว่าไม่ใช่ตัวอักษรในคำ สำหรับ word boundary ที่รู้จัก Unicode ให้รวมแฟล็ก v กับ Unicode property class
8. โหมด Multiline: Anchor ต่อบรรทัด
โดยค่าเริ่มต้น ^ จับเฉพาะจุดเริ่มต้นสุดของสตริง และ $ จับเฉพาะจุดสิ้นสุดสุดของสตริง แฟล็ก m (multiline) เปลี่ยนเรื่องนี้: ^ จับจุดเริ่มต้นของแต่ละบรรทัด และ $ จับจุดสิ้นสุดของแต่ละบรรทัด
const text = 'line one\nline two\nline three';
// ไม่มีแฟล็ก m — ^ จับคู่เฉพาะจุดเริ่มต้นของสตริงทั้งหมด
/^line/.test(text); // true (เฉพาะบรรทัดแรก)
// มีแฟล็ก m — ^ จับคู่จุดเริ่มต้นของ "แต่ละบรรทัด"
const matches = text.match(/^line/gm);
console.log(matches); // ['line', 'line', 'line'] นี่ขาดไม่ได้เมื่อประมวลผลข้อความหลายบรรทัด เช่น log file, ไฟล์ config หรือโค้ด การใช้งานทั่วไปได้แก่ การดึงบรรทัดที่ขึ้นต้นด้วยคีย์เวิร์ด การแทนที่ token ท้ายบรรทัด หรือการตรวจสอบว่าทุกบรรทัดในบล็อกตรงกับแพตเทิร์น
อย่าสับสนแฟล็ก m กับแฟล็ก s (dotAll) แฟล็ก s ทำให้ . จับตัวอักษรขึ้นบรรทัดใหม่ด้วย แฟล็ก m ไม่ส่งผลต่อ . เลย — มีผลแค่พฤติกรรมของ ^ และ $
9. Unicode Property Escape: จับคู่ตัวอักษรนานาชาติ
แฟล็ก u เปิดใช้ Unicode property escape ซึ่งให้คุณจับคู่ตัวอักษรตามหมวดหมู่, script หรือคุณสมบัติอื่นของ Unicode นี่คือวิธีที่ถูกต้องในการจับตัวอักษร, ตัวเลขหรือเครื่องหมายวรรคตอนทั่วทุกระบบการเขียนของมนุษย์ — ไม่ใช่แค่ ASCII
// แฟล็ก u เปิดใช้งาน Unicode property escapes
const letters = /\p{L}+/u;
letters.test('Héllo'); // true
letters.test('สวัสดี'); // true
letters.test('12345'); // false
// จับเฉพาะตัวอักษรพิมพ์ใหญ่ในทุกระบบการเขียน
const upper = /\p{Lu}+/u;
upper.test('ABC'); // true
upper.test('abc'); // false // จับคู่ emoji (Unicode general category: Symbol, Other)
const emoji = /\p{So}/u;
emoji.test('🚀'); // true คุณสมบัติ Unicode ที่ใช้บ่อยที่สุด ได้แก่:
\p{L}— ตัวอักษรใดๆ (ทุก script)\p{Lu}— ตัวอักษรพิมพ์ใหญ่\p{Ll}— ตัวอักษรพิมพ์เล็ก\p{N}— เลขใดๆ\p{Nd}— เลขฐานสิบ\p{P}— เครื่องหมายวรรคตอน\p{Script=Latin}— ตัวอักษร script ละติน\p{Emoji}— ตัวอักษร emoji
ใช้ \P{...} (P ตัวใหญ่) เพื่อ negate — จับทุกอย่างที่ ไม่มี คุณสมบัติที่ระบุ รายชื่อเต็มของคุณสมบัติ Unicode ที่รองรับและค่าต่างๆ อยู่ใน เอกสาร MDN
10. แพตเทิร์น Verbose ผ่านการประกอบสตริง
Regex หลายแบบ (Python, Ruby, .NET, PCRE) รองรับโหมด verbose หรือ extended (แฟล็ก x) ที่อนุญาตช่องว่างและคอมเมนต์ภายในแพตเทิร์น JavaScript ไม่มีแฟล็กนี้ — แฟล็ก x ไม่ถูกต้องใน ECMAScript
วิธีแก้มาตรฐานคือประกอบแพตเทิร์นจากสตริงค่าคงที่ที่ตั้งชื่อแล้วรวมด้วย new RegExp():
// JavaScript ไม่มีแฟล็ก x แบบเนทีฟ
// แต่สามารถสร้างแพตเทิร์นที่อ่านง่ายเป็นค่าคงที่สตริง:
const YEAR = '(?<year>\\d{4})';
const SEP = '-';
const MONTH = '(?<month>\\d{2})';
const DAY = '(?<day>\\d{2})';
const datePattern = new RegExp(YEAR + SEP + MONTH + SEP + DAY); ค่าคงที่แต่ละตัวอธิบายสิ่งที่จับ และแพตเทิร์นสุดท้ายอ่านได้เหมือนประโยค วิธีนี้ยังทำให้ประกอบ sub-pattern ที่ใช้ร่วมกันข้ามนิยาม regex หลายตัวใน codebase และ unit test แต่ละชิ้นแยกอิสระได้ง่าย
สำหรับแพตเทิร์นที่ซับซ้อนน้อยกว่า การเก็บ regex ทั้งหมดในบรรทัดเดียวพร้อมคอมเมนต์ inline ในบล็อกคอมเมนต์ใกล้ๆ มักเพียงพอ เป้าหมายคือทำให้นักพัฒนาคนต่อไป (รวมถึงคุณในอนาคต) เข้าใจแพตเทิร์นได้โดยไม่ต้องส่งผ่านตัวถอดรหัส
นำทุกอย่างมาประกอบกัน
เทคนิคทั้งสิบนี้ครอบคลุมพื้นที่ส่วนใหญ่ของคำถาม "ทำไม regex นี้ถึงพังในกรณีพิเศษ?" Lookahead และ lookbehind ให้คุณยืนยันบริบทโดยไม่กินมัน Named group ทำให้แพตเทิร์นอ่านง่ายผ่านการ refactor Non-greedy quantifier ป้องกันการจับเกินโดยไม่ตั้งใจ Unicode property escape จัดการ input ที่ก้าวข้าม ASCII
วิธีที่ดีที่สุดในการสร้างความคล่องคือทดลองแพตเทิร์นจริงกับข้อมูลจริง ใช้ Regex Tester เพื่อทำซ้ำอย่างรวดเร็ว เมื่อแพตเทิร์นของคุณสร้าง output ที่ต้อง diff หรือทำความสะอาด เครื่องมือ Text Diff แสดงสิ่งที่เปลี่ยนแปลงระหว่างการรันได้อย่างแม่นยำ และถ้า regex ของคุณ parse JSON อยู่ JSON Formatter ให้คุณตรวจสอบผลที่มีโครงสร้างได้โดยไม่ออกจาก browser
สำหรับเอกสารอ้างอิงครบถ้วนของไวยากรณ์และแฟล็ก regex ของ JavaScript, MDN Regex Cheatsheet คือหน้าเดียวที่ดีที่สุดที่ควร bookmark สำหรับการดีบักแพตเทิร์นเชิงโต้ตอบพร้อม visualization การจับคู่เต็ม, regex101.com รองรับโหมด JavaScript พร้อมคำอธิบาย built-in ของทุกองค์ประกอบในแพตเทิร์น