Python Regular Expressions: Groups, Quantifiers, re.findall, re.sub - Sinhala Guide

Python Regular Expressions: Groups, Quantifiers, re.findall, re.sub - Sinhala Guide

කොහොමද යාලුවනේ? Python RegEx වලට ගැඹුරින් යමු!

අද අපි කතා කරන්න යන්නේ Software Engineering වල, විශේෂයෙන්ම Data Processing, Text Mining වගේ දේවල් වලදී අතිශයින්ම වැදගත් වෙන මාතෘකාවක් ගැන. ඒ තමයි Python Regular Expressions, විශේෂයෙන්ම Groups සහ Quantifiers. සමහරවිට RegEx කිව්වම ඔලුව අවුල් වුන අයත් ඇති, ඒත් කලබල වෙන්න එපා! අපි අද ඒ හැම දෙයක්ම හරිම සරලව, ලස්සනට තේරුම් ගමු. මොකද මේවා හරියට තේරුම් ගත්තොත් ඔයාලගේ Code එකට හිතාගන්න බැරි තරම් බලයක් එකතු වෙනවා.

අපි මේ Post එකෙන් RegEx වල මූලික Grouping Concepts, Quantifiers වර්ග, Greedy සහ Non-Greedy Matching අතර වෙනස, ඒ වගේම re.findall() සහ re.sub() වගේ වැදගත් Functions කොහොමද Groups සහ Quantifiers එක්ක පාවිච්චි කරන්නේ කියලා පැහැදිලි උදාහරණ එක්ක බලමු. ඊට අමතරව, හොඳම Best Practices කිහිපයකුත් අපි කතා කරනවා. එහෙනම් වැඩේට බහිමු!

Regular Expressions වලට පොඩි හැඳින්වීමක් (An Introduction to Regular Expressions)

සරලව කිව්වොත්, Regular Expressions (RegEx හෝ RegExp) කියන්නේ String Patterns හොයන්න, Matches කරන්න, සහ Modify කරන්න පාවිච්චි කරන පොඩි භාෂාවක් වගේ දෙයක්. හිතන්නකෝ ඔයාලට Data Files ගොඩක් තියෙනවා, ඒ හැම එකකින්ම Email Addresses, Phone Numbers, නැත්නම් Specific Dates වගේ දේවල් ටිකක් විතරක් extract කරගන්න ඕනේ කියලා. මේ වගේ වෙලාවකදී RegEx කියන්නේ ඔයාලගේ හොඳම යාලුවා. Python වලදී RegEx එක්ක වැඩ කරන්න පුළුවන් re කියන Built-in Module එකෙන්.

import re

text = "මගේ ඊමේල් එක [email protected] වන අතර දුරකථන අංකය 071-1234567." 
pattern = r"\d{3}-\d{7}"

match = re.search(pattern, text)
if match:
    print(f"දුරකථන අංකය: {match.group(0)}")
# Output: දුරකථන අංකය: 071-1234567

මේ පොඩි Code Snippet එකෙන් පෙනවනවා නේද RegEx කොච්චර ප්‍රයෝජනවත්ද කියලා? දැන් අපි මේකේ ඊලඟ අදියරට, ඒ කියන්නේ Groups සහ Quantifiers ගැන ගැඹුරින් බලමු.

1. Groups: තොරතුරු එකට ගැටගැසීම සහ ලබාගැනීම (Capturing Information)

RegEx වලදී Groups කියන්නේ Pattern එකක කොටසක් එකට එකතු කරන්න (Group කරන්න) සහ පස්සේ ඒ Group කරපු කොටස වෙනම Extract කරගන්න පුළුවන් Mechanism එකක්. මේකට අපි () (Parentheses) පාවිච්චි කරනවා.

1.1. Capturing Groups

() එක ඇතුලට දාන ඕනෑම Pattern එකක් Capturing Group එකක් විදිහට හැඳින්වෙනවා. මේ Group කරපු කොටස් වලට අපි Match එකක් ගත්තාට පස්සේ ඒවා වෙනමම Access කරන්න පුළුවන්. මේක හරිම වැදගත් වෙන්නේ ඔයාලට String එකක කොටසක් විතරක් ඕනේ වුනොත්.

import re

text = "මගේ නම John Doe. මගේ ඊමේල් එක [email protected]." 

# Email address එකේ Username එක සහ Domain එක වෙන් වෙන්ව ගන්න Pattern එකක්
# Group 1: Username (.*?) - .*: ඕනෑම අකුරු ගානක්, ?: Non-greedy
# Group 2: Domain ([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})
pattern = r"([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})"

match = re.search(pattern, text)

if match:
    print(f"සම්පූර්ණ Email: {match.group(0)}") # Full match
    print(f"Username: {match.group(1)}")  # First capturing group
    print(f"Domain: {match.group(2)}")    # Second capturing group
# Output:
# සම්පූර්ණ Email: [email protected]
# Username: john.doe
# Domain: example.com

දැක්කා නේද? match.group(0) එකෙන් සම්පූර්ණ Match එක දුන්නා. match.group(1) එකෙන් Username එකත්, match.group(2) එකෙන් Domain එකත් අපිට ලැබුණා. මේක හරිම Practical Situation වලදී හරිම ප්‍රයෝජනවත්. උදාහරණයක් විදිහට, Log Files වලින් Specific Event ID එකක් එක්ක වෙලාව සහ Message එක විතරක් ගන්න ඕනෙ වුනොත් මේ වගේ Groups පාවිච්චි කරන්න පුළුවන්.

1.2. Non-Capturing Groups

සමහර වෙලාවට අපිට Pattern එකේ කොටසක් Group කරන්න ඕනේ වෙනවා, ඒත් ඒ කොටස Match results වලට ගන්න ඕනේ නැහැ. මේ වගේ වෙලාවට Non-Capturing Groups පාවිච්චි කරනවා. මේවට (?:...) Syntax එක පාවිච්චි කරනවා.

import re

text = "Apple, Banana, Orange"

# 'Fruit' කියන වචනයට කලින් එන ඕනෑම වචනයක් ගන්න.
# 'Fruit' කියන එක Capturing Group එකක් විදිහට එපා.
pattern = r"(?:Apple|Banana|Orange)"

matches = re.findall(pattern, text)
print(matches)
# Output: ['Apple', 'Banana', 'Orange'] - මේකේ Non-capturing Group එක පාවිච්චි වුනේ නැහැ
# ඒත් අපි fruit කියන එක group නොකර හිටියොත් (Apple|Banana|Orange) විදිහට, 
# ඒක capturing group එකක් වෙනවා.

# Non-capturing group for grouping alternatives, without capturing the content of the group itself
text_numbers = "The prices are $100 and 200 euros."
# Match a number preceded by '$' or 'euro' without capturing the currency symbol
pattern_non_capturing = r"(?:\$|euro\s?)(\d+)"

matches = re.findall(pattern_non_capturing, text_numbers)
print(matches)
# Output: ['100', '200'] - මෙතනදී $ සහ euro කියන ඒවා output එකට ආවේ නැහැ.

දෙවෙනි උදාහරණයේදී, (?:\$|euro\s?) කියන කොටස Group වුනාට, ඒක Capture වුනේ නැහැ. අපිට ඕන වුනේ මුදල විතරයි. මේක තමයි Non-Capturing Group එකක ප්‍රධානම වාසිය.

2. Quantifiers: රටා වල පුනරාවර්තන පාලනය කිරීම (Controlling Pattern Repetitions)

Quantifiers කියන්නේ RegEx වල Pattern එකක Character එකක්, Group එකක්, හෝ Character Set එකක් කී පාරක් එන්න පුලුවන්ද කියලා specify කරන්න පාවිච්චි කරන ඒවා. මේවා හරිම Powerful, මොකද මේවා නැතුව අපිට සංකීර්ණ Patterns Match කරන්න හරිම අමාරුයි.

2.1. සාමාන්‍ය Quantifiers

  • *: 0 හෝ ඊට වැඩි වාර ගණනක් (Zero or more times)
  • +: 1 හෝ ඊට වැඩි වාර ගණනක් (One or more times)
  • ?: 0 හෝ 1 වාරයක් (Zero or one time - Optional)
  • {n}: හරියටම n වාරයක් (Exactly n times)
  • {n,}: n හෝ ඊට වැඩි වාර ගණනක් (n or more times)
  • {n,m}: n සහ m අතර වාර ගණනක් (Between n and m times, inclusive)
import re

text = "123 12 12345 1"

# 3 digits exactly
pattern_exactly_3 = r"\b\d{3}\b"
print(re.findall(pattern_exactly_3, text)) # Output: ['123']

# 2 or more digits
pattern_2_or_more = r"\b\d{2,}\b"
print(re.findall(pattern_2_or_more, text)) # Output: ['123', '12', '12345']

# 'a' followed by zero or more 'b's
text_ab = "abbc abc ac"
pattern_star = r"ab*c"
print(re.findall(pattern_star, text_ab)) # Output: ['abbc', 'abc', 'ac']

2.2. Greedy vs. Non-Greedy (or Lazy) Quantifiers

මේක RegEx වලදී ගොඩක් දෙනෙක්ට අවුල් වෙන තැනක්. Default විදිහට, Quantifiers Greedy. ඒ කියන්නේ Match එකක් හොයනකොට, එයාලට පුලුවන් තරම් Characters ගණනක් Match කරන්න එයාලා උත්සාහ කරනවා. ඒ කියන්නේ, දීර්ඝම Match එක හොයනවා.

ඒත්, සමහර වෙලාවට අපිට ඕනේ කෙටිම Match එක. මේකට අපි Non-Greedy (හෝ Lazy) Quantifiers පාවිච්චි කරනවා. මේවා හදන්නේ සාමාන්‍ය Quantifier එකක් පස්සට ? එකක් දාලා.

  • *?: 0 හෝ ඊට වැඩි වාර ගණනක්, නමුත් කෙටිම Match එක (Shortest possible match)
  • +?: 1 හෝ ඊට වැඩි වාර ගණනක්, නමුත් කෙටිම Match එක
  • ??: 0 හෝ 1 වාරයක්, නමුත් කෙටිම Match එක
  • {n,}?: n හෝ ඊට වැඩි වාර ගණනක්, නමුත් කෙටිම Match එක
import re

html_text = "<b>This is bold</b> and <b>this too</b>."

# Greedy match: Finds the longest match
# .* will match everything from the first  to the last 
pattern_greedy = r"<b>.*</b>"
print(f"Greedy: {re.findall(pattern_greedy, html_text)}")
# Output: Greedy: ['This is bold and this too'] - All in one go!

# Non-Greedy match: Finds the shortest match
# .*? will match up to the next 
pattern_non_greedy = r"<b>.*?</b>"
print(f"Non-Greedy: {re.findall(pattern_non_greedy, html_text)}")
# Output: Non-Greedy: ['This is bold', 'this too'] - Each bold tag separately!

දැක්කා නේද වෙනස? Greedy Quantifier එක මුලු String එකම එක Match එකක් විදිහට ගත්තා, මොකද .* එකට පුලුවන් වුනා <b> සහ </b> අතරේ තියෙන හැම character එකක්ම අල්ලගන්න. Non-Greedy Quantifier එක .*? පාවිච්චි කරපු නිසා, එයාලා හැකි කෙටිම Match එක හොයනවා. මේක HTML/XML parsing වගේ දේවල් වලදී අනිවාර්යයෙන්ම දැනගෙන ඉන්න ඕනේ දෙයක්!

3. re.sub() භාවිතයෙන් රටා වෙනස් කිරීම (Replacing Patterns using re.sub())

re.sub() function එක පාවිච්චි කරන්නේ String එකක් ඇතුලේ තියෙන Patterns වෙනත් String එකකින් Replace කරන්න. මේකේදීත් Groups වලට හරිම වැදගත් තැනක් තියෙනවා.

re.sub(pattern, replacement, string, count=0, flags=0)

  • pattern: අපි හොයන RegEx Pattern එක.
  • replacement: Pattern එක Replace කරන්න ඕනේ String එක. මේකේදී අපිට Backreferences (\1, \2 වගේ) පාවිච්චි කරන්න පුළුවන්.
  • string: Replace කරන්න ඕනේ Original String එක.
import re

text = "හලෝ, මගේ දුරකථන අංකය 077-1234567 වන අතර, මගේ විකල්ප අංකය 071-9876543." 

# දුරකථන අංක වල Format එක වෙනස් කරමු (e.g., 0771234567)
# Group 1: 077, 071 වගේ Prefix එක
# Group 2: ඉතුරු අංක
pattern = r"(0\d{2})-(\d{7})"

# replacement string එකේ \1 සහ \2 කියන්නේ පළවෙනි සහ දෙවෙනි Capturing Group වලට.
replaced_text = re.sub(pattern, r"\1\2", text)
print(f"Modified Text: {replaced_text}")
# Output: Modified Text: හලෝ, මගේ දුරකථන අංකය 0771234567 වන අතර, මගේ විකල්ප අංකය 0719876543.

# Email address වල domain එක වෙනස් කරමු
email_text = "[email protected] සහ [email protected]"
email_pattern = r"([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})"

# @newcompany.lk විදිහට replace කරමු
replaced_emails = re.sub(email_pattern, r"\[email protected]", email_text)
print(f"Modified Emails: {replaced_emails}")
# Output: Modified Emails: [email protected] සහ [email protected]

re.sub() කියන්නේ Data Sanitization, Data Migration, Text Formatting වගේ දේවල් වලට නැතුවම බෑරි Function එකක්. Backreferences පාවිච්චි කරලා Group කරපු කොටස් වලට වෙනමම Access කරගන්න පුලුවන් වීම මේකේ තියෙන ලොකුම වාසියක්.

4. හොඳම භාවිතයන් සහ උපදෙස් (Best Practices and Tips)

4.1. re.compile() භාවිතය

ඔයාලා එකම RegEx Pattern එකක් ගොඩක් පාරක් පාවිච්චි කරනවා නම්, ඒක Compile කරලා තියාගන්න එක Performance පැත්තෙන් ගොඩක් වාසිදායකයි. Compile කරනවා කියන්නේ Python Interpreter එකට ඒ Pattern එක Parse කරලා, ඔප්ටිමයිස් කරලා, නැවත නැවත පාවිච්චි කරන්න පුළුවන් Object එකක් විදිහට හදාගන්න එක.

import re
import time

long_text = "This is a long string with many dates like 2023-01-15, 2024-03-22, 2025-07-01 and more. " * 1000

# Without compile
start_time = time.time()
for _ in range(100):
    re.findall(r"\d{4}-\d{2}-\d{2}", long_text)
end_time = time.time()
print(f"Without compile: {end_time - start_time:.4f} seconds")

# With compile
compiled_pattern = re.compile(r"\d{4}-\d{2}-\d{2}")
start_time = time.time()
for _ in range(100):
    compiled_pattern.findall(long_text)
end_time = time.time()
print(f"With compile: {end_time - start_time:.4f} seconds")
# Output will show 'With compile' is faster, especially for many operations.

පැහැදිලිවම, වැඩි වාර ගණනක් RegEx Operations කරනකොට re.compile() පාවිච්චි කරන එකෙන් Performance Improvement එකක් ගන්න පුළුවන්.

4.2. Readability සහ Comments

සංකීර්ණ RegEx Patterns කියවන්න, තේරුම් ගන්න හරිම අමාරුයි. ඒ නිසා, පුලුවන් තරම් readable විදිහට Patterns ලියන්න උත්සාහ කරන්න. re.VERBOSE (හෝ re.X) flag එක පාවිච්චි කරලා RegEx Pattern එක ඇතුලේ Whitespace සහ Comments දාන්න පුළුවන්. මේකෙන් Code එකේ Readability එක වැඩි වෙනවා.

import re

# Example for a simple date pattern: YYYY-MM-DD
# Without re.VERBOSE
pattern_simple = r"\d{4}-\d{2}-\d{2}"
print(re.match(pattern_simple, "2023-11-20"))

# With re.VERBOSE (re.X)
pattern_verbose = re.compile(r"""
    ^                   # Start of the string
    (\d{4})            # Year (Group 1)
    -                   # Separator
    (\d{2})            # Month (Group 2)
    -                   # Separator
    (\d{2})            # Day (Group 3)
    $                   # End of the string
""", re.VERBOSE)

match = pattern_verbose.match("2023-11-20")
if match:
    print(f"Year: {match.group(1)}, Month: {match.group(2)}, Day: {match.group(3)}")

දෙවෙනි Pattern එක කොච්චර පැහැදිලිද? අනිවාර්යයෙන්ම මේ වගේ සංකීර්ණ Patterns වලට re.VERBOSE පාවිච්චි කරන්න.

4.3. RegEx Testing Tools

RegEx ලියනකොට වරදින්න පුළුවන්. ඒ නිසා, RegEx Patterns Test කරන්න පුළුවන් Online Tools ගොඩක් තියෙනවා. (e.g., regex101.com, regexr.com). මේවා පාවිච්චි කරලා ඔයාලගේ Patterns නිවැරදිද කියලා ලේසියෙන්ම Check කරගන්න පුළුවන්. මේවා RegEx වලට අලුත් අයට වගේම පළපුරුදු අයටත් හරිම ප්‍රයෝජනවත්.

අවසන් වශයෙන් (Conclusion)

ඉතින් යාලුවනේ, ඔයාලා දැක්කා නේද Python Regular Expressions වල Groups සහ Quantifiers කොච්චර Powerful ද කියලා. මේවා හරියට තේරුම් අරගෙන පාවිච්චි කරන එක ඔයාලගේ Programming Skills ගොඩක් ඉහළට ගෙනියන්න උපකාරී වෙනවා. විශේෂයෙන්ම Data Extraction, Validation, Transformation වගේ දේවල් වලට RegEx නැතුවම බෑ.

අපි අද කතා කරපු Concepts ටික තවත් Practice කරන්න, පොඩි පොඩි Problems Solve කරන්න උත්සාහ කරන්න. Greedy vs. Non-Greedy වෙනස අනිවාර්යයෙන්ම මතක තියාගන්න. ඒ වගේම re.compile() සහ re.VERBOSE වගේ Best Practices පාවිච්චි කරන්නත් අමතක කරන්න එපා.

ඔයාලට මේ Post එක ගැන තියෙන අදහස්, ප්‍රශ්න, නැත්නම් RegEx එක්ක වැඩ කරපු Experience එකක් තියෙනවා නම්, පහලින් Comment එකක් දාගෙන යන්න. ඒක අනිත් අයටත් ගොඩක් වටිනවා! තවත් මේ වගේ වැදගත් දේවල් ගැන කතා කරන්න අපි ලෑස්තියි. එහෙනම් තවත් අලුත් Post එකකින් හමුවෙමු!