การดู : 117

04/11/2026 10:00น.

ภาพถ่ายมุมบนของตะแกรงร่อนวางเหนือชามไม้บนเคาน์เตอร์ครัว มีก้อนกรวดติดอยู่บนตาข่ายและทรายละเอียดอยู่ด้านล่าง พร้อมการ์ดสรุปชิ้นส่วน Regex: \d ตัวเลข 0-9, {n} ครบ n ตัวพอดี, ^ ต้นข้อความ

Regex คืออะไร? เขียน Regular Expression ทีละชิ้นจากงานจริง

#regex คืออะไร

#regular expression

#regex pattern

#regex python

#regex javascript

#regex เบอร์โทร

#regex อีเมล

ลองนึกภาพว่าหัวหน้าส่งข้อความแชตลูกค้ามาก้อนหนึ่ง แล้วบอกว่า "ช่วยดึงเบอร์มือถือกับอีเมลออกมาทำเป็นรายชื่อหน่อย" ข้อความข้างในรกมาก บางเบอร์มีขีด บางเบอร์ไม่มี มีเบอร์ออฟฟิศปนมาด้วย ถ้านั่งไล่อ่านทีละบรรทัดก็ทำได้ แต่พอข้อความมีเป็นพันบรรทัด งานนี้จะกินเวลาทั้งวัน และพลาดง่ายมาก

งานแบบนี้คือหน้าที่ของ Regex หรือ Regular Expression พอดี วันนี้ Superdev Academy จะพาไปเขียน Regex จากโจทย์นี้โจทย์เดียว ประกอบ Pattern ขึ้นมาทีละชิ้น แล้วรันจริงทั้งใน JavaScript และ Python ให้เห็นผลลัพธ์ทุกบรรทัด ไม่ต้องจำสัญลักษณ์ทั้งหมดก่อน ค่อยๆ เติมไปตามที่งานต้องใช้

Regex คืออะไร ถ้าพูดง่ายๆ

คู่มือ Regular expressions ของ MDN อธิบายว่า Regex คือรูปแบบ (Pattern) ที่ใช้จับคู่ลำดับตัวอักษรในข้อความ พูดง่ายๆ คือเราเขียน "หน้าตา" ของสิ่งที่อยากหา แทนที่จะเขียนคำที่อยากหาตรงๆ เช่น แทนที่จะค้นคำว่า 0812345678 เราบอกว่า "หาเลข 0 ตามด้วยตัวเลขอีก 9 ตัว" แล้วปล่อยให้โปรแกรมไล่หาทุกเบอร์ที่หน้าตาแบบนี้ให้

ข้อความตัวอย่างที่เราจะใช้ตลอดบทความคือบรรทัดนี้

const text = "ติดต่อ: 081-234-5678, 0898765432 อีเมล sales@example.com / a.team@shop.co.th โทร 02-123-4567";

ในนั้นมีเบอร์มือถือ 2 เบอร์ (แบบมีขีดและไม่มีขีด) อีเมล 2 อัน และเบอร์ออฟฟิศที่เราไม่ต้องการอีก 1 เบอร์ เป้าหมายคือหยิบเฉพาะของที่ต้องการออกมาให้ครบ

Regex ใช้ยังไง ชิ้นที่ 1: ตัวอักษรธรรมดา กับ \d

ตัวอักษรธรรมดาใน Pattern หมายถึงตัวมันเอง Pattern 0 ก็คือหาเลขศูนย์ ส่วนสัญลักษณ์ที่มีแบ็กสแลชนำหน้าคือ "ตัวแทนกลุ่ม" ที่ใช้บ่อยที่สุดมีสามตัว

  • \d: ตัวเลข 0-9

  • \w: ตัวอักษรอังกฤษ ตัวเลข และขีดล่าง (_)

  • \s: ช่องว่าง เช่น เว้นวรรค แท็บ ขึ้นบรรทัดใหม่

MDN ระบุชัดว่าใน JavaScript \d เท่ากับ [0-9] และ \w เท่ากับ [A-Za-z0-9_] ลองใช้ \d กับข้อความตัวอย่างดู (ตัว g ท้าย Pattern คือ Flag ที่บอกว่าให้หาทุกตัว ไม่ใช่หยุดที่ตัวแรก)

console.log(text.match(/\d/g).length);
// 29

ได้ตัวเลขมา 29 ตัว แต่แยกเป็นตัวๆ ยังใช้งานไม่ได้ เราต้องบอก Regex ว่าตัวเลขต้องมา "กี่ตัวติดกัน"

ชิ้นที่ 2: บอกจำนวนด้วย Quantifier

Quantifier (ตัวบอกจำนวน) คือสัญลักษณ์ที่วางต่อท้ายชิ้นก่อนหน้า เพื่อบอกว่าชิ้นนั้นซ้ำได้กี่ครั้ง

  • +: หนึ่งตัวขึ้นไป

  • *: ศูนย์ตัวขึ้นไป (ไม่มีเลยก็ได้)

  • ?: มีหรือไม่มีก็ได้ (0 หรือ 1 ครั้ง)

  • {n}: ครบ n ตัวพอดี และ {n,m}: ตั้งแต่ n ถึง m ตัว

console.log(text.match(/\d+/g));
// [ '081', '234', '5678', '0898765432', '02', '123', '4567' ]

console.log(text.match(/0\d{9}/g));
// [ '0898765432' ]

\d+ ได้ตัวเลขเป็นก้อน แต่เบอร์ที่มีขีดถูกหั่นเป็นสามท่อน ส่วน 0\d{9} (เลข 0 ตามด้วยตัวเลขอีก 9 ตัว) จับเบอร์ไม่มีขีดได้ถูกต้อง แต่เบอร์ 081-234-5678 หลุดไป เพราะมีขีดคั่นอยู่

ตรงนี้ ? เข้ามาช่วย เราบอกว่า "ขีดจะมีหรือไม่มีก็ได้" ด้วย -? แล้วแบ่งเบอร์เป็นสามช่วงตามที่คนไทยเขียนกัน

console.log(text.match(/0\d{2}-?\d{3}-?\d{4}/g));
// [ '081-234-5678', '0898765432' ]

ได้เบอร์มือถือครบทั้งสองแบบ และเบอร์ออฟฟิศ 02-123-4567 ไม่ติดมาด้วย เพราะมันมีตัวเลขแค่ 9 ตัว ไม่เข้ารูป 3-3-4 ที่เราเขียนไว้

ชิ้นที่ 3: [] เลือกได้หลายตัว และจุดที่ไม่ใช่จุด

วงเล็บเหลี่ยม [] เรียกว่า Character Class แปลว่า "ตัวใดตัวหนึ่งในนี้" เช่น [689] คือเลข 6 หรือ 8 หรือ 9 ตัวเดียว สมมติว่าระบบของทีมคุณมีกติกาว่ารับเฉพาะเบอร์ที่หลักที่สองเป็น 6, 8 หรือ 9 ก็เขียนได้แบบนี้

console.log(/^0[689]\d{8}$/.test("0912345678")); // true
console.log(/^0[689]\d{8}$/.test("0212345678")); // false

กติกาชุดเลขขึ้นต้นเป็นเรื่องของผู้ให้บริการและหน่วยงานที่ดูแลเลขหมาย ซึ่งเปลี่ยนได้ ถ้าจะใช้กับระบบจริง ให้ยึดกติกาที่ทีมตกลงกันไว้ ไม่ใช่ตัวอย่างในบทความ

อีกตัวที่มือใหม่สะดุดบ่อยคือจุด . ใน Regex จุดไม่ได้แปลว่าจุด แต่แปลว่า "ตัวอักษรอะไรก็ได้หนึ่งตัว" (ยกเว้นขึ้นบรรทัดใหม่) ถ้าอยากได้จุดจริงๆ ต้องใส่แบ็กสแลชเป็น \.

console.log("a.b".match(/./g));  // [ 'a', '.', 'b' ]
console.log("a.b".match(/\./g)); // [ '.' ]

ชิ้นที่ 4: ^ กับ $ ตอนตรวจ Input

งานแรกคือ "ค้นหา" ในข้อความยาว แต่อีกงานที่เจอบ่อยพอกันคือ "ตรวจ" ว่าสิ่งที่ผู้ใช้กรอกในฟอร์มถูกรูปแบบไหม เช่น ช่องเบอร์โทรต้องเป็น 10 หลักพอดี ถ้าใช้ Pattern เดิมเฉยๆ จะมีปัญหาแบบนี้

console.log(/0\d{9}/.test("08123456789"));
// true  (ทั้งที่กรอกมา 11 หลัก)

เพราะ Regex แค่เจอ 10 ตัวที่เข้ารูปอยู่ข้างใน ก็ถือว่าผ่านแล้ว ทางแก้คือใส่ Anchor (ตัวยึดตำแหน่ง) ^ แปลว่าต้นข้อความ และ $ แปลว่าท้ายข้อความ

console.log(/^0\d{9}$/.test("0812345678"));  // true
console.log(/^0\d{9}$/.test("081234567"));   // false (9 หลัก)
console.log(/^0\d{9}$/.test("08123456789")); // false (11 หลัก)

ฝั่ง Python ใช้ re.fullmatch ซึ่งเอกสารโมดูล re ของ Python อธิบายว่าจะคืนผลเมื่อ "ข้อความทั้งก้อน" ตรงกับ Pattern เท่านั้น จึงได้ผลเหมือนใส่ ^ กับ $

import re

print(re.fullmatch(r"0\d{9}", "0812345678"))
# <re.Match object; span=(0, 10), match='0812345678'>
print(re.fullmatch(r"0\d{9}", "08123456789"))
# None

สังเกตตัว r หน้าเครื่องหมายคำพูด นี่คือ Raw String ของ Python ที่ทำให้แบ็กสแลชไม่ถูกแปลความหมายก่อนถึง Regex เอกสาร Python เองก็แนะนำให้เขียน Pattern แบบนี้เป็นปกติ ลืมใส่เมื่อไหร่ Pattern ที่มีแบ็กสแลชจะเริ่มทำตัวแปลกๆ

อย่าลืมว่าการตรวจใน JavaScript ฝั่งหน้าเว็บเป็นแค่ด่านแรกเพื่อช่วยผู้ใช้ ข้อมูลที่ส่งเข้าระบบต้องตรวจซ้ำฝั่ง Server เสมอ เรื่องนี้เราเล่าไว้ละเอียดในบทความ เทคนิค Secure Coding ที่มือใหม่ต้องรู้

ชิ้นที่ 5: () แยกเบอร์ออกเป็นท่อน

บางครั้งเราอยากได้มากกว่า "เจอหรือไม่เจอ" เช่น อยากแยกรหัสนำหน้ากับเลขที่เหลือเพื่อจัดรูปแบบใหม่ วงเล็บ () เรียกว่า Group ใช้ครอบส่วนที่อยากหยิบออกมาแยก

const m = "081-234-5678".match(/^(0\d{2})-?(\d{3})-?(\d{4})$/);
console.log(m[1], m[2], m[3]);
// 081 234 5678

ใน Python ใช้ re.findall กับข้อความทั้งก้อนได้เลย เอกสาร Python ระบุว่าถ้า Pattern มีหลาย Group ผลลัพธ์จะเป็นรายการของ Tuple แต่ละ Tuple คือหนึ่งเบอร์ที่แยกท่อนไว้แล้ว

text = "ติดต่อ: 081-234-5678, 0898765432 อีเมล sales@example.com / a.team@shop.co.th โทร 02-123-4567"

print(re.findall(r"(0\d{2})-?(\d{3})-?(\d{4})", text))
# [('081', '234', '5678'), ('089', '876', '5432')]

ใช้ Group ต่อ: จัดรูปแบบเบอร์ให้เหมือนกัน และปิดเลขกลางก่อนส่งต่อ

พอแยกท่อนได้แล้ว งานถัดไปที่มักตามมาคือทำให้ทุกเบอร์หน้าตาเหมือนกันก่อนบันทึกลงชีต ตรงนี้ไม่ต้องเขียนลูปเอง ใช้คำสั่งแทนที่ข้อความของแต่ละภาษา แล้วอ้างถึงแต่ละ Group ด้วยเลขลำดับ ใน JavaScript เขียนเป็น $1 $2 $3

console.log(text.replace(/(0\d{2})-?(\d{3})-?(\d{4})/g, "$1-$2-$3"));
// ติดต่อ: 081-234-5678, 089-876-5432 อีเมล sales@example.com / a.team@shop.co.th โทร 02-123-4567

ฝั่ง Python ใช้ re.sub และอ้าง Group ด้วย \1 \2 \3 ผลออกมาเหมือนกันทุกตัวอักษร

print(re.sub(r"(0\d{2})-?(\d{3})-?(\d{4})", r"\1-\2-\3", text))
# ติดต่อ: 081-234-5678, 089-876-5432 อีเมล sales@example.com / a.team@shop.co.th โทร 02-123-4567

อีกงานที่มีประโยชน์กว่าที่คิดคือปิดเลขกลางก่อนส่งข้อความให้คนอื่นดู เช่น เวลาจะแปะ Log ลงแชตทีมเพื่อถามปัญหา ข้อมูลเบอร์ลูกค้าไม่ควรหลุดออกไปทั้งเบอร์ แค่เปลี่ยนท่อนกลางเป็น XXX ก็พอ

console.log(text.replace(/(0\d{2})-?(\d{3})-?(\d{4})/g, "$1-XXX-$3"));
// ติดต่อ: 081-XXX-5678, 089-XXX-5432 อีเมล sales@example.com / a.team@shop.co.th โทร 02-123-4567

สังเกตว่าเบอร์ออฟฟิศ 02-123-4567 ไม่ถูกแตะเลย เพราะ Pattern ของเราไม่เคยจับมันตั้งแต่แรก นี่คือข้อดีของการประกอบ Pattern ทีละชิ้นแล้วทดสอบทุกขั้น คุณรู้ชัดว่ามันจะจับอะไร และจะไม่จับอะไร

ดึงอีเมล และทำไม Regex ตรวจอีเมลได้แค่ "ประมาณ"

ใช้ชิ้นที่มีอยู่แล้วประกอบ Pattern อีเมลแบบง่าย: ตัวอักษร ตัวเลข หรือจุดหนึ่งตัวขึ้นไป ตามด้วย @ ตามด้วยชุดแบบเดียวกันอีกชุด

print(re.findall(r"[\w.]+@[\w.]+", text))
# ['sales@example.com', 'a.team@shop.co.th']

ใช้กับข้อความตัวอย่างได้ผล แต่ลองเจอประโยคที่จบด้วยจุดดู

print(re.findall(r"[\w.]+@[\w.]+", "ส่งมาที่ info@example.com."))
# ['info@example.com.']

จุดท้ายประโยคติดมาด้วย เพราะ Pattern ยอมให้มีจุดตรงไหนก็ได้ ถึงจะแก้ตรงนี้ได้ ก็จะมีกรณีใหม่โผล่มาเรื่อยๆ ขนาดข้อกำหนด HTML เองยังยอมรับตรงๆ ว่านิยามอีเมลที่ใช้ตรวจช่อง input type="email" เป็นการจงใจไม่ทำตาม RFC 5322 เพราะมองว่ามาตรฐานนั้นทั้งเข้มเกินไป คลุมเครือเกินไป และหลวมเกินไปจนใช้งานจริงไม่ได้ แล้วกำหนด Regex ของตัวเองที่ยาวกว่า 130 ตัวอักษรแทน

บทเรียนคือ Regex บอกได้แค่ว่าอีเมล "หน้าตาเหมือนอีเมล" แต่บอกไม่ได้ว่ามีกล่องจดหมายนั้นอยู่จริง วิธีที่ใช้กันในระบบจริงคือเช็กรูปแบบคร่าวๆ พอกันพิมพ์ผิดชัดๆ แล้วส่งลิงก์ยืนยันไปที่อีเมลนั้น

Greedy กับ Lazy: ทำไมมันกินเกิน

ค่าเริ่มต้นของ * และ + คือแบบ Greedy (โลภ) คือกินให้ยาวที่สุดเท่าที่ยังเข้ารูปอยู่ ลองหาข้อความในแท็ก <b> ดู

const html = "<b>ราคา</b> <b>โปร</b>";
console.log(html.match(/<b>.*<\/b>/)[0]);
// <b>ราคา</b> <b>โปร</b>
console.log(html.match(/<b>.*?<\/b>/)[0]);
// <b>ราคา</b>

แค่เติม ? หลัง * ก็กลายเป็นแบบ Lazy (กินให้น้อยที่สุด) หยุดที่ </b> ตัวแรกที่เจอ เอกสาร Python ใช้ตัวอย่างแบบเดียวกันนี้อธิบายเรื่อง Non-greedy เหมือนกัน

เลขไทย ๐-๙: จุดที่ JavaScript กับ Python ไม่เหมือนกัน

อันนี้เป็นเรื่องที่คนเขียนโค้ดภาษาไทยควรรู้ และเราเจอตอนรันตัวอย่างจริง สมมติผู้ใช้กรอกเบอร์เป็นเลขไทย ๐๘๑๒๓๔๕๖๗๘

// JavaScript
console.log(/^\d{10}$/.test("๐๘๑๒๓๔๕๖๗๘")); // false
# Python
print(re.fullmatch(r"\d{10}", "๐๘๑๒๓๔๕๖๗๘"))
# <re.Match object; span=(0, 10), match='๐๘๑๒๓๔๕๖๗๘'>
print(re.fullmatch(r"\d{10}", "๐๘๑๒๓๔๕๖๗๘", flags=re.ASCII))
# None

Pattern เดียวกันแต่ผลตรงข้ามกัน เพราะเอกสาร Python ระบุว่าสำหรับข้อความแบบ str นั้น \d ตรงกับตัวเลขทุกระบบใน Unicode ไม่ใช่แค่ 0-9 จะให้เหลือแค่ 0-9 ต้องใส่ Flag re.ASCII ส่วน JavaScript นั้น \d คือ 0-9 เท่านั้น ถ้าระบบของคุณมีทั้งหน้าเว็บ JavaScript และหลังบ้าน Python ที่ตรวจข้อมูลชุดเดียวกัน สองฝั่งจะตัดสินไม่ตรงกันทันที ทางที่ปลอดภัยคือเขียน [0-9] ให้ชัดไปเลย

งานที่ Regex ไม่ควรรับ และเครื่องมือช่วยทดสอบ

ตัวอย่างแท็ก <b> ข้างบนใช้แค่อธิบายเรื่อง Greedy อย่าเอาไปแกะหน้าเว็บจริง HTML ซ้อนแท็กกันได้หลายชั้น บางแท็กละแท็กปิดได้ และ Attribute เรียงสลับได้ Regex ตามไม่ทันทุกแบบ ให้ใช้ตัวอ่าน HTML ของภาษานั้นแทน เช่น DOMParser ใน JavaScript หรือ html.parser ใน Python ข้อมูลที่มีโครงสร้างอื่นก็เหมือนกัน ถ้าเป็นข้อมูลแบบที่เล่าไว้ในบทความ JSON คือไฟล์อะไร ให้ใช้ JSON.parse หรือ json.loads ไม่ต้องเขียน Regex แกะเอง

ระหว่างหัดเขียน เครื่องมืออย่าง regex101 ช่วยได้มาก วาง Pattern กับข้อความตัวอย่างลงไป แล้วดูได้ทันทีว่าแต่ละชิ้นจับตัวไหน แต่พอได้ Pattern แล้ว ให้เอามารันในภาษาที่ใช้จริงอีกรอบเสมอ เพราะอย่างที่เห็นในเรื่องเลขไทย แต่ละภาษาตีความสัญลักษณ์เดียวกันไม่เหมือนกันทุกตัว

ถ้ายังลังเลว่าจะฝึกกับภาษาไหนก่อน บทความ JavaScript vs Python มือใหม่ควรเริ่มจากภาษาไหนดี ช่วยตัดสินใจได้ และถ้าอยากเห็น Regex ในภาษา Go เทียบกับ JavaScript ต่อ อ่านได้ใน JS2GO EP.25 การใช้งาน Regular Expressions ใน JavaScript และ Go


สรุป: Regex คือการเขียนหน้าตาของสิ่งที่อยากหา

จากโจทย์ดึงเบอร์กับอีเมลโจทย์เดียว เราใช้ไปแค่ห้าชิ้น: ตัวแทนกลุ่มอย่าง \d ตัวบอกจำนวนอย่าง {9} และ ? ตัวเลือกใน [] ตัวยึด ^ $ และ Group () ห้าชิ้นนี้ครอบคลุมงานค้นหาและตรวจข้อมูลส่วนใหญ่ที่มือใหม่จะเจอ ที่เหลือค่อยเปิดเอกสารดูเมื่องานต้องใช้

สิ่งที่ควรจำไปพร้อมกันคือขอบเขตของมัน Regex ตรวจได้ว่า "หน้าตาถูก" แต่ยืนยันไม่ได้ว่า "ของจริง" และไม่ใช่เครื่องมือแกะ HTML หรือ JSON ลองหยิบข้อความจริงที่คุณมีอยู่วันนี้สักก้อน แชตกลุ่มหรือไฟล์ Log ก็ได้ แล้วเขียน Pattern ดึงของที่อยากได้ออกมาทีละชิ้นแบบที่ทำในบทความนี้ ทำเองครั้งเดียวจะจำได้มากกว่าอ่านตารางสัญลักษณ์สิบรอบ

ติดตามบทความและเทคนิคสำหรับคนเริ่มต้นสายโปรแกรมเมอร์ได้ที่ช่องทางของเรา

Facebook: Superdev Academy Thailand (https://www.facebook.com/superdev.academy.th)

YouTube: Superdev Academy Channel (https://www.youtube.com/@SuperdevAcademy)

Instagram: @superdevacademy (https://www.instagram.com/superdevacademy/)

TikTok: @superdevacademy (https://www.tiktok.com/@superdevacademy?lang=th-TH)

FAQ: คำถามที่พบบ่อยเกี่ยวกับบทความนี้

รวมคำถามและคำตอบที่ช่วยให้คุณเข้าใจเนื้อหาในบทความนี้ได้ดียิ่งขึ้น