3 คะแนน โดย GN⁺ 2024-03-21 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ใน 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" แมตช์
  • \z ของ Python ถูกเพิ่มเข้ามาใหม่ใน Python 3.14 โดยเวอร์ชันก่อนหน้านั้นยังไม่รองรับ
  • หากยอมให้ newline ท้ายสตริงถูกแมตช์ได้ $ ในโหมด multiline จะสามารถแมตช์ "cat\n" ได้อย่างสม่ำเสมอบนทุกแพลตฟอร์มในตาราง
  • หากไม่ต้องการให้แมตช์ newline ท้ายสตริง แพลตฟอร์มส่วนใหญ่ควรใช้ \z, ส่วน Python ก่อน 3.14 ควรใช้ \Z และ ECMAScript ควรใช้ $ ที่ไม่อยู่ในโหมด multiline
  • ข้อมูลในตารางรวบรวมจาก regex101.com และไม่ได้ทดสอบกับ runtime จริง

1 ความคิดเห็น

 
GN⁺ 2024-03-21
ความคิดเห็นบน Hacker News
  • ผมคิดมาตลอดว่า ^ คือ “จุดเริ่มต้นของบรรทัด” และ $ คือ “จุดสิ้นสุดของบรรทัด”
    เวลาทำงานกับ regex มักจะประมวลผลข้อความทีละบรรทัด ผลลัพธ์เลยมักเหมือนกัน แต่ภาพจำของผมต่อโอเปอเรเตอร์พวกนี้ยังใกล้กับ “บรรทัด” มากกว่า “สตริง” อยู่ดี
    น่าจะเป็นเพราะรู้จัก regex ผ่าน grep เป็นหลัก เลยติดนิสัยมองอินพุตเป็นบรรทัด ไม่ใช่สตริง

    • ผมเห็นหัวข้อแล้วก็คิดว่า “ก็แน่นอนว่าไม่ใช่อยู่แล้ว ไปได้ยินเรื่องแบบนั้นมาจากไหน?”
      ใช้ regex มาเกือบ 20 ปีแล้ว แต่เหมือนเพิ่งเคยได้ยินว่ามีคนบอกว่า $ คือ จุดสิ้นสุดของสตริง ผมมองว่ามันเป็นจุดสิ้นสุดของบรรทัดมาตลอด
    • ผมสะดุดตรงที่บทความเรียก ^ ว่า “จุดเริ่มต้นของสตริง”
      จริง ๆ แล้วเหมือนกับที่ $ เป็น “จุดสิ้นสุดของบรรทัด” ^ ก็คือ “จุดเริ่มต้นของบรรทัด” และจุดเริ่มต้นของสตริงน่าจะใกล้กับ \A ส่วนจุดสิ้นสุดของสตริงคือ \Z มากกว่า
    • ผมก็คิดแบบนั้นเหมือนกัน แต่พอลองเองใน Perl แล้ว $ โดยปริยายจะทำงานคล้าย positive lookahead assertion สำหรับจุดสิ้นสุดของสตริง
      มันไม่ได้แมตช์และกินอักขระขึ้นบรรทัดใหม่
      จะไปแมตช์ตำแหน่งขึ้นบรรทัดใหม่เฉพาะในโหมดหลายบรรทัดเท่านั้น แต่ตอนนั้นก็ดูเหมือนจะไม่กินมันอยู่ดี
      ที่จริงแล้วเมื่อใช้ $ ผมสร้าง regex ที่แคปเจอร์อักขระสุดท้ายของบรรทัดหนึ่ง กินอักขระขึ้นบรรทัดใหม่ แล้วแคปเจอร์อักขระแรกของบรรทัดถัดไปไม่ได้เลย กลุ่มแคปเจอร์ก็จบอยู่ตรง $ เฉย ๆ
    • สำหรับผม Vim ต่างหากที่ฝังความเข้าใจแบบนั้นให้ ไม่ใช่ grep
  • POSIX 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 วงเล็บ บางตัวไม่ต้อง และพฤติกรรมนี้บางทีก็ตั้งค่าได้ บางทีก็ไม่ได้
    ผ่านมาหมดแล้วทั้งช่วงสับสน หงุดหงิด และปฏิเสธ ตอนนี้ก็แค่ยอมรับมัน
    แนวคิดเหมือนกันทุกที่ แต่ ภาษาถิ่น เปลี่ยนไป

    • สมองผมคิดเป็น Perl regex แล้วค่อยแปลให้เข้ากับส่วนที่ไม่สม่ำเสมอของภาษาที่กำลังใช้อยู่
      โดยเฉพาะในเชลล์ แทนที่จะต้องนึกว่า sed/grep/awk เป็น GNU หรือ BSD ผมมักจะยัด perl เข้าไปใน pipeline บ่อยกว่ามาก
    • อยากรู้ว่าซึมซับจนใช้คล่องได้ยังไง
      Perl ดูเหมือนแมวเดินเหยียบคีย์บอร์ด
  • เหมือนได้ยินเสียงผู้จัดการฝ่ายสรรหางานแย่ ๆ จำนวนมากกำลังเพิ่มคำถาม “ใน regex จะแมตช์จุดสิ้นสุดของสตริงอย่างไร?” เข้าไปในรายการ คำถามหลอก

  • การตัด Perl ออกจากรายการเกี่ยวกับ regex นี่แปลก
    เอกสาร perlre อธิบาย $ ไว้แบบนี้: แมตช์จุดสิ้นสุดของสตริง หรือแมตช์ก่อนอักขระขึ้นบรรทัดใหม่ที่อยู่ท้ายสตริง หรือถ้าใช้ /m ก็แมตช์ก่อนอักขระขึ้นบรรทัดใหม่ใด ๆ

    • การละ Perl ซึ่งอาจเรียกได้ว่าเป็นภาษาที่เชื่อมโยงกับ regex มากที่สุด ถือว่าเป็นการตกหล่นค่อนข้างใหญ่
      อาจหมายความได้ด้วยว่าทุกวันนี้ Perl ถูกดันออกไปนอกความสนใจมากแค่ไหน
  • Raku ซึ่งเดิมคือ Perl 6 กำหนดให้ ^ และ $ เป็น จุดเริ่มต้น/จุดสิ้นสุดของสตริง และนำ ^^ กับ $$ มาใช้เป็นจุดเริ่มต้น/จุดสิ้นสุดของบรรทัด
    ไม่มีโหมดหลายบรรทัด และก็ไม่จำเป็นด้วย
    \h คือช่องว่างแนวนอน และ \v คือช่องว่างแนวตั้ง
    เพราะคิดใหม่และเขียนใหม่ทั้งหมด จึงมีข้อดีตรงที่ได้เรียนรู้จากข้อเท็จจริงว่า behavior แบบเดิมทำให้ผู้คนแปลกใจ

    • ดังนั้นคนหัวดื้ออย่างผมจึงใช้ Perl 6 ไม่ได้
      รู้สึกเหมือนเอา ไวยากรณ์ที่ดูเหมือน line noise ซึ่งฝึกใช้มาหลายสิบปี มาสลับปนกันแบบสุ่ม
      ถ้าค่าเริ่มต้นเป็นตรงกันข้ามน่าจะดูชัดเจนกว่า
      ^ กับ $ ใช้กับบรรทัด และ ^^ กับ $$ ใช้กับสตริง น่าจะเป็นธรรมชาติกว่า
      เพราะมันดูเหมือน ^^line1$\n^line2$\n^line3$\n$
      แถม Perl 6 ไม่ได้มีอยู่ทุกที่ แต่ Perl 5 มีอยู่ทุกที่
    • ถ้าเป็นผมคงเลือกตรงกันข้ามเป๊ะ
      ^^ ดู “เป็นจุดเริ่มต้น” มากกว่า ^
    • regex ที่ผมเขียนแทบทั้งหมดตั้งอยู่บนสมมติฐานว่าเป็น จุดเริ่มต้น/จุดสิ้นสุดของสตริง
      เพราะปกตินำบรรทัดเข้าไปประมวลผลด้วย regex ดังนั้นการเลือกให้ ^ และ $ ตัวเดียวใช้กับทั้งสตริงจึงช่วยรักษาความเข้ากันได้ย้อนหลังไว้ได้ระดับหนึ่ง
  • สงสัยว่ามีใครมองว่า regex ถูกทำให้เป็นมาตรฐานแล้วจริง ๆ หรือเปล่า
    ทุกครั้งที่ย้ายไปสภาพแวดล้อมใหม่ ก็ต้องเรียนใหม่เสมอ

    • มีอยู่ช่วงหนึ่งที่ผมรู้สึกว่ารู้ทุก dialect แล้ว
      คงมี dialect ของ regex อีกมาก แต่ผมไม่เจอ และสิ่งที่ผมรู้ก็แก้ปัญหาได้เป็นส่วนใหญ่
      คล้ายกับการขับรถเช่า
      มันเคลื่อนไหวต่างจากรถของตัวเองเล็กน้อย มีฟีเจอร์ที่หายไปและฟีเจอร์ที่เพิ่มมา แต่โดยรวมแล้วส่วนใหญ่ก็คล้ายกันพอสมควร
    • ไลบรารีมาตรฐาน ISO/IEC 14882 C++ กำหนดให้ต้องมีการ implement ไวยากรณ์ regex มาตรฐานโดยพฤตินัยตามกฎหมาย 6 แบบ: IEEE Std 1003.1-2008 หรือก็คือ BRE, ERE, awk, grep, egrep ของ POSIX และ ECMA-262 EcmaScript 3
      ดังนั้นอย่างน้อยผมก็มองว่า regex ถูกทำให้เป็นมาตรฐานโดย มาตรฐานทางการ ที่เผยแพร่หลายฉบับ
      https://open-std.org/jtc1/sc22/…
      https://pubs.opengroup.org/onlinepubs/9699919799/…
      https://262.ecma-international.org/14.0/…
    • กลุ่มหลัก ๆ ที่ผมรู้จักคือ POSIX, Perl/PCRE และ RE2 ที่ใช้กันในฝั่ง Go ประมาณนี้
      หลายระบบรวมถึง JavaScript implement PCRE เพราะ Perl เพิ่มส่วนขยายที่มีประโยชน์จำนวนมากให้กับระบบของ POSIX
      เท่าที่จำได้ RE2 มุ่งไปทางการลดปัญหาประสิทธิภาพและ behavior แปลก ๆ ของระบบเดิม และผมเคยเข้าใจว่ามันถูก implement ทั้งหมดด้วย Go
      ภายหลังถึงรู้ว่า RE2 ออกมาก่อน Go ซึ่งผมไม่เคยรู้มาก่อน
    • ภาษาที่ออกมาหลัง Perl โดยทั่วไปใช้ไวยากรณ์ regex ของ Perl ในรูปแบบดัดแปลงบางอย่าง แต่ก็มีความแตกต่างเล็ก ๆ น้อย ๆ เสมอ
      ถึงอย่างนั้น ความหมายของ $ และวิธีเปลี่ยนเป็น โหมดหลายบรรทัด มักจะค่อนข้างสอดคล้องกัน
    • น่าสนใจว่า RFC 9485 https://datatracker.ietf.org/doc/rfc9485/ “I-Regexp: An Interoperable Regular Expression Format” เพิ่งเผยแพร่เมื่อเดือนตุลาคมปีที่แล้ว
  • ผู้คนกำลังสับสนระหว่างสตริงกับบรรทัด
    สตริงคือ 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