นิพจน์ทั่วไปของ Python

นิพจน์ทั่วไปเป็นภาษาที่มีประสิทธิภาพสำหรับการจับคู่รูปแบบข้อความ หน้านี้ให้ข้อมูลเบื้องต้นเกี่ยวกับนิพจน์ทั่วไปที่เพียงพอสำหรับแบบฝึกหัด 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

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

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