นิพจน์ทั่วไปเป็นภาษาที่มีประสิทธิภาพสำหรับการจับคู่รูปแบบข้อความ หน้านี้ให้ข้อมูลเบื้องต้นเกี่ยวกับนิพจน์ทั่วไปที่เพียงพอสำหรับแบบฝึกหัด Python ของเรา และแสดงวิธีการทำงานของนิพจน์ทั่วไปใน Python โมดูล Python "re" รองรับนิพจน์ทั่วไป
ใน Python โดยทั่วไปการค้นหานิพจน์ทั่วไปจะเขียนดังนี้
match = re.search(pat, str)
เมธอด re.search() จะใช้รูปแบบนิพจน์ทั่วไปและสตริง และค้นหารูปแบบนั้นภายในสตริง หากการค้นหาสำเร็จ search() จะแสดงผลออบเจ็กต์ที่ตรงกันหรือ "ไม่มี" หากไม่เป็นเช่นนั้น ดังนั้น การค้นหามักจะตามหลังด้วยคำสั่ง if- เพื่อทดสอบว่าการค้นหานั้นสำเร็จหรือไม่ ดังที่ปรากฏในตัวอย่างต่อไปนี้ ซึ่งค้นหาด้วยรูปแบบ 'คำ:' ตามด้วยคำ 3 ตัวอักษร (รายละเอียดด้านล่าง):
import re str = 'an example word:cat!!' match = re.search(r'word:\w\w\w', str) # If-statement after search() tests if it succeeded if match: print('found', match.group()) ## 'found word:cat' else: print('did not find')
โค้ด match = re.search(pat, str)
จัดเก็บผลการค้นหาในตัวแปรที่ชื่อ "match" จากนั้นคำสั่ง if จะทดสอบการจับคู่ หากค่าเป็น "จริง" จะสำเร็จการค้นหา และ match.group() คือข้อความที่ตรงกัน (เช่น "word:cat") มิเช่นนั้น หากจับคู่เป็น "เท็จ" (ไม่มีข้อความที่จะเฉพาะเจาะจงมากกว่า) หมายความว่าการค้นหาจะไม่สำเร็จและไม่มีข้อความที่ตรงกัน
เครื่องหมาย "r" ที่จุดเริ่มต้นของสตริงรูปแบบจะกำหนดสตริง "raw" ของ Python ซึ่งส่งผ่านแบ็กสแลชไปโดยไม่มีการเปลี่ยนแปลง ซึ่งมีประโยชน์มากสำหรับนิพจน์ทั่วไป (Java จำเป็นต้องใช้ฟีเจอร์นี้เป็นอย่างยิ่ง) เราขอแนะนำให้คุณเขียนสตริงรูปแบบที่มี 'r' เป็นนิสัยเสมอ
ลวดลายพื้นฐาน
ความสามารถของนิพจน์ทั่วไปคือความสามารถในการระบุรูปแบบ ไม่ใช่เฉพาะอักขระคงที่ ต่อไปนี้เป็นรูปแบบพื้นฐานที่สุดที่ตรงกับอักขระเดี่ยว
- a, X, 9, < -- อักขระธรรมดาจะจับคู่กับตัวเองทั้งหมดเท่านั้น อักขระเมตาที่ไม่ตรงกับตัวเองเนื่องจากมีความหมายพิเศษ ได้แก่: ^ $ * + ? { [ ] \ | ( ) (โปรดดูรายละเอียดด้านล่าง)
- (จุด) -- จับคู่กับอักขระเดี่ยวใดๆ ยกเว้นบรรทัดใหม่ '\n'
- \w -- (ตัวพิมพ์เล็ก w) จะจับคู่กับอักขระ "คำ" นั่นคือ ตัวอักษรหรือตัวเลขหรือใต้แถบ [a-zA-Z0-9_] โปรดทราบว่าแม้ว่า "คำ" จะเป็นตัวช่วยฝึกความจำสำหรับข้อความนี้ แต่จะจับคู่กับอักขระที่เป็นคำเพียงตัวเดียว ไม่ใช่ทั้งคำ \W (W ตัวพิมพ์ใหญ่) จะจับคู่อักขระที่ไม่ใช่คำ
- \b -- ขอบเขตระหว่างคำและไม่ใช่คำ
- \s -- (ตัวพิมพ์เล็ก) จับคู่กับอักขระช่องว่าง 1 ตัว ได้แก่ เว้นวรรค, ขึ้นบรรทัดใหม่, ย้อนกลับ, แท็บ, แบบฟอร์ม [ \n\r\t\f] \S (ตัวพิมพ์ใหญ่ S) จะจับคู่อักขระที่ไม่ใช่ช่องว่าง
- \t, \n, \r -- แท็บ, บรรทัดใหม่, ย้อนกลับ
- \d -- เลขทศนิยม [0-9] (ยูทิลิตีนิพจน์ทั่วไปรุ่นเก่าบางส่วนไม่สนับสนุน \d แต่ทั้งหมดสนับสนุน \w และ \s)
- ^ = start, $ = end -- จับคู่จุดเริ่มต้นหรือจุดสิ้นสุดของสตริง
- \ -- ยับยั้ง "ความพิเศษ" ของอักขระ ตัวอย่างเช่น ใช้ \. เพื่อจับคู่กับเครื่องหมายจุด หรือ \\ เพื่อจับคู่กับเครื่องหมายทับ หากไม่แน่ใจว่าอักขระนั้นมีความหมายพิเศษหรือไม่ เช่น "@" คุณอาจลองใส่เครื่องหมายทับไว้หน้าอักขระนั้น เช่น \@ หากอักขระดังกล่าวไม่ใช่ลำดับหลีกที่ถูกต้อง เช่น \c โปรแกรม Python จะหยุดการทำงานโดยมีข้อผิดพลาด
ตัวอย่างเบื้องต้น
มุกตลก: ไหนเรียกว่าหมูที่มีตา 3 ตา ปิ๊ง!
กฎพื้นฐานของการค้นหานิพจน์ทั่วไปสำหรับรูปแบบภายในสตริงมีดังนี้
- การค้นหาจะดำเนินการผ่านสตริงตั้งแต่ต้นจนจบ โดยหยุดที่รายการแรกที่พบ
- รูปแบบทั้งหมดต้องตรงกัน แต่ไม่ใช่สตริงทั้งหมด
- หาก
match = re.search(pat, str)
สําเร็จ การจับคู่จะไม่ใช่ "ไม่มี" และหาก match.group() จะเป็นข้อความที่ตรงกัน
## Search for pattern 'iii' in string 'piiig'. ## All of the pattern must match, but it may appear anywhere. ## On success, match.group() is matched text. match = re.search(r'iii', 'piiig') # found, match.group() == "iii" match = re.search(r'igs', 'piiig') # not found, match == None ## . = any char but \n match = re.search(r'..g', 'piiig') # found, match.group() == "iig" ## \d = digit char, \w = word char match = re.search(r'\d\d\d', 'p123g') # found, match.group() == "123" match = re.search(r'\w\w\w', '@@abcd!!') # found, match.group() == "abc"
การกล่าวซ้ำๆ
สิ่งต่างๆ จะน่าสนใจมากขึ้นเมื่อคุณใช้ + และ * เพื่อระบุการทำซ้ำในรูปแบบ
- + -- มีการเกิดซ้ำของรูปแบบอย่างน้อย 1 รายการที่ด้านซ้าย เช่น "i+" = ฉันเท่ากับ
- * -- พบรูปแบบอย่างน้อย 0 รายการทางซ้าย
- ? -- จับคู่รูปแบบ 0 หรือ 1 รายการทางด้านซ้าย
ด้านซ้ายสุดและใหญ่ที่สุด
ขั้นแรก การค้นหาจะค้นหาการจับคู่ด้านซ้ายสุดสำหรับรูปแบบ และข้อที่สองคือพยายามใช้สตริงให้มากที่สุด เช่น + และ * ให้ไปไกลที่สุดเท่าที่จะทำได้ (เครื่องหมาย + และ * นั้น "ละโมบ")
ตัวอย่างการกล่าวซ้ำๆ
## i+ = one or more i's, as many as possible. match = re.search(r'pi+', 'piiig') # found, match.group() == "piii" ## Finds the first/leftmost solution, and within it drives the + ## as far as possible (aka 'leftmost and largest'). ## In this example, note that it does not get to the second set of i's. match = re.search(r'i+', 'piigiiii') # found, match.group() == "ii" ## \s* = zero or more whitespace chars ## Here look for 3 digits, possibly separated by whitespace. match = re.search(r'\d\s*\d\s*\d', 'xx1 2 3xx') # found, match.group() == "1 2 3" match = re.search(r'\d\s*\d\s*\d', 'xx12 3xx') # found, match.group() == "12 3" match = re.search(r'\d\s*\d\s*\d', 'xx123xx') # found, match.group() == "123" ## ^ = matches the start of string, so this fails: match = re.search(r'^b\w+', 'foobar') # not found, match == None ## but without the ^ it succeeds: match = re.search(r'b\w+', 'foobar') # found, match.group() == "bar"
ตัวอย่างอีเมล
สมมติว่าคุณต้องการหาที่อยู่อีเมลในสตริง "xyz alice-b@google.com monkey สีม่วง" เราจะใช้ตัวอย่างนี้เป็นตัวอย่างที่ใช้งานอยู่เพื่อสาธิตฟีเจอร์นิพจน์ทั่วไปเพิ่มเติม ต่อไปนี้เป็นความพยายามโดยใช้รูปแบบ r'\w+@\w+':
str = 'purple alice-b@google.com monkey dishwasher' match = re.search(r'\w+@\w+', str) if match: print(match.group()) ## 'b@google'
การค้นหาไม่ได้รับที่อยู่อีเมลทั้งหมดในกรณีนี้ เนื่องจาก \w ไม่ตรงกับ "-" หรือ "." ในที่อยู่ เราจะแก้ไขปัญหานี้โดยใช้ฟีเจอร์นิพจน์ทั่วไปด้านล่าง
วงเล็บเหลี่ยม
สามารถใช้วงเล็บเหลี่ยมเพื่อระบุชุดอักขระได้ ดังนั้น [abc] จะจับคู่กับ "a" หรือ "b" หรือ "c" รหัส \w, \s จะทำงานอยู่ในวงเล็บเหลี่ยมเช่นกัน โดยมีข้อยกเว้นหนึ่งข้อคือ จุด (.) หมายถึง จุดตามตัวอักษร สำหรับปัญหาอีเมล เครื่องหมายวงเล็บเหลี่ยมเป็นวิธีง่ายๆ ในการเพิ่ม '.' และ '-' ลงในชุดอักขระซึ่งอาจปรากฏรอบๆ เครื่องหมาย @ โดยมีรูปแบบ r'[\w.-]+@[\w.-]+' เพื่อรับที่อยู่อีเมลทั้งหมด:
match = re.search(r'[\w.-]+@[\w.-]+', str) if match: print(match.group()) ## 'alice-b@google.com'(ฟีเจอร์วงเล็บเหลี่ยมเพิ่มเติม) คุณยังใช้เครื่องหมายขีดกลางเพื่อระบุช่วงได้อีกด้วย เพื่อให้ [a-z] จับคู่กับตัวอักษรพิมพ์เล็กทั้งหมด หากต้องการใช้เครื่องหมายขีดคั่นโดยไม่ระบุช่วง ให้ใส่เครื่องหมายขีดคั่นไว้ท้ายสุด เช่น [abc-] เครื่องหมายขึ้น (^) ที่ส่วนต้นของวงเล็บเหลี่ยมจะกลับด้าน ดังนั้น [^ab] หมายถึงอักขระใดก็ได้ยกเว้น "a" หรือ "b"
การดึงข้อมูลกลุ่ม
ฟีเจอร์ "กลุ่ม" ของนิพจน์ทั่วไปช่วยให้คุณเลือกส่วนของข้อความที่ตรงกันได้ สมมติว่าเกิดปัญหาเกี่ยวกับอีเมลที่เราต้องการดึงชื่อผู้ใช้และโฮสต์แยกกัน ในการดำเนินการนี้ ให้เพิ่มวงเล็บ ( ) รอบชื่อผู้ใช้และโฮสต์ในรูปแบบ เช่นนี้: r'([\w.-]+)@([\w.-]+)' ในกรณีนี้ วงเล็บจะไม่เปลี่ยนการจับคู่รูปแบบ แต่จะเป็นการสร้าง "กลุ่ม" เชิงตรรกะภายในข้อความการจับคู่ ในการค้นหาที่ประสบความสำเร็จ match.group(1) คือข้อความการจับคู่ที่ตรงกับวงเล็บซ้ายที่ 1 และ match.group(2) คือข้อความที่ตรงกับวงเล็บซ้ายที่ 2 ข้อความธรรมดา match.group() จะยังคงเป็นข้อความที่ตรงกันทั้งหมดตามปกติ
str = 'purple alice-b@google.com monkey dishwasher' match = re.search(r'([\w.-]+)@([\w.-]+)', str) if match: print(match.group()) ## 'alice-b@google.com' (the whole match) print(match.group(1)) ## 'alice-b' (the username, group 1) print(match.group(2)) ## 'google.com' (the host, group 2)
เวิร์กโฟลว์ทั่วไปที่มีนิพจน์ทั่วไปคือ คุณสามารถเขียนรูปแบบของสิ่งที่คุณต้องการ เพิ่มกลุ่มวงเล็บเพื่อดึงส่วนที่คุณต้องการ
ค้นหาทั้งหมด
findall() อาจเป็นฟังก์ชันเดียวที่มีประสิทธิภาพมากที่สุดในโมดูล re ด้านบนนี้ เราใช้ re.search() เพื่อหารายการที่ตรงกันแรกสำหรับรูปแบบ findall() จะค้นหารายการที่ตรงกัน *ทั้งหมด* และส่งกลับมาเป็นรายการสตริง โดยแต่ละสตริงจะแสดงรายการที่ตรงกันเพียง 1 รายการ## Suppose we have a text with many email addresses str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher' ## Here re.findall() returns a list of all the found email strings emails = re.findall(r'[\w\.-]+@[\w\.-]+', str) ## ['alice@google.com', 'bob@abc.com'] for email in emails: # do something with each found email string print(email)
ค้นหาด้วย Files
สำหรับไฟล์ คุณอาจต้องเขียนวนซ้ำเพื่อทำซ้ำบนบรรทัดของไฟล์ และคุณสามารถเรียก findall() ในแต่ละบรรทัดได้ ให้ findall() ดำเนินการซ้ำให้คุณแทน ซึ่งจะดีกว่ามาก! เพียงป้อนทั้งข้อความลงใน findall() แล้วปล่อยให้ระบบส่งกลับรายการของรายการที่ตรงกันทั้งหมดในขั้นตอนเดียว (จำได้ว่า f.read() จะแสดงผลข้อความทั้งหมดในสตริงเดียว) ดังนี้
# Open file f = open('test.txt', encoding='utf-8') # Feed the file text into findall(); it returns a list of all the found strings strings = re.findall(r'some pattern', f.read())
Findall และ Groups
กลไกกลุ่มของวงเล็บ ( ) สามารถใช้ร่วมกับ findall() ได้ หากรูปแบบมีกลุ่มวงเล็บตั้งแต่ 2 กลุ่มขึ้นไป แทนการส่งคืนรายการสตริง findall() จะส่งกลับรายการ *tuples* แต่ละ Tuple แสดงการจับคู่รูปแบบ 1 รายการ และภายใน Tuple คือข้อมูลกลุ่ม(1), กลุ่ม(2) .. ดังนั้น ถ้ามีการเพิ่มกลุ่มวงเล็บ 2 กลุ่มในรูปแบบอีเมล findall() จะแสดงผลรายการ tuples โดยแต่ละความยาวเป็น 2 จะมีชื่อผู้ใช้และโฮสต์ เช่น ('alice', 'google.com')
str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher' tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', str) print(tuples) ## [('alice', 'google.com'), ('bob', 'abc.com')] for tuple in tuples: print(tuple[0]) ## username print(tuple[1]) ## host
เมื่อคุณมีรายการของ Tuple แล้ว คุณสามารถวนซ้ำเพื่อคำนวณ Tuple แต่ละรายการได้ หากรูปแบบไม่มีวงเล็บ findall() จะแสดงผลรายการสตริงที่พบเหมือนในตัวอย่างก่อนหน้านี้ หากรูปแบบมีวงเล็บชุดเดียว findall() จะแสดงผลรายการสตริงที่เกี่ยวข้องกับกลุ่มเดี่ยวนั้น (คุณลักษณะตัวเลือก คลุมเครือ: บางครั้งคุณมีการจัดกลุ่มวงเล็บ ( ) ในรูปแบบ แต่คุณไม่ต้องการดึงข้อมูล ในกรณีดังกล่าว ให้เขียนเครื่องหมาย ?: นำหน้า เช่น (?: ) และวงเล็บเปิดนั้นจะไม่นับเป็นผลลัพธ์กลุ่ม)
เวิร์กโฟลว์ RE และการแก้ไขข้อบกพร่อง
รูปแบบนิพจน์ทั่วไปนั้นมีความหมายมากเหลือเกินโดยมีอักขระเพียงไม่กี่ตัว แต่มีรูปแบบที่หนาแน่นมาก คุณจึงอาจใช้เวลานานในการแก้ไขข้อบกพร่องของรูปแบบ ตั้งค่ารันไทม์ของคุณเพื่อให้คุณสามารถเรียกใช้รูปแบบและพิมพ์สิ่งที่ตรงกันได้โดยง่าย เช่น เรียกใช้บนข้อความทดสอบขนาดเล็กและพิมพ์ผลลัพธ์ของ findall() หากรูปแบบดังกล่าวไม่ตรงกับอะไรเลย ให้ลองปรับรูปแบบให้เล็กลง ลบบางส่วนของรูปแบบออกเพื่อให้คุณได้รับการจับคู่มากเกินไป เมื่อผลลัพธ์ไม่ตรงกับใดเลย คุณจะคืบหน้าไปไม่ได้เพราะไม่มีอะไรให้พิจารณาได้อย่างเป็นรูปธรรม เมื่อจับคู่มากเกินไปแล้ว คุณก็สามารถค่อยๆ ปรับให้แน่นหนาขึ้นเพื่อให้ได้ผลลัพธ์ตามที่คุณต้องการ
ตัวเลือก
ฟังก์ชัน re ใช้ตัวเลือกเพื่อแก้ไขลักษณะการทำงานของการจับคู่รูปแบบ ระบบจะเพิ่มแฟล็กตัวเลือกเป็นอาร์กิวเมนต์เสริมให้กับ search() หรือ findall() เป็นต้น เช่น re.search(pat, str, re.IGNORECASE)
- IGNORECASE -- ไม่สนใจความแตกต่างที่ใช้ตัวพิมพ์ใหญ่/ตัวพิมพ์เล็กสำหรับการจับคู่ ดังนั้น "a" จะจับคู่ทั้ง "a" และ "A"
- DOTALL -- อนุญาตให้จุด (.) จับคู่กับบรรทัดใหม่ -- โดยทั่วไปจะจับคู่กับอะไรก็ได้ ยกเว้นบรรทัดใหม่ ซึ่งอาจทำให้คุณสับสนได้ คุณคิดว่า .* จะตรงกับทุกอย่าง แต่โดยค่าเริ่มต้นแล้ว จะไม่เกินท้ายบรรทัด โปรดทราบว่า \s (ช่องว่าง) จะรวมบรรทัดใหม่ ดังนั้นหากคุณต้องการจับคู่ช่องว่างของเซลล์ที่อาจมีการขึ้นบรรทัดใหม่ คุณก็แค่ใช้ \s*
- MULTILINE -- ภายในสตริงที่ประกอบด้วยหลายบรรทัด อนุญาตให้ ^ และ $ จับคู่จุดเริ่มต้นและจุดสิ้นสุดของแต่ละบรรทัด โดยปกติแล้ว ^/$ จะจับคู่จุดเริ่มต้นและจุดสิ้นสุดของทั้งสตริง
ความโลภกับการไม่โลภ (ไม่บังคับ)
ส่วนนี้เป็นส่วนที่ไม่บังคับซึ่งแสดงเทคนิคนิพจน์ทั่วไปขั้นสูงกว่าที่ไม่จำเป็นต้องใช้สำหรับแบบฝึกหัด
สมมติว่าคุณมีข้อความที่มีแท็ก: <b>foo</b> และ <i>so on</i>
สมมติว่าคุณกำลังพยายามจับคู่แต่ละแท็กด้วยรูปแบบ '(<.*>)' -- ระบบจะจับคู่อะไรเป็นอันดับแรก
ผลที่ได้อาจค่อนข้างน่าแปลกใจ แต่ลักษณะละโมบของ .* ทำให้มีการจับคู่ '<b>foo</b> ทั้งหมดและ <i>so on</i>' เป็นการจับคู่ที่สำคัญ แต่ปัญหาคือ .* ไปไกลที่สุดเท่าที่จะทำได้ แทนที่จะหยุดที่รายการแรก > (หรือที่เรียกว่า "โลภ")
มีส่วนขยายของนิพจน์ทั่วไปที่คุณเพิ่ม ? ต่อท้าย เช่น .*? หรือ .+? ตอนนี้คีย์เวิร์ดเหล่านั้นจะหยุดทันทีที่ทำได้ ดังนั้นรูปแบบ "(<.*?>)" จะได้รับ "<b>" เป็นการจับคู่แรกเท่านั้น และ "</b>" เป็นการจับคู่ที่สอง และส่งผลให้คู่ <..> แต่ละคู่กลับกัน โดยทั่วไปรูปแบบคือคุณใช้ .*? ตามด้วยเครื่องหมายคอนกรีต (> ในกรณีนี้) ที่การเรียกใช้ .*? ถูกบังคับให้ขยาย
ส่วนขยาย *? สร้างขึ้นใน Perl และนิพจน์ทั่วไปที่มีส่วนขยายของ Perl เรียกว่า Perl Compatible regular Expressions -- pcre Python มีการรองรับ pcre การใช้บรรทัดคำสั่งจำนวนมาก ฯลฯ จะมีแฟล็กที่ยอมรับรูปแบบ pcre
เทคนิคที่เก่ากว่าแต่ใช้กันอย่างแพร่หลายในการเขียนโค้ดแนวคิดของ "อักขระเหล่านี้ทั้งหมดยกเว้นการหยุดที่ X" จะใช้รูปแบบวงเล็บเหลี่ยม สำหรับรูปแบบข้างต้น คุณสามารถเขียนรูปแบบได้ แต่แทนที่จะใช้ .* หากต้องการอักขระทั้งหมด ให้ใช้ [^>]* ซึ่งจะข้ามอักขระทั้งหมดที่ไม่ใช่ > (เครื่องหมาย ^ นำหน้าจะ "กลับ" ชุดวงเล็บเหลี่ยม ดังนั้นจึงตรงกับอักขระใดๆ ที่ไม่ได้อยู่ในวงเล็บ)
การแทน (ไม่บังคับ)
ฟังก์ชัน re.sub(pat, replace, str) จะค้นหาอินสแตนซ์ทั้งหมดของรูปแบบในสตริงที่ระบุและแทนที่รูปแบบเหล่านั้น สตริงการแทนที่อาจมี '\1', '\2' ซึ่งอ้างอิงข้อความจากกลุ่ม(1), กลุ่ม(2) และต่อไปเรื่อยๆ จากข้อความเดิมที่ตรงกัน
นี่คือตัวอย่างที่ค้นหาที่อยู่อีเมลทั้งหมด แล้วเปลี่ยนให้คงผู้ใช้ไว้ (\1) แต่มี yo-yo-dyne.com เป็นโฮสต์
str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher' ## re.sub(pat, replacement, str) -- returns new string with all replacements, ## \1 is group(1), \2 group(2) in the replacement print(re.sub(r'([\w\.-]+)@([\w\.-]+)', r'\1@yo-yo-dyne.com', str)) ## purple alice@yo-yo-dyne.com, blah monkey bob@yo-yo-dyne.com blah dishwasher
การออกกำลังกาย
หากต้องการฝึกนิพจน์ทั่วไป โปรดดูแบบฝึกหัดการตั้งชื่อทารก