คำแนะนำในบทความนี้มุ่งเน้นไปที่ความสามารถในการอ่านนิพจน์ทั่วไปเป็นหลัก โดยการพัฒนานิสัยเหล่านี้ในระหว่างการพัฒนา คุณจะพิจารณาโครงสร้างการออกแบบและนิพจน์ได้ชัดเจนยิ่งขึ้น ซึ่งจะช่วยลดข้อบกพร่องและการบำรุงรักษาโค้ด หากคุณจะรู้สึกผ่อนคลายมากขึ้น เป็นผู้ดูแลโค้ดนี้ด้วยตัวเอง คุณสามารถดูตัวเองและใส่ใจกับประสบการณ์เหล่านี้ด้วยสำนวนปกติในการใช้งานจริงของคุณ
นิพจน์ทั่วไปเขียนยาก อ่านยาก และดูแลรักษายาก มักไม่ตรงกับข้อความที่ไม่คาดคิดหรือข้อความที่ถูกต้องไม่ถูกต้อง การผสมผสานระหว่างความสามารถและความแตกต่างของอักขระเมตาแต่ละตัวทำให้โค้ดไม่สามารถตีความได้โดยไม่ต้องใช้เทคนิคทางปัญญา
เครื่องมือจำนวนมากมีคุณสมบัติที่ทำให้ง่ายต่อการอ่านและเขียนนิพจน์ทั่วไป แต่ก็ไม่มีสำนวนมากนัก สำหรับโปรแกรมเมอร์หลายๆ คน การเขียนนิพจน์ทั่วไปถือเป็นศิลปะมหัศจรรย์ พวกเขายึดติดกับคุณลักษณะที่พวกเขารู้จักและมีทัศนคติในการมองโลกในแง่ดีอย่างแท้จริง หากคุณยินดีที่จะนำลักษณะนิสัยห้าประการที่กล่าวถึงในบทความนี้ไปใช้ คุณจะสามารถออกแบบนิพจน์ทั่วไปที่ทนต่อการลองผิดลองถูกได้
บทความนี้จะใช้ภาษา Perl, PHP และ Python เป็นตัวอย่างโค้ด แต่คำแนะนำในบทความนี้ใช้ได้กับการใช้งานนิพจน์การแทนที่ (regex) เกือบทั้งหมด
1. ใช้ช่องว่างและความคิดเห็น
สำหรับโปรแกรมเมอร์ส่วนใหญ่ การใช้ช่องว่างและการเยื้องในสภาพแวดล้อมนิพจน์ทั่วไปไม่ใช่ปัญหา หากพวกเขาไม่ทำเช่นนี้ พวกเขาจะถูกหัวเราะเยาะจากเพื่อนร่วมงานและแม้แต่คนธรรมดาอย่างแน่นอน เกือบทุกคนรู้ดีว่าการบีบโค้ดลงในบรรทัดเดียวทำให้อ่าน เขียน และบำรุงรักษาได้ยาก นิพจน์ทั่วไปแตกต่างกันอย่างไร
เครื่องมือนิพจน์การแทนที่ส่วนใหญ่มีคุณสมบัติช่องว่างเพิ่มเติม ซึ่งช่วยให้โปรแกรมเมอร์สามารถขยายนิพจน์ทั่วไปเป็นหลายบรรทัด และเพิ่มความคิดเห็นที่ส่วนท้ายของแต่ละบรรทัด เหตุใดโปรแกรมเมอร์เพียงไม่กี่คนจึงใช้ประโยชน์จากคุณลักษณะนี้ นิพจน์ทั่วไปของ Perl 6 ใช้รูปแบบการขยายช่องว่างตามค่าเริ่มต้น อย่าปล่อยให้ภาษาขยายช่องว่างโดยค่าเริ่มต้นสำหรับคุณ จงใช้ประโยชน์จากมันด้วยตัวคุณเอง
เคล็ดลับอย่างหนึ่งที่ต้องจำเกี่ยวกับช่องว่างที่ขยายคือการบอกให้เอ็นจิ้นนิพจน์ทั่วไปละเว้นช่องว่างที่ขยาย วิธีนี้ถ้าคุณต้องการจับคู่ช่องว่าง คุณจะต้องระบุให้ชัดเจน
ในภาษา Perl ให้เพิ่ม x ต่อท้ายนิพจน์ทั่วไป ดังนั้น "m/foo bar/" จึงกลายเป็นรูปแบบต่อไปนี้:
m/
ฟู
บาร์
/x
ในภาษา PHP ให้เพิ่ม x ต่อท้ายนิพจน์ทั่วไป ดังนั้น ""/foo bar/"" จึงกลายเป็นรูปแบบต่อไปนี้:
"/
ฟู
บาร์
/x"
ในภาษา Python ให้ส่งพารามิเตอร์การปรับเปลี่ยนรูปแบบ "re.VERBOSE" เพื่อรับฟังก์ชันที่คอมไพล์ดังนี้:
pattern = r'''
ฟู
บาร์
-
regex = re.compile(pattern, re.VERBOSE)
จัดการกับนิพจน์ทั่วไปที่ซับซ้อนมากขึ้น การเว้นวรรคและความคิดเห็นจะมีความสำคัญมากขึ้น สมมติว่ามีการใช้นิพจน์ทั่วไปต่อไปนี้เพื่อจับคู่หมายเลขโทรศัพท์ในสหรัฐอเมริกา:
(?d{3})? ?d{3}[-.]d{4}
นิพจน์ทั่วไปนี้ตรงกับหมายเลขโทรศัพท์ เช่น "( 314)555-4000" คุณคิดว่านิพจน์ทั่วไปนี้ตรงกับ "314-555-4000" หรือ "555-4000" หรือไม่ คำตอบคือไม่ตรงกัน การเขียนบรรทัดโค้ดดังกล่าวจะซ่อนข้อบกพร่องและผลลัพธ์ของการออกแบบเอง จำเป็นต้องมีรหัสพื้นที่ของโทรศัพท์ แต่นิพจน์ทั่วไปไม่มีสัญลักษณ์คั่นระหว่างรหัสพื้นที่และคำนำหน้า
การแบ่งโค้ดบรรทัดนี้ออกเป็นหลายบรรทัดและเพิ่มความคิดเห็นจะเผยให้เห็นข้อบกพร่องและทำให้แก้ไขได้ง่ายขึ้น
ในภาษา Perl ควรอยู่ในรูปแบบต่อไปนี้:
/
(? # วงเล็บเสริม
d{3} # รหัสพื้นที่โทรศัพท์ที่จำเป็น
)? # วงเล็บเพิ่มเติม
[-s.]? # ตัวคั่นอาจเป็นขีดกลาง ช่องว่าง หรือจุดก็ได้
d{3} # คำนำหน้าสามหลัก
[-.] # ตัวคั่นอื่น
d{4} # หมายเลขโทรศัพท์สี่หลัก
/x
ขณะนี้ regex ที่เขียนใหม่มีตัวคั่นเพิ่มเติมหลังรหัสพื้นที่ ดังนั้นจึงควรตรงกับ "314-555-4000" อย่างไรก็ตาม ยังคงต้องใช้รหัสพื้นที่ โปรแกรมเมอร์อีกรายที่ต้องการทำให้รหัสพื้นที่ของโทรศัพท์เป็นตัวเลือกสามารถเห็นได้อย่างรวดเร็วว่าตอนนี้ไม่ใช่ทางเลือกแล้ว และการเปลี่ยนแปลงเล็กน้อยสามารถแก้ปัญหาได้
2.
การทดสอบการเขียนมีสามระดับ แต่ละระดับจะเพิ่มความน่าเชื่อถือให้กับโค้ดของคุณ ขั้นแรก คุณต้องคิดให้รอบคอบว่าโค้ดใดที่คุณต้องจับคู่ และดูว่าคุณสามารถจัดการกับความไม่ตรงกันได้หรือไม่ ประการที่สอง คุณต้องใช้อินสแตนซ์ข้อมูลเพื่อทดสอบนิพจน์ทั่วไป สุดท้ายนี้ คุณจะต้องผ่านการทดสอบอย่างเป็นทางการ
การตัดสินใจว่าจะจับคู่อะไรจริงๆ ก็คือการค้นหาสมดุลระหว่างการจับคู่ผลลัพธ์ที่ไม่ถูกต้องกับการพลาดผลลัพธ์ที่ถูกต้อง หาก regex ของคุณเข้มงวดเกินไป ก็จะพลาดการจับคู่ที่ถูกต้องบางรายการ หากหลวมเกินไป ก็จะทำให้เกิดการจับคู่ที่ไม่ถูกต้อง เมื่อนิพจน์ทั่วไปถูกเผยแพร่เป็นโค้ดจริง คุณอาจไม่เห็นทั้งสองอย่าง ลองพิจารณาตัวอย่างหมายเลขโทรศัพท์ด้านบน ซึ่งจะตรงกับ "800-555-4000 = -5355" แมตช์ที่ไม่ถูกต้องนั้นตรวจพบได้ยาก ดังนั้นการวางแผนล่วงหน้าและทดสอบให้ดีจึงเป็นสิ่งสำคัญ
ดำเนินการต่อด้วยตัวอย่างหมายเลขโทรศัพท์ หากคุณยืนยันหมายเลขโทรศัพท์ในรูปแบบเว็บ คุณอาจพอใจกับตัวเลขสิบหลักในรูปแบบใดก็ได้ อย่างไรก็ตาม หากคุณต้องการแยกหมายเลขโทรศัพท์ออกจากข้อความจำนวนมาก คุณอาจต้องยกเว้นการจับคู่ที่ผิดพลาดซึ่งไม่เป็นไปตามข้อกำหนดอย่างระมัดระวัง
เมื่อคิดถึงข้อมูลที่คุณต้องการจับคู่ ให้จดสถานการณ์จำลองบางกรณีไว้ เขียนโค้ดเพื่อทดสอบนิพจน์ทั่วไปของคุณกับสถานการณ์กรณีและปัญหา สำหรับนิพจน์ทั่วไปที่ซับซ้อน วิธีที่ดีที่สุดคือเขียนโปรแกรมขนาดเล็กเพื่อทดสอบ ซึ่งอาจอยู่ในรูปแบบเฉพาะต่อไปนี้
ในภาษา Perl:
#!/usr/bin/perl
my @tests = ( "314-555-4000",
"800-555-4400",
"(314)555-4000",
"314.555.4000",
"555-4000",
"aasdklfjklas",
"1234-123-12345"
);
ทดสอบ $test ของฉัน (@tests) {
ถ้า ( $ทดสอบ =~ ม./
(? # วงเล็บเสริม
d{3} # รหัสพื้นที่โทรศัพท์ที่จำเป็น
)? # วงเล็บเพิ่มเติม
[-s.]? # ตัวคั่นอาจเป็นขีดกลาง ช่องว่าง หรือจุดก็ได้
d{3} # คำนำหน้าสามหลัก
[-s.] # ตัวคั่นอื่น
d{4} # หมายเลขโทรศัพท์สี่หลัก
/x ) {
พิมพ์ "ตรงกับ $testn";
-
อื่น {
พิมพ์ "ล้มเหลวในการจับคู่เมื่อ $testn";
-
}
ในภาษา PHP:
<?php
$ทดสอบ = array( "314-555-4000",
"800-555-4400",
"(314)555-4000",
"314.555.4000",
"555-4000",
"aasdklfjklas",
"1234-123-12345" );
$regex = "/
(? # วงเล็บเสริม
d{3} # รหัสพื้นที่โทรศัพท์ที่จำเป็น
)? # วงเล็บเพิ่มเติม
[-s.]? # ตัวคั่นอาจเป็นขีดกลาง ช่องว่าง หรือจุดก็ได้
d{3} # คำนำหน้าสามหลัก
[-s.] # ตัวคั่นอื่น
d{4} # หมายเลขโทรศัพท์สี่หลัก
/x";
foreach ($ทดสอบเป็น $test) {
ถ้า (preg_match($regex, $test)) {
echo "ตรงกับ $test
-
-
อื่น {
echo "การจับคู่ $test ไม่สำเร็จ
-
-
-
?>;
ในภาษา Python:
import re
tests = ["314-555-4000",
"800-555-4400",
"(314)555-4000",
"314.555.4000",
"555-4000",
"aasdklfjklas",
"1234-123-12345"
]
รูปแบบ = r'''
(? # วงเล็บเสริม
d{3} # รหัสพื้นที่โทรศัพท์ที่จำเป็น
)? # วงเล็บเพิ่มเติม
[-s.]? # ตัวคั่นอาจเป็นขีดกลาง ช่องว่าง หรือจุดก็ได้
d{3} # คำนำหน้าสามหลัก
[-s.] # ตัวคั่นอื่น
d{4} # หมายเลขโทรศัพท์สี่หลัก
'''
regex = re.compile( pattern, re.VERBOSE ) สำหรับการทดสอบในการทดสอบ:
ถ้า regex.match (ทดสอบ):
พิมพ์ "Matched on", ทดสอบ, "n"
อื่น:
พิมพ์ "Failed match on", test, "n"
การรันโค้ดทดสอบจะเผยให้เห็นปัญหาอื่น: มันตรงกับ "1234-123-12345"
ตามทฤษฎีแล้ว คุณต้องรวมการทดสอบทั้งหมดสำหรับแอปพลิเคชันทั้งหมดเข้ากับทีมทดสอบ แม้ว่าคุณจะยังไม่มีกลุ่มการทดสอบ แต่การทดสอบนิพจน์ทั่วไปของคุณจะเป็นพื้นฐานที่ดีสำหรับกลุ่มทดสอบ และตอนนี้ก็เป็นเวลาที่ดีในการเริ่มต้น แม้ว่าจะไม่ใช่เวลาที่เหมาะสมในการสร้าง แต่คุณยังคงควรเรียกใช้และทดสอบนิพจน์ทั่วไปหลังการแก้ไขแต่ละครั้ง การใช้เวลาเพียงเล็กน้อยที่นี่จะช่วยคุณประหยัดปัญหาได้มาก
3. การดำเนินการสลับกลุ่ม
สัญลักษณ์การดำเนินการสลับ ( ) มีลำดับความสำคัญต่ำ ซึ่งหมายความว่ามักจะสลับมากกว่าที่โปรแกรมเมอร์ตั้งใจไว้ ตัวอย่างเช่น นิพจน์ทั่วไปเพื่อแยกที่อยู่อีเมลออกจากข้อความอาจเป็นดังนี้:
^CC: ถึง:(.*)
ความพยายามข้างต้นไม่ถูกต้อง แต่ข้อผิดพลาดนี้มักจะไม่สังเกตเห็น วัตถุประสงค์ของโค้ดด้านบนคือการค้นหาข้อความที่ขึ้นต้นด้วย "CC:" หรือ "ถึง:" จากนั้นแยกที่อยู่อีเมลที่ท้ายบรรทัดนี้
น่าเสียดาย หาก "To:" ปรากฏขึ้นกลางบรรทัด นิพจน์ทั่วไปนี้จะไม่จับบรรทัดใดๆ ที่ขึ้นต้นด้วย "CC:" และจะแยกข้อความแบบสุ่มหลายส่วนแทน พูดตามตรง นิพจน์ทั่วไปจะจับคู่บรรทัดที่ขึ้นต้นด้วย "CC:" แต่ไม่ได้จับอะไรเลย หรือจับคู่กับบรรทัดใดๆ ที่มี "ถึง:" แต่จับส่วนที่เหลือของบรรทัด โดยปกติแล้ว นิพจน์ทั่วไปนี้จะบันทึกที่อยู่อีเมลจำนวนมาก ดังนั้นจึงไม่มีใครสังเกตเห็นข้อบกพร่อง
หากคุณต้องการบรรลุจุดประสงค์ที่แท้จริง คุณควรเพิ่มวงเล็บเพื่อให้ชัดเจน นิพจน์ทั่วไปจะเป็นดังนี้:
(^CC:) (ถึง:(.*))
หากเจตนาที่แท้จริงคือจับข้อความที่ขึ้นต้นด้วย " CC:" หรือ "To:" ส่วนที่เหลือของบรรทัด ดังนั้นนิพจน์ทั่วไปที่ถูกต้องคือ:
^(CC: To:)(.*)
นี่เป็นข้อผิดพลาดในการจับคู่ที่ไม่สมบูรณ์ทั่วไปที่คุณจะหลีกเลี่ยงได้หากคุณสร้างนิสัยในการจัดกลุ่ม สำหรับการดำเนินการสลับกัน ข้อผิดพลาดนี้
4. ใช้ตัวระบุปริมาณแบบหลวม ๆ
โปรแกรมเมอร์จำนวนมากหลีกเลี่ยงการใช้ตัวระบุปริมาณแบบหลวม ๆ เช่น "*?", "+?" และ "???" แม้ว่าจะทำให้นิพจน์ง่ายต่อการเขียนและเข้าใจก็ตาม
ตัวระบุปริมาณแบบผ่อนคลายจะจับคู่ข้อความให้น้อยที่สุด ซึ่งช่วยให้การจับคู่แบบตรงทั้งหมดประสบความสำเร็จ หากคุณเขียนว่า "foo(.*?)bar" ตัวปริมาณจะหยุดจับคู่ในครั้งแรกที่พบ "bar" ไม่ใช่ครั้งสุดท้าย นี่เป็นสิ่งสำคัญหากคุณต้องการจับภาพ "###" จาก "foo###bar+++bar" ตัวระบุปริมาณที่เข้มงวดจะจับ "###bar++ +" ;) นี่จะทำให้เกิดปัญหามากมาย หากคุณใช้ตัวระบุปริมาณแบบผ่อนคลาย คุณสามารถสร้างนิพจน์ทั่วไปใหม่ได้โดยใช้เวลาเพียงเล็กน้อยในการประกอบประเภทอักขระ
ปริมาณแบบผ่อนปรนจะมีประโยชน์อย่างยิ่งเมื่อคุณทราบโครงสร้างของบริบทที่คุณต้องการจับข้อความ
5. ใช้ตัวคั่นที่มีอยู่
ภาษา Perl และ PHP มักใช้เครื่องหมายทับซ้าย (/) เพื่อทำเครื่องหมายจุดเริ่มต้นและจุดสิ้นสุดของนิพจน์ทั่วไป ภาษา Python จะใช้ชุดเครื่องหมายคำพูดเพื่อทำเครื่องหมายจุดเริ่มต้นและจุดสิ้นสุด หากคุณยืนยันที่จะใช้เครื่องหมายทับซ้ายใน Perl และ PHP คุณจะต้องหลีกเลี่ยงการเครื่องหมายทับในนิพจน์ หากคุณใช้เครื่องหมายคำพูดใน Python คุณจะต้องหลีกเลี่ยงเครื่องหมายแบ็กสแลช () การเลือกตัวคั่นหรือเครื่องหมายคำพูดที่แตกต่างกันสามารถช่วยให้คุณหลีกเลี่ยงครึ่งหนึ่งของนิพจน์ทั่วไปได้ ซึ่งจะทำให้อ่านสำนวนได้ง่ายขึ้นและลดข้อบกพร่องที่อาจเกิดขึ้นจากการลืมหลีกเลี่ยงสัญลักษณ์
ภาษา Perl และ PHP อนุญาตให้ใช้อักขระที่ไม่ใช่ตัวเลขและช่องว่างเป็นตัวคั่นได้ หากคุณเปลี่ยนมาใช้ตัวคั่นใหม่ คุณสามารถหลีกเลี่ยงการพลาดเครื่องหมายทับซ้ายเมื่อจับคู่ URL หรือแท็ก HTML (เช่น "http://" หรือ "<br/>;")
ตัวอย่างเช่น "/http://(S)*/" สามารถเขียนเป็น "#http://(S)*#" ได้
ตัวคั่นทั่วไปคือ "#", "!" และ " " หากคุณใช้วงเล็บเหลี่ยม วงเล็บเหลี่ยม หรือวงเล็บปีกกา ให้รักษาให้ตรงกัน ต่อไปนี้เป็นตัวอย่างของตัวคั่นทั่วไป:
#…# !…! {…} s … … (Perl เท่านั้น) s[…][…] (Perl เท่านั้น) s<…>;/…/ (Perl เท่านั้น)
ใน Python นิพจน์ทั่วไปจะถือเป็นสตริงก่อน หากคุณใช้เครื่องหมายคำพูดเป็นตัวคั่น คุณจะพลาดแบ็กสแลชทั้งหมด แต่คุณสามารถหลีกเลี่ยงปัญหานี้ได้โดยใช้สตริง "r'' หากคุณใช้เครื่องหมายคำพูดเดี่ยวสามเครื่องหมายติดกันสำหรับตัวเลือก "re.VERBOSE" จะทำให้คุณสามารถขึ้นบรรทัดใหม่ได้ ตัวอย่างเช่น regex = "( file://w+)(//d +)" สามารถเขียนได้ในรูปแบบต่อไปนี้:
regex = r''''
(w+)
(d+)
-