นิพจน์ทั่วไปเป็นภาษาที่มีประสิทธิภาพสำหรับการจับคู่รูปแบบข้อความ หน้านี้จะให้ข้อมูลเบื้องต้นเกี่ยวกับนิพจน์ทั่วไปที่เพียงพอสำหรับแบบฝึกหัด 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-statement จะทดสอบการจับคู่ ถ้าเป็นจริง แสดงว่าการค้นหาสำเร็จและ match.group() คือข้อความที่ตรงกัน (เช่น "word:cat") หรือหากผลลัพธ์เป็น "เท็จ" (ไม่มี เพื่อระบุที่เจาะจงกว่านี้) แสดงว่าการค้นหาไม่สำเร็จ และไม่มีข้อความที่ตรงกัน
ตัว "r" ที่จุดเริ่มต้นของสตริงรูปแบบจะกำหนด Python "raw" สตริงที่ผ่านแบ็กสแลชโดยไม่เปลี่ยนแปลง ซึ่งสะดวกมากสำหรับนิพจน์ทั่วไป (Java ต้องการฟีเจอร์นี้มาก) เราขอแนะนำให้คุณเขียนสตริงรูปแบบด้วยเครื่องหมาย "r" เสมอ ให้เป็นนิสัย
รูปแบบพื้นฐาน
ความสามารถของนิพจน์ทั่วไปคือ สามารถระบุรูปแบบ ไม่ใช่เพียงอักขระคงที่เท่านั้น ต่อไปนี้คือรูปแบบพื้นฐานที่สุดซึ่งตรงกับอักขระเดี่ยว:
- ก, X, 9, < -- อักขระปกติจะตรงทุกประการกับ อักขระเมตาที่ไม่ตรงกับตัวเองเนื่องจากมีความหมายพิเศษ ได้แก่: ^ $ * + ? { [ ] \ | ( ) (ตามรายละเอียดด้านล่าง)
- (จุด) -- ตรงกับอักขระเดี่ยวใดๆ ยกเว้นบรรทัดใหม่ '\n'
- \w -- (w ตัวพิมพ์เล็ก) ตรงกับ "คำ" อักขระ: ตัวอักษรหรือตัวเลขหรือใต้แถบ [a-zA-Z0-9_] โปรดทราบว่าแม้ว่า "คำ" เป็นฟังก์ชันที่ช่วยจำในกรณีนี้ โดยจะจับคู่กับอักขระเพียงคำเดียว ไม่ใช่ทั้งคำ \W (ตัวอักษร W ตัวพิมพ์ใหญ่) จะจับคู่กับอักขระที่ไม่ใช่คำ
- \b -- ขอบเขตระหว่างคำและไม่ใช่คำ
- \s -- (ตัวพิมพ์เล็ก s) ตรงกับอักขระช่องว่างเดี่ยวๆ -- space, newline, Return, Tab, form [ \n\r\t\f] \S (S ตัวพิมพ์ใหญ่ และตัวพิมพ์เล็ก) จะจับคู่กับอักขระที่ไม่ใช่ช่องว่าง
- \t, \n, \r -- แท็บ, บรรทัดใหม่, ส่งกลับ
- \d -- เลขทศนิยม [0-9] (ยูทิลิตีนิพจน์ทั่วไปรุ่นเก่าบางรุ่นไม่รองรับ \d แต่ทุกตัวรองรับ \w และ \s)
- ^ = เริ่มต้น, $ = สิ้นสุด -- จับคู่จุดเริ่มต้นหรือจุดสิ้นสุดของสตริง
- \ -- ห้าม "ความพิเศษ" ของตัวละคร เช่น ให้ใช้ \ เพื่อจับคู่กับเครื่องหมายจุด หรือ \\ เพื่อจับคู่กับเครื่องหมายทับ หากไม่แน่ใจว่าอักขระมีความหมายพิเศษหรือไม่ เช่น "@" คุณอาจลองใส่เครื่องหมายทับไว้หน้าอักขระนั้น \@ หากลำดับหลีกไม่ถูกต้อง เช่น \c โปรแกรม Python จะหยุดลงพร้อมข้อผิดพลาด
ตัวอย่างพื้นฐาน
มุก: หนูเรียกว่าอะไรเป็นหมูที่มีตา 3 ตา piiig!
กฎพื้นฐานของการค้นหานิพจน์ทั่วไปสำหรับรูปแบบภายในสตริงมีดังนี้
- การค้นหาจะดำเนินต่อผ่านสตริงจากต้นจนจบ โดยหยุดตรงรายการแรกที่พบ
- รูปแบบทั้งหมดต้องตรงกับ แต่ไม่ใช่ทุกสตริง
- หาก
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+" = i's [มากกว่าหนึ่ง]
- * -- รูปแบบทางด้านซ้าย 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 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 ฯลฯ ใช้ได้ในวงเล็บเหลี่ยมเช่นกัน โดยมีข้อยกเว้น 1 ข้อที่จุด (.) จะหมายถึงจุดตรงตัวเท่านั้น สำหรับปัญหาอีเมล การเพิ่ม "." ในวงเล็บเหลี่ยมเป็นวิธีง่ายๆ และ "-" เป็นชุดอักขระที่สามารถปรากฏอยู่รอบ @ ที่มีรูปแบบ r'[\w.-]+@[\w.-]+' เมื่อต้องการรับอีเมลแบบเต็ม:
match = re.search(r'[\w.-]+@[\w.-]+', str) if match: print(match.group()) ## 'alice-b@google.com'
การดึงข้อมูลกลุ่ม
"กลุ่ม" ของนิพจน์ทั่วไปช่วยให้คุณเลือกส่วนของข้อความที่ตรงกันได้ สมมติว่าเกิดปัญหาเกี่ยวกับอีเมลที่เราต้องการแยกชื่อผู้ใช้และโฮสต์แยกกัน ในการดำเนินการนี้ ให้เพิ่มวงเล็บ ( ) รอบชื่อผู้ใช้และโฮสต์ในรูปแบบดังนี้: r'([\w.-]+)@([\w.-]+)' ในกรณีนี้ วงเล็บจะไม่เปลี่ยนสิ่งที่ตรงกับรูปแบบ แต่จะสร้าง "กลุ่ม" เชิงตรรกะแทน ภายในข้อความที่ตรงกัน ในการค้นหาที่ประสบความสำเร็จ match.group(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
findall() อาจเป็นฟังก์ชันเดียวที่มีประสิทธิภาพมากที่สุดในโมดูลใหม่ ด้านบน เราใช้ 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)
Findall ด้วย 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 กลุ่มขึ้นไป การค้นหารายการสตริงจะส่งกลับรายการ *tuples* แทนที่จะแสดงรายการของสตริง แต่ละ Tuple แสดงถึงการจับคู่รูปแบบหนึ่ง และภายใน 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 แต่ละรายการ หากรูปแบบไม่มีวงเล็บ searchall() จะแสดงรายการสตริงที่พบดังในตัวอย่างก่อนหน้านี้ ถ้ารูปแบบมีวงเล็บชุดเดียว findall() จะแสดงรายการของสตริงที่เกี่ยวข้องกับกลุ่มเดี่ยวนั้น (คุณลักษณะตัวเลือกคลุมเครือ: บางครั้งคุณมีการจัดกลุ่มวงเล็บ ( ) ในรูปแบบนี้ แต่คุณไม่ต้องการดึงข้อมูล ในกรณีนี้ ให้เขียนวงเล็บด้วยเครื่องหมาย ?: ที่ด้านหน้า เช่น (?: ) และวงเล็บด้านซ้ายจะไม่นับเป็นผลลัพธ์กลุ่ม)
เวิร์กโฟลว์ RE และแก้ไขข้อบกพร่อง
รูปแบบนิพจน์ปกติมีความหมายมากมายโดยมีอักขระเพียงไม่กี่ตัว แต่มีความหมายมาก คุณจึงใช้เวลามากมายไปกับการแก้ไขข้อบกพร่องของรูปแบบ ตั้งค่ารันไทม์เพื่อให้คุณเรียกใช้รูปแบบและพิมพ์การจับคู่ที่ตรงกันได้อย่างง่ายดาย ตัวอย่างเช่น โดยการเรียกใช้บนข้อความทดสอบขนาดเล็กและพิมพ์ผลลัพธ์ของ findall() หากรูปแบบไม่ตรงกันเลย ให้ลองทำให้รูปแบบอ่อนลง นำส่วนต่างๆ ของรูปแบบออกเพื่อให้ตรงกันมากเกินไป หากไม่พบผลลัพธ์ที่ตรงกัน คุณจะดำเนินการคืบหน้าต่อไปไม่ได้เนื่องจากไม่มีอะไรเป็นรูปธรรมให้พิจารณา เมื่อจับคู่ได้มากเกินไปแล้ว คุณสามารถค่อยๆ กระชับเพื่อให้ได้ผลลัพธ์ที่ต้องการ
ตัวเลือก
ฟังก์ชันอีกครั้งจะใช้ตัวเลือกในการแก้ไขลักษณะการทำงานของการจับคู่รูปแบบ มีการเพิ่มค่าสถานะตัวเลือกเป็นอาร์กิวเมนต์พิเศษใน search() หรือ findall() ฯลฯ เช่น re.search(pat, str, re.IGNORECASE)
- IGNORECASE -- ไม่พิจารณาความแตกต่างของตัวพิมพ์ใหญ่/เล็กในการจับคู่ เช่น "a" ตรงกับทั้ง 'a' และ "A"
- DOTALL -- อนุญาตให้จุด (.) จับคู่กับบรรทัดใหม่ โดยปกติแล้วจะตรงเท่าใดก็ได้ ยกเว้นบรรทัดใหม่ สิ่งนี้อาจทำให้คุณเหลื่อมล้ำขึ้น คุณคิดว่า .* ตรงกับทุกอย่าง แต่โดยค่าเริ่มต้น ข้อความจะไม่ยาวสุดบรรทัด โปรดทราบว่า \s (ช่องว่าง) จะรวมบรรทัดใหม่ด้วย ดังนั้นหากคุณต้องการจับคู่การเว้นวรรคที่อาจรวมขึ้นบรรทัดใหม่ คุณก็สามารถใช้ \s* ได้
- MULTILINE -- ภายในสตริงที่ประกอบด้วยหลายบรรทัด อนุญาตให้ ^ และ $ จับคู่จุดเริ่มต้นและจุดสิ้นสุดของแต่ละบรรทัด โดยปกติ ^/$ จะจับคู่จุดเริ่มต้นและจุดสิ้นสุดของสตริงทั้งหมด
โล่งกับไม่โล่ง (ไม่บังคับ)
ส่วนนี้เป็นส่วนที่ไม่บังคับซึ่งแสดงเทคนิคนิพจน์ทั่วไปขั้นสูงที่ไม่จำเป็นต้องใช้สำหรับแบบฝึกหัด
สมมติว่าคุณมีข้อความที่มีแท็ก: <b>foo</b> และ <i>อื่นๆ </i>
สมมติว่าคุณพยายามจับคู่แต่ละแท็กกับรูปแบบ '(<.*>)' -- สิ่งที่จับคู่เป็นอันดับแรกคืออะไร
ผลที่ได้นั้นน่าประหลาดใจเล็กน้อย แต่ความโลภของ .* กลับทำให้ผลลัพธ์นั้นตรงกับ "<b>foo</b> ทั้งหมด" และ <i>เช่น บน</i>" เป็นหนึ่งแมตช์สำคัญ ปัญหาคือ .* จะดำเนินการให้มากที่สุดเท่าที่ทำได้ แทนที่จะหยุดที่แรก > (หรือเรียกว่า "โลภ")
มีส่วนขยายไปยังนิพจน์ทั่วไปที่เพิ่ม ? ต่อท้าย เช่น .*? หรือ .+? โดยเปลี่ยนเป็นการไม่ละโมบ ตอนนี้ลูกค้าจะหยุดโดยเร็วที่สุด ดังนั้นรูปแบบ '(<.*?>)' จะได้รับเพียง "<b>" เป็นการจับคู่แรก และ '</b>' เป็นการจับคู่ที่ 2 และเป็นเช่นนี้ไปเรื่อยๆ เพื่อให้ได้ <..> แต่ละรายการ กลับกัน รูปแบบนี้มักใช้ไฟล์ .*? แล้วตามด้วยเครื่องหมายที่เป็นรูปธรรม (> ในกรณีนี้) ซึ่งเครื่องหมาย .*? จะถูกบังคับให้ขยาย
อักขระ *? ส่วนขยายเริ่มต้นใน Perl และนิพจน์ทั่วไปที่มีส่วนขยายของ Perl เรียกว่านิพจน์ทั่วไปที่ใช้ร่วมกันได้ของ Perl -- pcre Python มีการสนับสนุน pcre คำสั่ง util หรืออื่นๆ จำนวนมากมี Flag ที่ยอมรับรูปแบบ pcre
เทคนิคเก่าแต่ใช้กันอย่างแพร่หลายในการเขียนโค้ดแนวคิด "อักขระเหล่านี้ทั้งหมด ยกเว้นการหยุดที่ X" ใช้รูปแบบวงเล็บเหลี่ยม สำหรับด้านบน คุณสามารถเขียนรูปแบบ แต่แทนที่จะใช้ .* เพื่อให้ได้อักขระทั้งหมด ให้ใช้ [^>]* ซึ่งจะข้ามอักขระทั้งหมดที่ไม่ใช่ > (เครื่องหมาย ^ นำหน้า "กลับ" เครื่องหมายวงเล็บเหลี่ยมที่ตั้งค่าไว้ เพื่อให้จับคู่กับอักขระทุกตัวที่ไม่อยู่ในวงเล็บ)
การแทน (ไม่บังคับ)
ฟังก์ชัน re.sub(pat, Replace, str) จะค้นหาอินสแตนซ์ทั้งหมดของรูปแบบในสตริงที่ระบุและแทนที่อินสแตนซ์เหล่านั้น สตริงที่จะแทนที่มี "\1", "\2" ได้ ซึ่งจะอ้างถึงข้อความจาก group(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
การออกกำลังกาย
หากต้องการฝึกใช้นิพจน์ทั่วไป โปรดดูแบบฝึกหัดการตั้งชื่อเด็ก