นิพจน์ทั่วไปของ 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-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'
(คุณลักษณะวงเล็บเหลี่ยมเพิ่มเติม) คุณยังสามารถใช้เครื่องหมายขีดคั่นเพื่อระบุช่วงได้ เพื่อให้ [a-z] ตรงกับตัวพิมพ์เล็กทั้งหมด หากต้องการใช้ขีดกลางโดยไม่ระบุช่วง ให้ใส่เครื่องหมายขีดกลางสุดท้าย เช่น [abc-]. เครื่องหมายอัพหมวก (^) อยู่ด้านหน้าของเครื่องหมายวงเล็บเหลี่ยมจะกลับด้าน ดังนั้น [^ab] จึงหมายถึงอักขระทุกตัวยกเว้น "a" หรือ 'b'

การดึงข้อมูลกลุ่ม

"กลุ่ม" ของนิพจน์ทั่วไปช่วยให้คุณเลือกส่วนของข้อความที่ตรงกันได้ สมมติว่าเกิดปัญหาเกี่ยวกับอีเมลที่เราต้องการแยกชื่อผู้ใช้และโฮสต์แยกกัน ในการดำเนินการนี้ ให้เพิ่มวงเล็บ ( ) รอบชื่อผู้ใช้และโฮสต์ในรูปแบบดังนี้: 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

การออกกำลังกาย

หากต้องการฝึกใช้นิพจน์ทั่วไป โปรดดูแบบฝึกหัดการตั้งชื่อเด็ก