ข้ามไปยังเนื้อหาหลัก
ProgrammerThailand

AI & Vibe Coding

Regex กับ AI: เขียนให้ถูกและทดสอบให้ครบ

Regex กับ AI: เขียนให้ถูกและทดสอบให้ครบ

Regular expression เป็นเครื่องมือที่นักพัฒนาส่วนใหญ่ใช้บ่อยแต่ไม่ค่อยมีใครจำ syntax ได้หมด จึงไม่แปลกที่หลายคนให้ AI เขียน regex ให้แทบทุกครั้ง ปัญหาคือ regex ที่ AI เขียนมักดูถูกและผ่านตัวอย่างที่เรานึกออก แต่พลาดกับกรณีที่เราไม่ได้นึกถึง เช่น ข้อความภาษาไทย ช่องว่างแปลก ๆ หรือ input ที่ยาวผิดปกติจนเว็บค้าง บทความนี้จะพาดูวิธีใช้ AI กับ regex อย่างเป็นระบบ: ขอคำอธิบายทีละส่วน สร้างชุดทดสอบทั้งกรณีที่ต้องผ่านและไม่ผ่าน ป้องกัน ReDoS และใช้ preg_match ของ PHP กับภาษาไทยให้ถูกต้องด้วย flag u

อย่ารับ regex ที่อ่านไม่ออก: ขอคำอธิบายทีละส่วน

กฎข้อแรกคือ ถ้าเราอ่าน regex ไม่ออก เราก็ตรวจไม่ได้ว่ามันถูก และอีกหกเดือนข้างหน้าก็แก้ไม่ได้ ดังนั้นทุกครั้งที่ให้ AI เขียน regex ให้ขอคำอธิบายแยกทีละส่วนมาด้วยเสมอ

ไม่ดี:
ขอ regex ตรวจเบอร์มือถือไทย

ดี:
เขียน regex สำหรับ PHP preg_match ตรวจเบอร์มือถือไทย
- รับรูปแบบ 0812345678, 081-234-5678 และ +66812345678
- ไม่รับเบอร์บ้าน (02xxxxxxx) และไม่รับเบอร์ที่มีตัวอักษรปน
- ต้องตรงทั้งสตริง ไม่ใช่แค่บางส่วน
ให้ผลลัพธ์เป็น:
1. pattern
2. ตารางอธิบายทีละส่วนของ pattern
3. ตัวอย่างที่ต้องผ่าน 8 ข้อ และไม่ผ่าน 8 ข้อ รวมกรณีขอบ
4. ข้อจำกัดที่ pattern นี้ยังไม่ครอบคลุม

ตัวอย่างคำอธิบายที่ควรได้:

ส่วนความหมาย
^เริ่มต้นสตริง
(?:\+66|0)ขึ้นต้นด้วย +66 หรือ 0 (group แบบไม่จับค่า)
[689]ตัวเลขหลักแรกหลังรหัส คือ 6, 8 หรือ 9
\d-?\d{3}-?\d{4}ตัวเลขที่เหลืออีก 8 หลัก คั่นด้วยขีดได้ในตำแหน่งที่กำหนด
$จบสตริง

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

ชุดทดสอบ: กรณีที่ต้องผ่านและต้องไม่ผ่าน

regex ที่ไม่มีเทสต์คือ regex ที่รอพัง ให้ AI สร้างชุดทดสอบทั้งสองฝั่ง โดยเฉพาะฝั่ง "ต้องไม่ผ่าน" ซึ่งคนมักลืม แล้วเก็บไว้เป็นไฟล์ที่รันซ้ำได้ทุกครั้งที่แก้ pattern

<?php

declare(strict_types=1);

$pattern = '/^(?:\+66|0)[689]\d-?\d{3}-?\d{4}$/D';

$mustMatch = [
    '0812345678',
    '081-234-5678',
    '+66812345678',
    '0612345678',
    '0912345678',
];

$mustNotMatch = [
    '',
    '021234567',        // เบอร์บ้าน
    '081234567',        // สั้นไป
    '08123456789',      // ยาวไป
    '081234567a',       // มีตัวอักษร
    ' 0812345678',      // มีช่องว่างนำหน้า
    "0812345678\n",     // มีขึ้นบรรทัดใหม่ต่อท้าย
    '๐๘๑๒๓๔๕๖๗๘',       // เลขไทย
];

$failed = 0;
foreach ($mustMatch as $s) {
    if (preg_match($pattern, $s) !== 1) {
        echo "FAIL (ควรผ่าน): " . json_encode($s, JSON_UNESCAPED_UNICODE) . PHP_EOL;
        $failed++;
    }
}
foreach ($mustNotMatch as $s) {
    if (preg_match($pattern, $s) !== 0) {
        echo "FAIL (ไม่ควรผ่าน): " . json_encode($s, JSON_UNESCAPED_UNICODE) . PHP_EOL;
        $failed++;
    }
}
echo $failed === 0 ? "ผ่านทั้งหมด" . PHP_EOL : "ไม่ผ่าน {$failed} กรณี" . PHP_EOL;
exit($failed === 0 ? 0 : 1);

มีสองจุดที่น่าสนใจในชุดทดสอบนี้:

  • ขึ้นบรรทัดใหม่ต่อท้าย ใน PCRE ค่าเริ่มต้นของ $ จะยอมให้มี \n หนึ่งตัวท้ายสตริง ถ้าไม่ใส่ modifier D (หรือใช้ \z แทน $) สตริง "0812345678\n" จะผ่าน ซึ่งเป็นช่องโหว่ที่ AI มักมองข้าม
  • เลขไทย pattern นี้ไม่มี flag u ทำให้ \d จับเฉพาะ 0-9 แต่ใน PHP ถ้าใส่ flag u เมื่อไร \d และ \w จะกลายเป็นแบบ Unicode และจับเลขไทย ๐-๙ ได้ด้วย ถ้าต้องการเฉพาะเลขอารบิกให้เขียน [0-9] ตรง ๆ เรื่องแบบนี้ AI ตอบผิดได้ง่าย การมีเทสต์ทำให้เรารู้พฤติกรรมจริงแทนที่จะเดา

ใน Yii2 เราสามารถย้ายชุดทดสอบนี้ไปเป็น unit test และใช้ pattern เดียวกันใน model rule ได้ เช่น ['phone', 'match', 'pattern' => self::PHONE_PATTERN] เพื่อให้มีแหล่งความจริงแหล่งเดียว

ภาษาไทยกับ flag u

ฟังก์ชัน preg_* ของ PHP ทำงานกับสตริงแบบ byte โดยค่าเริ่มต้น แต่ตัวอักษรไทยใน UTF-8 ใช้ 3 byte ต่อตัว ถ้าลืม flag u จะเจอปัญหาแปลก ๆ เช่น . จับแค่ 1 byte ทำให้ตัดตัวอักษรขาดครึ่ง หรือ {1,50} นับเป็น byte ไม่ใช่ตัวอักษร

<?php

$name = 'สมชาย ใจดี';

// ไม่มี u: นับ byte ข้อความไทย 10 ตัวอักษรจะยาวเกิน 20 byte
var_dump(preg_match('/^.{1,20}$/', $name));   // int(0)

// มี u: นับเป็นตัวอักษร
var_dump(preg_match('/^.{1,20}$/u', $name));  // int(1)

// ตรวจชื่อภาษาไทยด้วย Unicode script property
var_dump(preg_match('/^[\p{Thai}]+(?: [\p{Thai}]+)*$/uD', $name)); // int(1)

// ข้อความที่ไม่ใช่ UTF-8 ที่ถูกต้อง preg_match จะคืน false เมื่อใช้ u
$broken = "\xE0\xB8";
var_dump(preg_match('/^.+$/u', $broken));     // bool(false)
echo preg_last_error_msg(), PHP_EOL;          // Malformed UTF-8 characters...

ข้อสังเกตสำคัญ:

  • \p{Thai} ครอบคลุมพยัญชนะ สระ วรรณยุกต์ และเลขไทยด้วย ถ้าไม่ต้องการให้ชื่อมีเลขไทย ต้องเพิ่มการตรวจแยก เช่น ปฏิเสธเมื่อ preg_match('/[๐-๙]/u', $name) === 1
  • เมื่อใส่ flag u ใน PHP \w จะจับตัวอักษรไทยได้ แต่ \b ยังใช้หาขอบเขตคำไทยไม่ได้ เพราะภาษาไทยไม่เว้นวรรคระหว่างคำ ถ้าต้องการตัดคำให้ใช้ IntlBreakIterator จาก extension intl แทน
  • เมื่อใช้ flag u ต้องตรวจค่า false จาก preg_match เสมอ เพราะหมายถึงเกิด error ไม่ใช่ "ไม่ตรง" ใช้ === 1 ในการตรวจ ไม่ใช่แค่ truthy/falsy
  • ข้อความไทยบางครั้งพิมพ์ด้วยลำดับต่างกันแต่หน้าตาเหมือนกัน เช่น สระอำ ถ้าระบบต้องเปรียบเทียบข้อความ ควรพิจารณา normalize ก่อนด้วย Normalizer ของ intl

ReDoS: regex ที่ทำให้เซิร์ฟเวอร์ค้าง

ReDoS (Regular expression Denial of Service) เกิดจาก pattern บางแบบที่เมื่อเจอ input ที่ "เกือบตรงแต่ไม่ตรง" เครื่องจะลองจับคู่ (backtracking) แบบทวีคูณ input ยาวแค่ไม่กี่สิบตัวอักษรก็ทำให้ CPU ทำงานหนักได้ ผู้โจมตีจึงใช้จุดนี้ส่ง input ที่ออกแบบมาให้ระบบช้า

รูปแบบอันตรายที่ควรจำ:

  • quantifier ซ้อน quantifier เช่น (a+)+, (\w*)*, (.*a){10}
  • ทางเลือกที่ทับซ้อนกันภายใต้ quantifier เช่น (a|a)* หรือ (\w|\d)+
  • .* หลายตัวต่อกัน ใน pattern เดียวกับข้อความยาว
<?php

// อันตราย: quantifier ซ้อน เมื่อท้ายสตริงไม่ตรง จะ backtrack มหาศาล
$evil = '/^(\w+\s?)+$/u';
$input = str_repeat('a', 5000) . '!';

$result = preg_match($evil, $input);
if ($result === false) {
    // PHP มีเพดาน pcre.backtrack_limit จึงหยุดและคืน false แทนการค้างตลอดไป
    error_log('regex error: ' . preg_last_error_msg());
}

// ปลอดภัยกว่า: เขียนให้แต่ละตำแหน่งมีทางจับคู่ได้ทางเดียว
$safe = '/^\w+(?:\s\w+)*$/uD';
var_dump(preg_match($safe, $input)); // int(0) และจบเร็ว

PHP มีด่านป้องกันชั้นหนึ่งคือค่า pcre.backtrack_limit และ JIT ของ PCRE แต่เมื่อชนเพดาน preg_match จะคืน false ซึ่งถ้าโค้ดเช็กแค่ falsy ก็จะถือว่า "ไม่ตรง" แบบเงียบ ๆ และยังเสีย CPU ไปจนถึงเพดานแล้ว ดังนั้นแนวทางป้องกันที่ดีคือ:

  1. จำกัดความยาว input ก่อนส่งเข้า regex เช่น ตรวจ mb_strlen() ใน validation rule
  2. หลีกเลี่ยง quantifier ซ้อน และขอให้ AI ตรวจ pattern ด้วยคำถามตรง ๆ ว่า "pattern นี้เสี่ยง catastrophic backtracking หรือไม่ ยกตัวอย่าง input ที่ทำให้ช้า"
  3. ทดสอบด้วย input ยาว ๆ ที่เกือบตรง แล้ววัดเวลา
  4. ตรวจค่า false และ log ด้วย preg_last_error_msg()

อ่านเพิ่มเติมเรื่อง ReDoS ได้จากเอกสารของ OWASP และพฤติกรรมของฟังก์ชัน PCRE ได้จากคู่มือที่ php.net

Workflow สรุปเมื่อใช้ AI กับ regex

  1. เขียนกติกาเป็นภาษาคนให้ชัด รวมตัวอย่างที่ต้องผ่านและไม่ผ่าน
  2. ขอ pattern พร้อมคำอธิบายทีละส่วน และอ่านจนเข้าใจ
  3. สร้างชุดทดสอบเป็นโค้ดที่รันซ้ำได้ เพิ่มกรณีขอบ: ค่าว่าง ช่องว่าง ขึ้นบรรทัดใหม่ ภาษาไทย เลขไทย อีโมจิ ข้อความยาวมาก
  4. ตรวจความเสี่ยง ReDoS และจำกัดความยาว input
  5. ถ้า pattern ซับซ้อนเกินไป ลองถามว่าควรใช้วิธีอื่นแทนหรือไม่ เช่น filter_var($email, FILTER_VALIDATE_EMAIL) สำหรับอีเมล หรือ parser เฉพาะสำหรับ HTML และ JSON

สรุป

  • อย่ารับ regex ที่อ่านไม่ออก ให้ AI อธิบายทีละส่วนและตรวจว่าตรงกับกติกาจริงของระบบ
  • ชุดทดสอบต้องมีทั้งกรณีที่ต้องผ่านและต้องไม่ผ่าน และเก็บเป็นโค้ดที่รันซ้ำได้
  • ข้อความภาษาไทยต้องใช้ flag u เสมอ และตรวจผล preg_match ด้วย === 1 เพราะ false คือ error
  • ใช้ D หรือ \z เมื่อต้องการให้ตรงถึงท้ายสตริงจริง ๆ
  • ระวัง quantifier ซ้อนที่ทำให้เกิด ReDoS และจำกัดความยาว input ก่อนตรวจ

ลองทำ

ให้ AI เขียน regex ตรวจรูปแบบเลขบัตรประชาชนไทยที่รับทั้ง 1234567890123 และ 1-2345-67890-12-3 โดยใช้ prompt แบบ "ดี" ข้างบน นำชุดทดสอบไปใส่ในสคริปต์แบบเดียวกับตัวอย่างเบอร์มือถือ เพิ่มกรณีที่คุณคิดเองอีกอย่างน้อย 5 กรณี (เช่น เลขไทย ขีดผิดตำแหน่ง ขึ้นบรรทัดใหม่ท้ายสตริง) แล้วลองถาม AI ต่อว่า regex ตรวจ checksum ของเลขบัตรได้หรือไม่ ซึ่งคำตอบที่ถูกคือควรตัดขีดออกแล้วคำนวณ checksum ด้วยโค้ด PHP แยกต่างหาก ลองเขียนฟังก์ชันนั้นพร้อมเทสต์ด้วย

ติดปัญหาตอนทำตาม? ถามในชุมชน

ความคิดเห็น (0)

ยังไม่มีความคิดเห็น — เริ่มคุยเป็นคนแรก ถามสิ่งที่สงสัย หรือแชร์ประสบการณ์ของคุณ