`$` ใน Regular Expression ไม่ได้หมายถึง "จุดสิ้นสุดของสตริง" เสมอไป
(sethmlarson.dev)- ใน Python
reนั้น$อาจแมตช์ได้ไม่เพียงแค่ จุดสิ้นสุดของสตริง แต่ยังรวมถึง ตำแหน่งก่อน newline ตัวสุดท้าย ที่อยู่ท้ายสตริง แม้จะไม่ได้เปิดโหมด multiline ก็ตาม - ถึง
^จะดูเหมือนหมายถึง “จุดเริ่มต้นของสตริง” แต่ก็ไม่ควรสรุปว่า$จะทำงานแบบสมมาตรอย่างสมบูรณ์ เพราะความหมายจริงนั้นแตกต่างกันไปตาม implementation ของ regular expression - ผลลัพธ์ของ
$,\z,\Zกับ"cat\n"แตกต่างกันระหว่าง PHP, ECMAScript, Python, Go, Java 8, .NET 7.0 และ Rust โดย\zของ Python ถูกเพิ่มเข้ามาใหม่ใน Python 3.14 - ถ้ายอมให้ newline ท้ายสตริงถูกนับรวมด้วย
$ในโหมด multiline จะสามารถแมตช์"cat\n"ได้บนทุกแพลตฟอร์มในตาราง แต่ถ้าต้องการแมตช์เฉพาะ จุดสิ้นสุดโดยไม่รวม newline การเลือกไวยากรณ์จะต่างกัน - หากต้องการไม่ให้แมตช์ newline ตัวสุดท้าย แพลตฟอร์มส่วนใหญ่ควรใช้
\zส่วน Python ก่อน 3.14 และ ECMAScript ต้องพิจารณาทางเลือกอื่นตามลำดับ
ตำแหน่งที่ $ แมตช์ใน Python re
- ในโมดูล regular expression
reของ Python นั้น$อาจแมตช์ได้ทั้ง จุดสิ้นสุดของสตริง หรือจุดก่อน newline ตัวสุดท้ายที่ท้ายสตริง แม้จะไม่ได้เปิดโหมด multiline ก็ตาม cat$แมตช์กับ"lolcat"และไม่แมตช์กับ"internet cat video"จึงดูเหมือนตรงไปตรงมา แต่เมื่อสตริงลงท้ายด้วย newline เช่น"cat\n"ผลลัพธ์อาจไม่เป็นไปตามที่คาด- เมื่อกำหนด
re.MULTILINEแล้ว$จะแมตช์ทั้งจุดสิ้นสุดของสตริงและจุดสิ้นสุดของแต่ละบรรทัด นั่นคือก่อน newline แต่ละตัว - แม้ในค่าเริ่มต้น
$ก็ยังแมตช์ที่จุดสิ้นสุดของสตริง และถ้าสตริงลงท้ายด้วย newline ก็จะแมตช์ก่อน newline ตัวนั้นด้วย
การแมตช์โดยไม่รวม newline ตัวสุดท้าย
- หากต้องการแมตช์เฉพาะจุดสิ้นสุดของสตริงอย่างเคร่งครัด การใช้
$อย่างเดียวอาจไม่พอ และ\zกับ\Zก็เป็นตัวเลือกของ end anchor - เมื่ออ้างอิงจาก เอกสาร regular expression ของ Python และ คำอธิบายไวยากรณ์ regular expression อื่น ๆ จะเห็นว่าการรองรับและความหมายของ
\zและ\Zแตกต่างกันไปตาม implementation - ความแตกต่างสำหรับ
"cat\n"มีดังนี้- PHP:
"cat$"แมตช์ไม่ว่าจะเป็นโหมด multiline หรือไม่,"cat\z"ไม่แมตช์ และ"cat\Z"แมตช์ - ECMAScript:
"cat$"แบบ multiline แมตช์,"cat$"ที่ไม่ใช่ multiline ไม่แมตช์ และไม่รองรับ\zกับ\Z - Python:
"cat$"แมตช์ไม่ว่าจะเป็นโหมด multiline หรือไม่ และ"cat\z"กับ"cat\Z"ไม่แมตช์กับ"cat\n" - Go และ Rust:
"cat$"แบบ multiline แมตช์,"cat$"ที่ไม่ใช่ multiline และ"cat\z"ไม่แมตช์ และไม่รองรับ\Z - Java 8 และ .NET 7.0:
"cat$"แมตช์ไม่ว่าจะเป็นโหมด multiline หรือไม่,"cat\z"ไม่แมตช์ และ"cat\Z"แมตช์
- PHP:
\zของ Python ถูกเพิ่มเข้ามาใหม่ใน Python 3.14 โดยเวอร์ชันก่อนหน้านั้นยังไม่รองรับ- หากยอมให้ newline ท้ายสตริงถูกแมตช์ได้
$ในโหมด multiline จะสามารถแมตช์"cat\n"ได้อย่างสม่ำเสมอบนทุกแพลตฟอร์มในตาราง - หากไม่ต้องการให้แมตช์ newline ท้ายสตริง แพลตฟอร์มส่วนใหญ่ควรใช้
\z, ส่วน Python ก่อน 3.14 ควรใช้\Zและ ECMAScript ควรใช้$ที่ไม่อยู่ในโหมด multiline - ข้อมูลในตารางรวบรวมจาก regex101.com และไม่ได้ทดสอบกับ runtime จริง
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ผมคิดมาตลอดว่า
^คือ “จุดเริ่มต้นของบรรทัด” และ$คือ “จุดสิ้นสุดของบรรทัด”เวลาทำงานกับ regex มักจะประมวลผลข้อความทีละบรรทัด ผลลัพธ์เลยมักเหมือนกัน แต่ภาพจำของผมต่อโอเปอเรเตอร์พวกนี้ยังใกล้กับ “บรรทัด” มากกว่า “สตริง” อยู่ดี
น่าจะเป็นเพราะรู้จัก regex ผ่าน
grepเป็นหลัก เลยติดนิสัยมองอินพุตเป็นบรรทัด ไม่ใช่สตริงใช้ regex มาเกือบ 20 ปีแล้ว แต่เหมือนเพิ่งเคยได้ยินว่ามีคนบอกว่า
$คือ จุดสิ้นสุดของสตริง ผมมองว่ามันเป็นจุดสิ้นสุดของบรรทัดมาตลอด^ว่า “จุดเริ่มต้นของสตริง”จริง ๆ แล้วเหมือนกับที่
$เป็น “จุดสิ้นสุดของบรรทัด”^ก็คือ “จุดเริ่มต้นของบรรทัด” และจุดเริ่มต้นของสตริงน่าจะใกล้กับ\Aส่วนจุดสิ้นสุดของสตริงคือ\Zมากกว่า$โดยปริยายจะทำงานคล้าย positive lookahead assertion สำหรับจุดสิ้นสุดของสตริงมันไม่ได้แมตช์และกินอักขระขึ้นบรรทัดใหม่
จะไปแมตช์ตำแหน่งขึ้นบรรทัดใหม่เฉพาะในโหมดหลายบรรทัดเท่านั้น แต่ตอนนั้นก็ดูเหมือนจะไม่กินมันอยู่ดี
ที่จริงแล้วเมื่อใช้
$ผมสร้าง regex ที่แคปเจอร์อักขระสุดท้ายของบรรทัดหนึ่ง กินอักขระขึ้นบรรทัดใหม่ แล้วแคปเจอร์อักขระแรกของบรรทัดถัดไปไม่ได้เลย กลุ่มแคปเจอร์ก็จบอยู่ตรง$เฉย ๆgrepPOSIX regex กับ regex ของ Python ไม่เหมือนกัน
โดยทั่วไป syntax ของ regex ไม่ได้เป็นสากล จึงต้องดูเอกสารของ implementation ที่ใช้อยู่
ตาม POSIX บทที่ 9 regex ทำงานบนสตริง แต่ยูทิลิตีบางตัวจำกัดการประมวลผลไว้ทีละบรรทัด
อีกทั้ง
$ถูกระบุว่าเป็น anchor ที่ยึดกับจุดสิ้นสุดของสตริงเป้าหมายที่จะนำมาแมตช์ ดังนั้นท้ายที่สุดแล้ว$จะหมายถึงจุดสิ้นสุดของสตริงหรือจุดสิ้นสุดของบรรทัด ก็ขึ้นกับยูทิลิตีหรือโหมดที่ใช้เครื่องมือที่ใช้กันทั่วไปอย่าง
grep,sed,awk, Python โดยปริยายทำงานทีละบรรทัด จึงมักถือว่าเป็นจุดสิ้นสุดของบรรทัดไม่มี syntax ของ regex แบบสากลเพียงหนึ่งเดียว
ถ้าไม่รู้ว่าใช้ภาษาและออปชันใดอยู่ ก็อ่านหรือเขียน regex ให้มั่นคงไม่ได้
https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1...
ถ้าเป็นหัวข้อนี้ เหมาะมากที่จะแนะนำ Robert Elder ให้คนที่ยังไม่รู้จัก
เขาทำคอนเทนต์ดี ๆ ทั้งบน YouTube และบล็อก และใน ซีรีส์ regex ก็เจาะลึกความแตกต่างของพฤติกรรม regex ที่เครื่องมือต่าง ๆ implement ไว้ค่อนข้างมาก
วิดีโอล่าสุดก็ดี: https://www.youtube.com/watch?v=ys7yUyyQA-Y
ยังมีคอนเทนต์อีกมากที่ผู้อ่าน HN น่าจะสนใจ รวมถึงหัวข้ออย่างความเป็นจริงและความลำบากของงานคอนซัลต์
https://www.youtube.com/@RobertElderSoftware
https://blog.robertelder.org/
https://blog.robertelder.org/regular-expressions/
https://www.youtube.com/watch?v=cK87ktENPrI
ตอนเรียน Perl regex เป็นหนึ่งในสิ่งแรก ๆ ที่ผมซึมซับจนใช้คล่องจริง ๆ และจนถึงตอนนี้ Perl ก็ยังนั่งอยู่สบาย ๆ ในมุมหนึ่งของใจ ต้องขอบคุณหนังสือ “Camel”
ตอนนี้ความรู้ที่สำคัญที่สุดคือ แต่ละ implementation ต่างกัน ผมเลยติดนิสัยหยิบตารางอ้างอิงของสิ่งนั้น ๆ ขึ้นมาดูทุกครั้งที่ต้องทำงานอะไรสักอย่าง
ตัวอย่างเช่น regex ของ Emacs ไม่รองรับอักขระคำแบบ
\wและต้องใช้ character class ที่คล้าย\s_-แทน ซึ่งน่าหงุดหงิด แต่ผมมองว่า Emacs ยอดเยี่ยมที่สุดด้านเอกสารและการค้นพบความสามารถยูทิลิตีบางตัวต้อง escape วงเล็บ บางตัวไม่ต้อง และพฤติกรรมนี้บางทีก็ตั้งค่าได้ บางทีก็ไม่ได้
ผ่านมาหมดแล้วทั้งช่วงสับสน หงุดหงิด และปฏิเสธ ตอนนี้ก็แค่ยอมรับมัน
แนวคิดเหมือนกันทุกที่ แต่ ภาษาถิ่น เปลี่ยนไป
โดยเฉพาะในเชลล์ แทนที่จะต้องนึกว่า
sed/grep/awkเป็น GNU หรือ BSD ผมมักจะยัดperlเข้าไปใน pipeline บ่อยกว่ามากPerl ดูเหมือนแมวเดินเหยียบคีย์บอร์ด
เหมือนได้ยินเสียงผู้จัดการฝ่ายสรรหางานแย่ ๆ จำนวนมากกำลังเพิ่มคำถาม “ใน regex จะแมตช์จุดสิ้นสุดของสตริงอย่างไร?” เข้าไปในรายการ คำถามหลอก
การตัด Perl ออกจากรายการเกี่ยวกับ regex นี่แปลก
เอกสาร
perlreอธิบาย$ไว้แบบนี้: แมตช์จุดสิ้นสุดของสตริง หรือแมตช์ก่อนอักขระขึ้นบรรทัดใหม่ที่อยู่ท้ายสตริง หรือถ้าใช้/mก็แมตช์ก่อนอักขระขึ้นบรรทัดใหม่ใด ๆอาจหมายความได้ด้วยว่าทุกวันนี้ Perl ถูกดันออกไปนอกความสนใจมากแค่ไหน
Raku ซึ่งเดิมคือ Perl 6 กำหนดให้
^และ$เป็น จุดเริ่มต้น/จุดสิ้นสุดของสตริง และนำ^^กับ$$มาใช้เป็นจุดเริ่มต้น/จุดสิ้นสุดของบรรทัดไม่มีโหมดหลายบรรทัด และก็ไม่จำเป็นด้วย
\hคือช่องว่างแนวนอน และ\vคือช่องว่างแนวตั้งเพราะคิดใหม่และเขียนใหม่ทั้งหมด จึงมีข้อดีตรงที่ได้เรียนรู้จากข้อเท็จจริงว่า behavior แบบเดิมทำให้ผู้คนแปลกใจ
รู้สึกเหมือนเอา ไวยากรณ์ที่ดูเหมือน line noise ซึ่งฝึกใช้มาหลายสิบปี มาสลับปนกันแบบสุ่ม
ถ้าค่าเริ่มต้นเป็นตรงกันข้ามน่าจะดูชัดเจนกว่า
^กับ$ใช้กับบรรทัด และ^^กับ$$ใช้กับสตริง น่าจะเป็นธรรมชาติกว่าเพราะมันดูเหมือน
^^line1$\n^line2$\n^line3$\n$แถม Perl 6 ไม่ได้มีอยู่ทุกที่ แต่ Perl 5 มีอยู่ทุกที่
^^ดู “เป็นจุดเริ่มต้น” มากกว่า^เพราะปกตินำบรรทัดเข้าไปประมวลผลด้วย regex ดังนั้นการเลือกให้
^และ$ตัวเดียวใช้กับทั้งสตริงจึงช่วยรักษาความเข้ากันได้ย้อนหลังไว้ได้ระดับหนึ่งสงสัยว่ามีใครมองว่า regex ถูกทำให้เป็นมาตรฐานแล้วจริง ๆ หรือเปล่า
ทุกครั้งที่ย้ายไปสภาพแวดล้อมใหม่ ก็ต้องเรียนใหม่เสมอ
คงมี dialect ของ regex อีกมาก แต่ผมไม่เจอ และสิ่งที่ผมรู้ก็แก้ปัญหาได้เป็นส่วนใหญ่
คล้ายกับการขับรถเช่า
มันเคลื่อนไหวต่างจากรถของตัวเองเล็กน้อย มีฟีเจอร์ที่หายไปและฟีเจอร์ที่เพิ่มมา แต่โดยรวมแล้วส่วนใหญ่ก็คล้ายกันพอสมควร
ดังนั้นอย่างน้อยผมก็มองว่า regex ถูกทำให้เป็นมาตรฐานโดย มาตรฐานทางการ ที่เผยแพร่หลายฉบับ
https://open-std.org/jtc1/sc22/…
https://pubs.opengroup.org/onlinepubs/9699919799/…
https://262.ecma-international.org/14.0/…
หลายระบบรวมถึง JavaScript implement PCRE เพราะ Perl เพิ่มส่วนขยายที่มีประโยชน์จำนวนมากให้กับระบบของ POSIX
เท่าที่จำได้ RE2 มุ่งไปทางการลดปัญหาประสิทธิภาพและ behavior แปลก ๆ ของระบบเดิม และผมเคยเข้าใจว่ามันถูก implement ทั้งหมดด้วย Go
ภายหลังถึงรู้ว่า RE2 ออกมาก่อน Go ซึ่งผมไม่เคยรู้มาก่อน
ถึงอย่างนั้น ความหมายของ
$และวิธีเปลี่ยนเป็น โหมดหลายบรรทัด มักจะค่อนข้างสอดคล้องกันผู้คนกำลังสับสนระหว่างสตริงกับบรรทัด
สตริงคือ sequence ของอักขระ ส่วนบรรทัดมองได้สองแบบ
ถ้ามอง line break เป็นตัวจบบรรทัด บรรทัดคืออักขระที่ไม่ใช่ line break จำนวน 0 ตัวขึ้นไปตามด้วย line break และถ้าท้ายสุดไม่มี line break ก็ไม่ใช่บรรทัดที่สมบูรณ์
POSIX ใช้มุมมองนี้
ถ้ามอง line break เป็นตัวคั่นบรรทัด บรรทัดคือ sequence ของอักขระที่ไม่ใช่ line break จำนวน 0 ตัวขึ้นไป
ไม่ว่าจะมองแบบไหน เนื้อหาของบรรทัดก็สิ้นสุดก่อน line break
semantics ของ
^และ$เป็นแบบ อิงบรรทัด ไม่ว่าจะเป็นโหมดบรรทัดเดียวหรือโหมดหลายบรรทัดสำหรับ semantics แบบอิงสตริง หรือถ้าจัดการไฟล์ก็อาจมองว่าเป็น semantics ของทั้งไฟล์ ควรใช้
\Aและ\Zหรือสิ่งที่เทียบเท่าการตีความทั้งสองแบบมีข้อดี
เวลาส่งข้อความผ่านการเชื่อมต่อแบบ serial การให้ line break เป็นตัวจบบรรทัดช่วยให้รู้ได้ง่ายว่าได้รับบรรทัดครบแล้วหรือไม่
ในไฟล์ข้อความ การมอง line break เป็นตัวคั่นบรรทัดอาจสะดวกกว่าเพราะบรรทัดสุดท้ายจะไม่กลายเป็นสถานะผิดพลาด แต่การมีตัวจบบรรทัดก็ช่วยตรวจจับบรรทัดที่เขียนไม่สมบูรณ์ได้
เรื่องนี้ทำให้เกิดบั๊กร้ายแรงหลายครั้งในแอปที่ใช้ Ruby
ต้องใช้
\A\zเสมอhttps://homakov.blogspot.com/2012/05/saferweb-injects-in-var...
https://sakurity.com/blog/2015/02/28/openuri.html
https://sakurity.com/blog/2015/06/04/mongo_ruby_regexp.html