ข้ามไปเนื้อหา

คู่มือการใช้ไพวิกิพีเดีย/นิพจน์ปรกติในไพวิกิพีเดีย

จาก วิกิตำรา
« คู่มือการใช้ไพวิกิพีเดีย
นิพจน์ปรกติในไพวิกิพีเดีย
»
ติดต่อชุมชนผู้พัฒนา เรียนรู้เพิ่มเติม


ในไพวิกิพีเดีย อาจจะมีการใช้นิพจน์ปรกติในสองโอกาส คือในการใช้งานสคริปต์ replace.py หรือไม่ก็ใช้ในการเขียนสคริปต์ เนื่องจากไพวิกิพีเดียเขียนขึ้นด้วยภาษาไพทอน นิพจน์ปรกติของไพวิกิพีเดียจึงอาศัยนิพจน์ปรกติของภาษาไพทอนเป็นฐาน ส่งผลให้วากยสัมพันธ์เหมือนกันทุกประการ

นิพจน์ปรกติของภาษาไพทอนรองรับความสามารถของนิพจน์ปรกติของภาษาเพิร์ล ซึ่งมีความสามารถเพิ่มเข้ามามากมายเทียบกับมาตรฐานนิพจน์ปรกติทั่วไป

เพื่อแยกแยะระหว่างนิพจน์ปรกติทั่วไปกับข้อความ จะขอเขียนนิพจน์ปรกติคร่อมด้วยเครื่องหมาย /.../ แทนที่จะคร่อมด้วย "..." ตามปกติ อย่างไรก็ตาม ขอให้เข้าใจว่าในภาษาไพทอนนิพจน์ปรกติคร่อมด้วย "..." เหมือนข้อความทั่วไป

ต่อจากนี้จะแสดงถึงความสามารถหรือวากยสัมพันธ์เฉพาะของนิพจน์ปรกติบนไพวิกิพีเดีย/ภาษาไพทอนพอเป็นสังเขปเท่านั้น โดยจะไม่ลงลึกถึงการใช้งานนิพจน์ปรกติ และอาจมีตัวอย่างประกอบเพื่อให้เข้าใจได้ง่าย

อักขระควบคุม

[แก้ไข | แก้ไขต้นฉบับ]

อักขระควบคุม (metacharacters) เป็นเครื่องหมายพิเศษที่ใช้ในการควบคุมรูปแบบของข้อความ ได้แก่

* + ? { } [ ] \ | ( )

ตารางด้านล่างนี้จะอธิบายเฉพาะอักขระที่มีความกำกวมในการใช้อย่างย่อเท่านั้น

อักขระ คำอธิบาย
* + ความหมายเหมือนมาตรฐานนิพจน์ปรกติ โดยจะมีการจับแบบโลภ
( ) ใช้ในการทำ group โปรดสังเกตว่าไม่ได้ใช้ \( \) เหมือน Basic Regular Expression แต่จะเหมือน Extended Regular Expression แทน

group ในลักษณะนี้เป็น capturing group ซึ่งสามารถใช้ร่วมกับการอ้างย้อนหลัง ได้ด้วย

{ } ใช้ในการระบุจำนวนในการทำซ้ำ โปรดสังเกตว่าไม่ได้ใช้ \{ \} เหมือน Basic Regular Expression แต่จะเหมือน Extended Regular Expression แทน

การหนีอักขระด้วยเครื่องหมายทับหลัง

[แก้ไข | แก้ไขต้นฉบับ]

อักขระสัญลักษณ์โดยส่วนใหญ่จะเป็นอักขระควบคุมด้วย จึงไม่สามารถใส่อักขระสัญลักษณ์โดด ๆ เพื่อให้นิพจน์ปรกติจับได้ ทางออกที่จะให้นิพจน์ปรกติจับกับตัวสัญลักษณ์เหล่านั้น คือการหนีอักขระดังกล่าว (escape character) โดยให้ใส่เครื่องหมายทับหลัง (\) เข้าไปก่อนหน้าสัญลักษณ์ต่าง ๆ ลักษณะเดียวกันกับ \n \r \0

เช่นต้องการจะจับกับข้อความ "a+" ในข้อความ "qwea+ewqaaaqwe" หากใช้นิพจน์ปรกติ /a+/ จะเป็นการจับกับ "a" และ "aaa" ใน "qwea+ewqaaaqwe" แทน เพราะ + เป็นอักขระควบคุมในการจับซ้ำ แต่ถ้าใช้นิพจน์ปรกติ /a\+/ ก็จะจับได้ "a+" ใน "qwea+ewqaaaqwe" ตามที่ต้องการ

เครื่องหมายทับหลัง (\) ควรใส่เข้าไปก่อนหน้าเครื่องหมายทุกอย่างที่จะจับ (ควรใช้กับทุกอักขระที่ไม่ใช่ alphanumeric characters หรือก็คือ อักขระทุกตัวที่ไม่ใช่ตัวอักษร ตัวเลข และเครื่องหมายขีดล่าง ('_')) ถึงแม้ว่าสัญลักษณ์ดังกล่าวจะไม่ใช่อักขระควบคุมก็ตาม เพื่อให้ไม่เกิดความกำกวม อย่างไรก็ตามในทางปฏิบัติ หากการเขียนอักขระนั้นโดด ๆ ไม่ทำให้เกิดความหมายพิเศษ ก็ไม่จำเป็นจะต้องหนีอักขระดังกล่าวเสมอไป เช่น เครื่องหมาย # @ ! < >

ข้อควรระวัง

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

ข้อแนะนำ

วิธีหลีกเลี่ยงการที่จะต้องหนีอักขระถึงสองรอบก็คือการเลือกใช้ข้อความดั้งเดิม (raw string) แทนที่ข้อความธรรมดา โดยใช้ r"..." แทนที่ "..." จากตัวอย่างข้างต้น หากต้องการจะจับกับอักขระ "\" ก็สามารถเขียนในภาษาไพทอนได้ว่า r"\\"

ชุดอักขระ (character set) คือกลุ่มของอักขระที่นิยามมาให้แล้ว ตารางด้านล่างนี้แสดงชุดอักขระทั้งหมด

รหัส ความหมาย เทียบเท่า
. อักขระทุกตัวยกเว้น \n แต่หากอยู่ในโหมด dotall จะทำให้รวม \n ด้วย
\d อักขระที่เป็นตัวเลข /[0-9]/
\D อักขระที่ไม่ใช่อักขระตัวเลข /[^0-9]/
\s อักขระที่เป็น whitespace /[ \t\n\r\f\v]/
\S อักขระที่ไม่เป็น whitespace /[^ \t\n\r\f\v]/
\w อักขระที่เป็นตัวอักษร ตัวเลข และ เครื่องหมายขีดล่าง ('_') /[a-zA-Z0-9_]/
\W อักขระที่ไม่เป็นตัวอักษร ตัวเลข และ เครื่องหมายขีดล่าง ('_') /[^a-zA-Z0-9_]/

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

รหัส ความหมาย
^ จับกับต้นข้อความ หากอยู่ในโหมด multiline จะทำให้จับต้นบรรทัดด้วย
$ จับกับท้ายข้อความ หากอยู่ในโหมด multiline จะทำให้จับท้ายบรรทัดด้วย
\A ในกรณีโหมดทั่วไปจะเหมือนกับ ^ คือจับกับต้นข้อความ แต่เมื่ออยู่ในโหมด multiline จะได้ว่า ^ จับกับต้นบรรทัดทั้งหลายด้วย แต่ \A จะยังคงจับกับต้นข้อความเหมือนเดิม
\Z ในกรณีโหมดทั่วไปจะเหมือนกับ $ คือจับกับท้ายข้อความ แต่เมื่ออยู่ในโหมด multiline จะได้ว่า $ จับกับท้ายบรรทัดทั้งหลายด้วย แต่ \Z จะยังคงจับกับท้ายข้อความเหมือนเดิม
\b เรียกว่า word boundary เป็นการจับในตำแหน่งระหว่าง \w กับ \W
\B จับในตำแหน่งที่อยู่ระหว่าง \w กับ \w หรือ \W กับ \W
ข้อควรระวัง

หากต้องการจะจับกับอักขระสัญลักษณ์ต่าง ๆ ที่เป็นส่วนประกอบของชุดอักขระก็ต้องทำการหนีอักขระด้วยเช่นเดียวกัน นั่นก็คือถ้าจะจับกับ "." "\" "^" "$" ก็ต้องเขียนเป็น "\." "\\" "\^" "\$" แทน

โลภกับไม่โลภ

[แก้ไข | แก้ไขต้นฉบับ]

การจับข้อความซ้ำโดยเครื่องหมาย * หรือ + จะพยายามจับให้ไกลที่สุดเท่าที่เป็นไปได้ไปก่อน

หากไม่ต้องการให้มีการจับแบบโลภ กล่าวคือพยายามหยุดการจับซ้ำทันที่ถ้าเป็นไปได้ ให้ใส่เครื่องหมาย ? เข้าไปด้านหลังเครื่องหมาย * หรือ + เพื่อให้นิพจน์ปรกติพยายามที่จะหยุดการจับซ้ำทันทีที่สามารถจับอักขระถัดไปได้

ตัวอย่างเช่น สำหรับข้อความ "abcdbcdbc" ตัวอย่างการจับซ้ำแบบโลภ คือใช้นิพจน์ปรกติ /b.*d/ ซึ่งจะได้ "bcdbcd" จาก "abcdbcdbc"

หากต้องการจะจับให้ได้ "bcd" จาก "abcdbcdbc" แทนที่ "abcdbcdbc" ก็ต้องทำการจับแบบไม่โลภ นั่นคือใช้นิพจน์ปรกติ /b.*?d/ แทน

โหมด (mode, flag) เป็นลักษณะของการประมวลผลรูปแบบที่แตกต่างกันออกไป มีทั้งหมดดังนี้

โหมด พารามิเตอร์ใน replace.py ชื่อ flag inline flag คำอธิบาย
dotall -dotall re.DOTALL s จะทำให้จุด . จับกับอักขระทุก ๆ ตัวรวมถึง \n ด้วย
ignorecase -nocase re.IGNORECASE i จะไม่พิจารณาความแตกต่างระหว่างตัวอักษรพิมพ์เล็กกับพิมพ์ใหญ่ เช่น "aBcDe" = "AbCdE"
multiline ไม่มี re.MULTILINE m อักขระควบคุม ^ และ $ จะจับกับต้น/ท้ายบรรทัดด้วย
verbose ไม่มี re.VERBOSE x ทำให้อักขระวรรค (' ') ไม่มีผลในการจับอักขระวรรค (ต้องใช้ \ อย่างเดียวเท่านั้น) และอักขระ '#' กลายเป็นการคอมเมนต์แทน (ยกเว้น '#' ที่อยู่ใน [...] ) การทำเช่นนี้ทำให้สามารถอ่านรูปแบบนิพจน์ปกติได้ง่ายยิ่งขึ้น
unicode ไม่มี re.UNICODE u ทำให้ \w \b \s \d จับกับอักขระยูนิโค้ดอื่น ๆ ด้วย
locale ไม่มี re.LOCALE L เกี่ยวกับการตั้ง locale ของเครื่องด้วย จึงไม่อธิบายและไม่แนะนำให้ใช้

ในการใช้งาน replace.py อาจมีบางโหมดที่สามารถกำหนดได้จากพารามิเตอร์ เช่น โหมด dotall และ ignorecase (nocase) และโหมดทั้งหลายสามารถเรียกใช้พร้อมกันได้ด้วย เช่น python replace.py -regex -dotall -nocase "Abc.*" "def" -page:xxx หากหน้า xxx มีข้อความ

qweqweqweqweABCqwe
qewqweqw
e

ก็จะถูกแทนที่เป็น

qweqweqweqwedef

แต่บางโหมดเช่น multiline ไม่มีพารามิเตอร์ให้ใช้ในการเปิดโหมด ก็อาจใช้ inline flag ในการกำหนด โดยใส่ (?flag1flag2flag3...flagN) ไว้ด้านหน้าสุดของข้อความ โดยที่ flag1 flag2 ... flagN เป็นค่า inline flag ที่ปรากฎในตารางด้านบน เช่น หากต้องการกำหนด multiline กับ ignorecase พร้อมกัน ก็อาจใช้ python replace.py -regex "(?mi)^ *Q" "z" -page:xxx หากหน้า xxx มีข้อความ

   QweqweqweqweABCqwe
            qewqweqw
   e
d

ก็จะถูกแทนที่เป็น

zweqweqweqweABCqwe
zewqweqw
   e
d

สำหรับ การเขียนโปรแกรมภาษาไพทอน สามารถกำหนดโหมดได้ผ่านพารามิเตอร์เพิ่มเติม หากต้องการระบุหลาย ๆ โหมดพร้อมกัน ให้ใช้ vertical bar (|) คั่น เช่น

re.findall("aBc.*", mystr, flags = re.DOTALL | re.IGNORECASE)

สำหรับการแทนที่ข้อความ ก็มีลักษณะแบบเดียวกัน

re.sub("aBc.*", "123", mystr, flags = re.DOTALL | re.IGNORECASE)

หรือไม่เช่นนั้นก็อาจใช้ inline flag คล้ายกับที่เคยกล่าวมาแล้วก็ได้

re.findall("(?si)aBc.*", mystr)

การอ้างย้อนหลัง

[แก้ไข | แก้ไขต้นฉบับ]

การอ้างย้อนหลัง (backreference) คือความสามารถในการจับข้อความจากข้อความค้นหา มาใส่อีกครั้งในข้อความค้นหาด้วยกันเอง หรือจะจับมาใส่ในข้อความแทนที่ก็ได้ โดยกำหนดข้อความที่จะจับจาก capturing group ((...)) ในข้อความค้นหา และใช้รหัสอ้างย้อนหลัง (\1 \2 \3 ...) ในการอ้างย้อนหลัง ซึ่งรหัสอ้างย้อนหลังนี้จะปรากฎใน ข้อความค้นหา หรือ ข้อความแทนที่ ก็ได้

สำหรับข้อความที่ต้องการจะจัดกลุ่ม แต่ไม่ต้องการจับให้มาอ้างในภายหลัง สามารถใช้ non-capturing group ((?:...) ) ได้

ข้อควรระวัง

สำหรับผู้เขียนภาษาไพทอน ตามที่ได้กล่าวมาแล้วว่าข้อความปกติต้องมีการหนีตัวอักษรถ้ามีสัญลักษณ์ โปรดตระหนักว่า \number เมื่อ number เป็นเลขฐาน 8 จะกลายเป็นอักขระในเลขฐาน 8 แทน ดังนั้นถ้าจะใช้การอ้างย้อนหลัง ก็อย่าลืมที่จะหนีอักขระ หรือไม่ก็หันไปใช้ข้อความดั้งเดิมแทน

ตัวอย่างการใช้การอ้างย้อนหลังกับรูปแบบด้วยกันเอง เช่น ต้องการจะจับข้อความที่มีวรรณยุกต์ตัวเดียวกันซ้อน ได้แก่ "่่" "้้" "๊๊" "๋๋" ก็อาจเขียนเป็นนิพจน์ปรกติได้ว่า /([่้๊๋])\1/ ซึ่งจะทำให้ในข้อความ "เทวะ, อันข้้านี้ไซร้ มานี่้อย่่างไร บทราบสำนึกสักนิด" มีการจับ "้้" และ "่่" จาก "เทวะ, อันข้้านี้ไซร้ มานี่้อย่่างไร บทราบสำนึกสักนิด" สังเกตว่า "่้" จาก "มานี่้" จะไม่ถูกจับเนื่องจาก "่" กับ "้" เป็นคนละอักขระกัน แต่หากใช้นิพจน์ปรกติ /[่้๊๋]{2}/ ก็จะจับกรณีนี้ด้วย

ตัวอย่างการใช้การอ้างย้อนหลังกับข้อความแทนที่ เช่น หากต้องการจะแทนที่ {{เรตติง-a|b}} → {{เรตติง|b|a}} (ตัวอย่างคือ {{เรตติง-5|3}} → {{เรตติง|3|5}}) ก็อาจกำหนดข้อความค้นหาเป็น /\{\{เรตติง-(.*?)|(.*?)\}\}/ และข้อความแทนที่เป็น "{{เรตติง|\2|\1}}"

การยืนยันด้วยอักขระความยาวศูนย์

[แก้ไข | แก้ไขต้นฉบับ]

การยืนยันด้วยอักขระความยาวศูนย์ (zero-width assertion) หรือที่รู้จักกันในชื่อ lookaround เป็นรูปแบบซึ่งนิพจน์ปรกติจะพยายามจับข้อความนั้น แต่ไม่นับข้อความนั้นเป็นข้อความที่ค้นเจอ ประกอบไปด้วย

ประเภท รูปแบบสัญลักษณ์
positive lookahead (?=)
negative lookahead (?!)
positive lookbehind (?<=)
negative lookbehind (?<!)

ตัวอย่างเช่น สมมุติมีข้อความ

<b>ข้อความตัวหนา</b> สามารถกระทำได้โดยเขียน <nowiki><b>ข้อความตัวหนา</b></nowiki>

หากต้องการจะจับ <b> หรือ </b> โดยที่ไม่ยุ่งกับข้อความที่อยู่ใน <nowiki>...</nowiki> อาจมองปัญหาใหม่เป็นการจับ <b> หรือ </b> โดยที่จะต้องไม่มี </nowiki> เป็นข้อความอยู่ด้านหลัง ยกเว้นกรณีที่เจอ <nowiki> ก่อน (หากเจอ <nowiki> ก่อนแสดงว่าจริง ๆ แล้วยังไม่ได้เริ่มแท็ก)

จากปัญหาข้างต้น สามารถเขียนนิพจน์ปรกติออกมาได้ว่า </?b>(?!(?:(?!</?nowiki>).)*</nowiki>)

ลำดับที่ นิพจน์ปรกติ คำอธิบาย
1 </?b> จับกับ <b> หรือ </b>
2 (?! เริ่ม negative lookahead
3 (?: เริ่ม non-capturing group
4 (?!</?nowiki>) negative lookahead ให้ ไม่จับกับ <nowiki> หรือ </nowiki>
5 . นอกจากนั้นให้จับ
6 )* จบ non-capturing group และให้จำ group นี้ซ้ำไปเรื่อย ๆ
3 - 6 กล่าวคือให้จับกับอะไรก็ได้ไปเรื่อย ๆ แต่เมื่อเจอ <nowiki> หรือ </nowiki> แล้วให้หยุด
7 </nowiki>) จับกับ </nowiki> และจบ negative lookahead
1 - 7 กล่าวคือให้เริ่มจับ <b> หรือ </b> แล้วทดสอบดูว่าข้างหน้ามี </nowiki> หรือไม่ ถ้ามีให้การจับล้มเหลว แต่ถ้าเจอ <nowiki> ก่อนให้การทดสอบนี้ล้มเหลวแทน นั่นคือสามารถจับได้