সরাসরি পর্বে যান
পর্ব ২৮ · ৫৫-এর মধ্যে · মডিউল ৪

regular expression — লেখার আকৃতি দিয়ে খোঁজা

Regular Expressions in Python — Finding Text by Its Shape

Taught in Bangla, this lesson teaches regular expressions in Python: raw strings and the re module's search, match and fullmatch, character classes, quantifiers and anchors, greedy versus lazy matching, named groups, validating phone numbers, emails and dates, regex pitfalls with Bangla text, and catastrophic backtracking.

এই পর্বে যা শিখবেন

  • regular expression আসলে কী, কোন কাজে এটি সেরা আর কোন কাজে একেবারে ভুল যন্ত্র
  • re module-এর মূল ফাংশন — search, match, fullmatch, findall, finditer, sub, split, compile
  • প্যাটার্নের ইট — character class, quantifier, anchor, group, alternation আর flag; greedy বনাম lazy
  • বাস্তব কাজ — বাংলাদেশি মোবাইল নম্বর যাচাই, bKash/Nagad-ধাঁচের SMS থেকে টাকা ও TrxID বের করা, নম্বর ঢেকে দেখানো
  • বাংলা লেখায় regex-এর ফাঁদ, catastrophic backtracking, আর AI-এর লেখা regex কীভাবে যাচাই করবেন

১ · regular expression কী — লেখার “আকৃতি” বর্ণনার ছোট একটি ভাষা

পর্ব ০৯-এ আপনি string method দিয়ে লেখা নিয়ে কাজ করেছেন: "OTP" in sms, text.replace("মো.", "মোহাম্মদ"), phone.startswith("017"). এগুলো সবই একটি নির্দিষ্ট লেখা খোঁজে। কিন্তু বাস্তবের অনেক প্রশ্ন নির্দিষ্ট লেখা নিয়ে নয়, লেখার আকৃতি নিয়ে। “এই ফর্মে যা লেখা হয়েছে, সেটা কি একটি বৈধ মোবাইল নম্বরের মতো দেখতে?” “এই SMS-এ টাকার অঙ্কটা কোথায়?” “এই লগ ফাইলে যত তারিখ আছে, সব বের করো।” আপনি আগে থেকে জানেন না নম্বরটা কী হবে — জানেন শুধু তার চেহারা: 01 দিয়ে শুরু, তারপর 3 থেকে 9-এর একটি অঙ্ক, তারপর আরও ৮টি অঙ্ক।

এই চেহারা বর্ণনা করার জন্য একটি ছোট, ঘন ভাষা আছে, যার নাম regular expression — ছোট করে regex. উপরের মোবাইল নম্বরের বর্ণনাটি regex-এ লেখা যায় এভাবে: 01[3-9]\d{8}. পড়ুন বাম থেকে: 0, তারপর 1, তারপর [3-9] মানে “3 থেকে 9-এর যেকোনো একটি”, তারপর \d{8} মানে “ঠিক ৮টি অঙ্ক”। পুরো একটি বাক্যের কাজ মাত্র বারোটি চিহ্নে।

ধরুন আপনি কমলাপুর স্টেশনে একজন অচেনা আত্মীয়কে আনতে গেছেন। তাঁর নাম জানেন না, ছবিও দেখেননি — শুধু জানেন “লম্বা, সাদা পাঞ্জাবি, হাতে সবুজ ব্যাগ”। এই বর্ণনা দিয়ে আপনি হাজার মানুষের ভিড়ে খুঁজতে পারেন, আর যাকে দেখছেন তার সাথে মিলিয়ে বলতে পারেন “মিলছে” বা “মিলছে না”। regex ঠিক এমন একটি বর্ণনা — নির্দিষ্ট মানুষ নয়, মানুষের চেহারার নিয়ম। আর বর্ণনা যত ঢিলেঢালা, তত ভুল লোককেও আপনি “মিলছে” বলবেন; যত কড়া, তত সঠিক লোককেও হয়তো বাদ দেবেন।

একটি regex দিয়ে মূলত চার ধরনের কাজ হয়:

🔍 খোঁজা

লেখার ভেতরে প্যাটার্নটা কোথাও আছে কি না, থাকলে কোথায়।

re.search

✅ যাচাই

পুরো লেখাটাই কি প্যাটার্নের সাথে হুবহু মেলে — ফর্মের ইনপুটের জন্য।

re.fullmatch

📤 বের করা

মেলা সব অংশ, বা মেলা অংশের ভেতরের নির্দিষ্ট টুকরো তুলে আনা।

re.findall · group()

✏️ বদলানো

মেলা অংশ অন্য কিছু দিয়ে বদলানো, বা প্যাটার্ন ধরে লেখা ভাঙা।

re.sub · re.split
মূল ধারণা

regex হলো লেখার আকৃতির একটি ছোট ভাষা. এটি খুব শক্তিশালী, কিন্তু খুব ঘন — এক লাইনের regex পড়তে প্রায়ই দশ লাইনের কোডের চেয়ে বেশি সময় লাগে। তাই এই পর্বের প্রথম নিয়ম: সাধারণ string method (in, startswith, split, replace) দিয়ে কাজ হলে regex ব্যবহার করবেন না. regex তখনই, যখন সত্যিই “আকৃতি” বর্ণনা করতে হবে।

“Regular expression” নামটি এসেছে গণিত থেকে। ১৯৫০-এর দশকে মার্কিন গণিতবিদ Stephen Kleene স্নায়ুকোষের জাল নিয়ে গবেষণা করতে গিয়ে “regular event” বর্ণনার একটি বীজগণিত তৈরি করেন — আজকের * চিহ্নটিকে এখনো তাঁর নামে “Kleene star” বলা হয়। ১৯৬৮ সালে Ken Thompson এই তত্ত্বকে প্রথমবার একটি টেক্সট এডিটরে কাজে লাগান, আর সেখান থেকে ed, grep হয়ে আজ প্রায় সব প্রোগ্রামিং ভাষায়। পুরো গল্প ভাবনার প্রশ্ন ০১-এ।

২ · re module — raw string আর search, match, fullmatch

Python-এ regex-এর কাজ করে standard library-র re module (পর্ব ২২-এ import শিখেছেন, পর্ব ২৭-এ library-র আরও কয়েকটি module দেখেছেন)। প্রথম নিয়মটি পর্ব ০৯ থেকেই চেনা: প্যাটার্ন সবসময় raw string-এ লিখুন — r"\d+", শুধু "\d+" নয়।

কেন? regex-এ backslash (\) অনেক: \d, \w, \s, \b. আর সাধারণ Python string-ও backslash-কে নিজের মতো ব্যাখ্যা করে। সবচেয়ে বিপজ্জনক উদাহরণ \b: regex-এ এর মানে “শব্দের সীমানা”, কিন্তু সাধারণ string-এ "\b" হলো backspace নামের একটি অদৃশ্য অক্ষর। তাই re.search("\bcat\b", "a cat here") ফল দেয় None — কোনো error ছাড়াই চুপচাপ ভুল! আর re.search(r"\bcat\b", "a cat here") ঠিকঠাক cat খুঁজে পায়। r লিখলে Python backslash ছোঁয় না, পুরোটা যায় re-এর কাছে। ("\d"-এর মতো অচেনা escape-এ Python এখন SyntaxWarning দেয়, কিন্তু "\b" চেনা escape বলে কোনো সতর্কবার্তাও আসে না।)

তিন ভাই — search, match আর fullmatch

তিনটি ফাংশনই প্রশ্ন করে “প্যাটার্নটা মেলে কি?”, কিন্তু কোথায় মিলতে হবে তা আলাদা। মিললে তিনটিই একটি match object ফেরত দেয়, না মিললে None.

ফাংশনকোথায় মিলতে হবেr"\d+" আর "abc123"কখন ব্যবহার
re.searchলেখার যেকোনো জায়গায়, প্রথম মিলমেলে: '123'লেখার ভেতরে কিছু খোঁজা
re.matchশুধু একেবারে শুরু থেকেNone (শুরুতে a)খুব কম; প্রায়ই ভুল বোঝা হয়
re.fullmatchপুরো লেখা, শুরু থেকে শেষNoneইনপুট যাচাই

নামটি বিভ্রান্তিকর: re.match “পুরোটা মেলে কি না” দেখে না, শুধু শুরুটা দেখে। re.match(r"\d+", "123abc") সফল — শুরুর 123 মিলেছে, পরের abc নিয়ে তার মাথাব্যথা নেই। তাই ফর্মে কেউ 017123456789 (১২ অঙ্ক) লিখলে match আর search দুটোই “ঠিক আছে” বলবে, কারণ প্রথম ১১টি অঙ্ক তো প্যাটার্নের সাথে মেলে! re.fullmatch (Python 3.4 থেকে আছে) ঠিক এই সমস্যার সমাধান: পুরো লেখা মিলতে হবে, একটি অক্ষরও বাড়তি থাকা চলবে না।

এবার দেখুন regex ইঞ্জিন ভেতরে কীভাবে মেলায় — প্যাটার্নের প্রতিটি অংশ এক এক করে লেখার অক্ষরের সাথে তুলনা:

প্যাটার্নের অংশ: 0লেখার ঘর 0
প্যাটার্ন: 0  1  [3-9]  \d{8}
লেখা    : 0 1 7 1 2 3 4 5 6 7 8
           ↑ '0' == '0' ✔
প্যাটার্নের অংশ: 1লেখার ঘর 1
প্যাটার্ন: 0  1  [3-9]  \d{8}
লেখা    : 0 1 7 1 2 3 4 5 6 7 8
             ↑ '1' == '1' ✔
প্যাটার্নের অংশ: [3-9]লেখার ঘর 2
প্যাটার্ন: 0  1  [3-9]  \d{8}
লেখা    : 0 1 7 1 2 3 4 5 6 7 8
               ↑ '7' কি 3–9-এর মধ্যে? ✔
প্যাটার্নের অংশ: \d{8}ঘর 3 থেকে 10
প্যাটার্ন: 0  1  [3-9]  \d{8}
লেখা    : 0 1 7 1 2 3 4 5 6 7 8
                 ↑ ৮টি অঙ্ক গোনা হলো ✔ — প্যাটার্ন শেষ
নতুন লেখা: 01212345678অংশ [3-9]
প্যাটার্ন: 0  1  [3-9]  \d{8}
লেখা    : 0 1 2 1 2 3 4 5 6 7 8
               ↑ '2' 3–9-এর মধ্যে নেই ✘
search: শুরুর ঘর সরিয়ে আবার চেষ্টাঘর 1, 2, 3 …
ঘর 1 থেকে: '1' ≠ '0' ✘
ঘর 2 থেকে: '2' ≠ '0' ✘
…  কোথাও আর '0' নেই
ফল: None — কোথাও মিল নেই
ইঞ্জিনের অবস্থা
লেখা
এ পর্যন্ত মিলেছে
ফল
  1. প্রথম অংশ 0. ইঞ্জিন প্যাটার্নের প্রথম অংশ নিয়ে লেখার ঘর 0-এর অক্ষরের সাথে মেলায়। সাধারণ অক্ষর (literal) নিজেকেই মেলায় — '0' আর '0', মিলে গেল।
  2. দ্বিতীয় অংশ 1. এক ঘর এগিয়ে, আবার literal তুলনা। মিলেছে। ইঞ্জিন সবসময় প্যাটার্ন আর লেখা দুটোতেই একসাথে সামনে এগোয়।
  3. [3-9] — একটি ঘর, অনেক বিকল্প। বন্ধনীর ভেতরের পুরোটা মিলে একটি অক্ষর মেলায়, যা 3 থেকে 9-এর মধ্যে যেকোনো একটি হতে পারে। '7' মিলেছে। এখানেই অপারেটর-কোডের নিয়মটি বসানো: লেখার সময় চালু অপারেটর কোড 013 থেকে 019, তাই 010, 011, 012 বাতিল।
  4. \d{8} — ঠিক ৮টি অঙ্ক. \d মানে একটি অঙ্ক, {8} মানে “ঠিক ৮ বার”। ইঞ্জিন ৮টি অঙ্ক গুনে নেয়, প্যাটার্ন শেষ — মিল পাওয়া গেছে, span (0, 11). লেখাও ঠিক এখানে শেষ, তাই fullmatch-ও সফল হতো।
  5. ব্যর্থ উদাহরণ: 01212345678. 0 আর 1 মিলল, কিন্তু [3-9]-এর সামনে '2' — বাতিল। match আর fullmatch শুধু শুরু থেকে চেষ্টা করে, তাই এখানেই উত্তর None.
  6. search হাল ছাড়ে না। শুরুর ঘর এক এক করে সরিয়ে আবার পুরো প্যাটার্ন মেলানোর চেষ্টা করে — ঘর 1, ঘর 2… এখানে আর কোথাও '0' নেই, তাই শেষে None. উল্টো দিকে 017123456789 (১২ অঙ্ক) দিলে search প্রথম ১১টি অঙ্কেই মিল পেয়ে খুশি, কিন্তু fullmatch দেখে শেষে একটি 9 বাড়তি রয়ে গেছে — তাই বাতিল। যাচাইয়ের জন্য এটাই চাই।

বাকি ফাংশনগুলো — এক টেবিলে

ফাংশনকী ফেরত দেয়উদাহরণ → ফল
re.findall(p, s)সব মিলের list (group থাকলে group-এর)re.findall(r"\d+", "a12b345") → ['12', '345']
re.finditer(p, s)প্রতিটি মিলের match object, একটি একটি করে (iterator)বড় লেখায় মেমরি বাঁচায়, অবস্থানও জানায়
re.sub(p, new, s)মেলা অংশ বদলে নতুন stringre.sub(r"\s+", " ", "a b") → 'a b'
re.split(p, s)প্যাটার্নের জায়গায় ভেঙে listre.split(r"[,;]\s*", "আম, কলা;লিচু") → ['আম', 'কলা', 'লিচু']
re.compile(p)একটি প্যাটার্ন object, যার ওপর একই method-গুলো চলেPHONE = re.compile(r"..."), পরে PHONE.fullmatch(s)

re.compile কেন? regex লেখা থেকে ইঞ্জিন প্রথমে একটি ভেতরের কাঠামো বানায় — একে বলে compile করা। re module সাম্প্রতিক ব্যবহৃত প্যাটার্নগুলো নিজেই cache করে রাখে, তাই গতির পার্থক্য সাধারণত সামান্য। compile-এর আসল লাভ পরিষ্কার কোড: প্যাটার্নটি একবার একটি অর্থপূর্ণ নাম দিয়ে (BD_MOBILE) module-এর ওপরে রাখা যায়, পুরো প্রোগ্রাম সেটাই ব্যবহার করে, আর বদলাতে হলে এক জায়গায় বদলালেই হয়।

৩ · প্যাটার্নের ইট — character class, quantifier আর anchor

regex-এ বেশিরভাগ অক্ষর নিজেকেই মেলায়: Tk মেলায় ঠিক “Tk”. কিন্তু কিছু অক্ষরের বিশেষ মানে আছে — এদের বলে metacharacter: . ^ $ * + ? { } [ ] \ | ( ). এদের আসল অক্ষর হিসেবে খুঁজতে আগে backslash দিন: \. মানে আসল ডট, \+ মানে আসল যোগচিহ্ন। +880 খুঁজতে তাই লিখতে হয় \+880.

একটি ঘরে কোন অক্ষর বসতে পারে — character class

তৃতীয় বন্ধনী [...] দিয়ে লেখা হয় character class: ভেতরের যেকোনো একটি অক্ষর মেলে। [aeiou] একটি স্বরবর্ণ, [0-9] একটি অঙ্ক, [A-Za-z] একটি ইংরেজি অক্ষর। শুরুতে ^ দিলে উল্টো মানে: [^0-9] মানে “অঙ্ক ছাড়া যেকোনো একটি অক্ষর”। বহুল ব্যবহৃত class-গুলোর ছোট নামও আছে:

লিখবেনমানেসাবধানতা
.newline ছাড়া যেকোনো একটি অক্ষরআসল ডট খুঁজতে \.
\d / \Dএকটি অঙ্ক / অঙ্ক নয় এমনPython 3-এ বাংলা অঙ্কও অঙ্ক!
\w / \Wশব্দের অক্ষর (অক্ষর, অঙ্ক, _) / নয় এমনবাংলা কার-চিহ্ন \w নয়
\s / \Sফাঁকা জায়গা (space, tab, newline) / নয় এমনঅনেক Unicode ফাঁকাও এর মধ্যে
^ / $লেখার শুরু / শেষ (anchor)কোনো অক্ষর খায় না, শুধু অবস্থান
\bশব্দের সীমানা (word boundary)বাংলায় ভুল জায়গায় পড়ে (অংশ ৭)

\d মানে শুধু 0–9 নয়। Python 3-এ str প্যাটার্ন ডিফল্টে Unicode নিয়ম মানে, তাই \d মেলায় যেকোনো লিপির দশমিক অঙ্ক — বাংলা ০–৯, আরবি, দেবনাগরী সবই। re.fullmatch(r"\d{11}", "০১৭১২৩৪৫৬৭৮") সফল! পর্ব ০৯-এর isdigit()-এর মতোই সুখবর আর দুঃসংবাদ একসাথে: বাংলা অঙ্কে লেখা নম্বর পাস করবে, কিন্তু আপনার পরের কোড বা ডেটাবেস যদি শুধু ইংরেজি অঙ্ক আশা করে, সেখানে গিয়ে ভাঙবে। শুধু ইংরেজি অঙ্ক চাইলে লিখুন [0-9], অথবা পুরো প্যাটার্নে re.ASCII flag দিন। আর যদি বাংলা অঙ্কও গ্রহণ করতে চান, তাহলে সেটা সচেতন সিদ্ধান্ত হোক — যাচাইয়ের পর int() দিয়ে রূপান্তর করে নিন, যা বাংলা অঙ্কও বোঝে।

কতবার — quantifier

একটি অংশ কতবার পরপর আসবে, তা বলে quantifier. এটি সবসময় ঠিক আগের অংশের ওপর কাজ করে — একটি অক্ষর, একটি class, বা বন্ধনীতে ঘেরা একটি group.

লিখবেনকতবারউদাহরণ
*০ বা তার বেশিab* মেলে a, ab, abbb
+১ বা তার বেশি\d+ মেলে যেকোনো দৈর্ঘ্যের সংখ্যা
?০ বা ১ (ঐচ্ছিক)colou?r মেলে color, colour
{n}ঠিক n বার\d{4} — চার অঙ্কের বছর
{n,m}n থেকে m বার\d{2,3} মেলে 12, 123
{n,}অন্তত n বার\w{3,} — অন্তত তিন অক্ষর

একটি সূক্ষ্মতা: re.findall(r"\d{2,3}", "1 12 123 1234") দেয় ['12', '123', '123'] — শেষেরটি 1234-এর প্রথম তিন অঙ্ক, কারণ প্যাটার্নে বলা নেই সংখ্যাটা কোথায় শেষ হতে হবে। পুরো সংখ্যা চাইলে সীমানা বসান: \b\d{2,3}\b. এটাই anchor আর boundary-র কাজ — তারা কোনো অক্ষর খায় না, শুধু বলে “এখানে একটি সীমা থাকতে হবে”।

Anchor: ^, $ আর \b

^\d+$ মানে “শুরু থেকে শেষ পর্যন্ত শুধু অঙ্ক”। re.search-এর সাথে ^...$ দিলে প্রায় fullmatch-এর মতো কাজ হয় — তবে পুরোপুরি নয়: $ লেখার একেবারে শেষে থাকা একটি newline-এর ঠিক আগেও মেলে, তাই re.search(r"^\d+$", "123\n") সফল হয়। যাচাইয়ের জন্য তাই fullmatch বেশি নিরাপদ ও পরিষ্কার। আর \b বসে একটি “শব্দের অক্ষর” আর “শব্দের অক্ষর নয়” এমন কিছুর মাঝখানে: re.findall(r"\bcat\b", "cat catalog bobcat cat.") দেয় শুধু দুটি 'cat' — catalog আর bobcat-এর ভেতরেরগুলো বাদ।

৪ · greedy বনাম lazy — .* কতটুকু খাবে

quantifier ডিফল্টে greedy — লোভী: যতটা সম্ভব বেশি অক্ষর খায়, তারপর দরকার হলে একটু একটু করে ফেরত দেয়। সাধারণত এটাই চাই, কিন্তু একটি ক্লাসিক ক্ষেত্রে এটি চমকে দেয়। ধরুন "<b>bold</b>" থেকে ট্যাগগুলো তুলতে চান, আর লিখলেন <.*> — “<, তারপর যেকোনো কিছু, তারপর >”. আশা করছেন দুটি ট্যাগ, পাবেন পুরো লেখা একটিমাত্র মিল হিসেবে! quantifier-এর পরে ? দিলে সেটা হয় lazy — যত কম পারা যায়। ধাপে ধাপে দেখুন:

greedy: <.*>অংশ <
ঘর  : 0 1 2 3 4 5 6 7 8 9 10
লেখা: < b > b o l d < / b >
      ↑ '<' মিলল
greedy: .* সব খেয়ে ফেললঘর 1 থেকে শেষ
ঘর  : 0 1 2 3 4 5 6 7 8 9 10
লেখা: < b > b o l d < / b >
                            ↑ শেষ — এবার '>' চাই, কিন্তু কিছু বাকি নেই
greedy: এক ঘর ফেরত (backtrack)ফল
ঘর  : 0 1 2 3 4 5 6 7 8 9 10
লেখা: < b > b o l d < / b >
      .* ফেরত দিল ঘর 10-এর '>' → '>' মিলল ✔
মিল: '<b>bold</b>' — পুরোটা!
lazy: <.*?>.*? শুরুতে কিছুই খায় না
ঘর  : 0 1 2 3 4 5 6 7 8 9 10
লেখা: < b > b o l d < / b >
      .*? = '' ; এখন '>' চাই, ঘর 1-এ 'b' ✘
lazy: এক অক্ষর বাড়ালপ্রথম মিল
ঘর  : 0 1 2 3 4 5 6 7 8 9 10
লেখা: < b > b o l d < / b >
      .*? = 'b' ; ঘর 2-এ '>' ✔
প্রথম মিল: '<b>'
findall: ঘর 3 থেকে আবারদ্বিতীয় মিল
ঘর  : 0 1 2 3 4 5 6 7 8 9 10
লেখা: < b > b o l d < / b >
      ঘর 3–6 এ '<' নেই; ঘর 7 থেকে '</b>' ✔
ফল: ['<b>', '</b>']
মিলের অবস্থা
প্যাটার্ন
.* যা ধরে আছে
ফল
  1. < মিলল। প্যাটার্নের প্রথম অংশ সাধারণ অক্ষর <, লেখার ঘর 0-এ পাওয়া গেল।
  2. greedy .* সব খেয়ে নিল। . যেকোনো অক্ষর, * যতবার খুশি — তাই লোভী ইঞ্জিন ঘর 1 থেকে লেখার একেবারে শেষ পর্যন্ত সব খেয়ে ফেলে, শেষের >-সহ। এবার প্যাটার্নের শেষ অংশ > মেলাতে হবে, কিন্তু লেখায় আর কিছু বাকি নেই।
  3. ফেরত দেওয়া — backtracking. ইঞ্জিন হাল ছাড়ে না: .*-কে একটি অক্ষর ফেরত দিতে বলে। ফেরত আসে ঘর 10-এর >, আর সেটাই প্যাটার্নের > মেলায়। মিল সম্পূর্ণ — কিন্তু পুরো <b>bold</b> একটিমাত্র মিল। greedy “সবচেয়ে লম্বা সম্ভাব্য” মিল খোঁজে, আর মাঝখানের >-গুলো . দিব্যি খেয়ে ফেলেছে।
  4. lazy .*? — শুরুতে কিছুই না। এবার উল্টো কৌশল। lazy quantifier প্রথমে শূন্যটি অক্ষর নেয়, আর সঙ্গে সঙ্গে পরের অংশ > মেলানোর চেষ্টা করে। ঘর 1-এ b, মেলে না।
  5. এক এক করে বাড়ানো। lazy একটি অক্ষর বেশি নেয় (b), আবার > চেষ্টা করে — ঘর 2-এ >, মিলে গেল! প্রথম মিল <b>. lazy “সবচেয়ে ছোট সম্ভাব্য” মিল খোঁজে।
  6. findall চালিয়ে যায়। আগের মিল যেখানে শেষ, সেখান থেকে (ঘর 3) আবার খোঁজা শুরু। ঘর 7-এ <, তারপর /b, তারপর > — দ্বিতীয় মিল </b>. চূড়ান্ত ফল ['<b>', '</b>']. আরও ভালো উপায় প্রায়ই negated class: <[^>]*> — “> ছাড়া যেকোনো কিছু”, যাতে ফেরত দেওয়ার দরকারই পড়ে না। (তবে আসল HTML-এর জন্য regex নয় — অংশ ৮.)
.* → যত বেশি পারে (greedy)
.*? → যত কম পারে (lazy)
[^>]* → সীমা নিজেই বলে দেয়, প্রায়ই সেরা

বিকল্প | আর বন্ধনী

Alternation | মানে “এটা অথবা ওটা”: bKash|Nagad|Rocket. এর অগ্রাধিকার সবচেয়ে কম, তাই ^Tk|BDT$ মানে “শুরুতে Tk” অথবা “শেষে BDT” — সম্ভবত যা চেয়েছিলেন তা নয়। দলবদ্ধ করতে বন্ধনী দিন: ^(?:Tk|BDT)\s?\d+. আরও একটি চমক: Python-এর ইঞ্জিন বিকল্পগুলো বাম থেকে ক্রমানুসারে চেষ্টা করে, আর প্রথম যেটা মেলে সেটাই নেয় — সবচেয়ে লম্বাটা নয়। re.findall(r"a|ab", "ab") দেয় ['a']. তাই লম্বা বিকল্প আগে লিখুন: ab|a.

৫ · group, named group আর match object — মিলের ভেতর থেকে টুকরো তোলা

সাধারণ বন্ধনী ( ) দুটি কাজ করে: কয়েকটি অংশকে একসাথে বাঁধে (যাতে quantifier পুরোটার ওপর বসে, যেমন (ab)+), আর মেলা অংশটুকু আলাদা করে মনে রাখে। দ্বিতীয় কাজের জন্য একে বলে capturing group. শুধু বাঁধতে চাইলে, মনে রাখার দরকার না থাকলে, লিখুন (?:...) — non-capturing group.

মিল পাওয়া গেলে match object থেকে সব তথ্য তোলা যায়। m = re.search(r"(\d+)-(\d+)", "x 12-34 y") হলে:

  • m.group() বা m.group(0) — পুরো মিল: '12-34'
  • m.group(1), m.group(2) — প্রথম ও দ্বিতীয় বন্ধনীর মিল: '12', '34'
  • m.groups() — সব group একটি tuple-এ: ('12', '34')
  • m.span() — মিলটি লেখার কোন ঘর থেকে কোন ঘর: (2, 7); m.start(), m.end() আলাদা করেও পাওয়া যায়

group নম্বর দিয়ে ডাকা ভঙ্গুর — প্যাটার্নের শুরুতে একটি নতুন বন্ধনী যোগ করলেই সব নম্বর এক ঘর সরে যায়। এর সমাধান named group: (?P<amount>...). এখন m["amount"] বা m.group("amount") দিয়ে নাম ধরে ডাকা যায়, আর m.groupdict() সব নামসহ একটি dict দেয়। একটি bKash/Nagad-ধাঁচের SMS (কাল্পনিক) থেকে টাকার অঙ্ক আর transaction ID তোলা দেখুন:

1sms = "Received Tk 1,250.50 ... TrxID BK7Q2M9XA1"2m = TXN.search(sms)3trx = m["trx"]4amount = float(m["amount"].replace(",", ""))5print(m.groupdict(), amount)
নাম → মান (সবুজ লাইনটি এখন চলছে)
m
m["amount"]
trx
amount
  1. SMS-টি শুধু লেখা। প্রোগ্রামের কাছে এটি একটি লম্বা string; টাকার অঙ্ক কোথায় তা সে জানে না। আমাদের প্যাটার্ন TXN বলছে: Tk\s(?P<amount>[0-9,]+\.[0-9]{2}), তারপর .*?, তারপর TrxID\s(?P<trx>[A-Z0-9]{10}).
  2. search মিল খুঁজে পেল। “Tk ”-এর পর অঙ্ক আর কমা, একটি আসল ডট (\.), ঠিক দুই ঘর দশমিক — 1,250.50. তারপর lazy .*? যত কম পারে লেখা পার হয়ে প্রথম “TrxID ”-এ থামে। প্রতিটি named group তার অংশটুকু মনে রাখল। খেয়াল করুন: group-এর মান সবসময় str, সংখ্যা নয়।
  3. নাম ধরে ডাকা। m["trx"] দেয় 'BK7Q2M9XA1'. পরে কেউ প্যাটার্নের শুরুতে আরেকটি group যোগ করলেও এই লাইন ভাঙবে না — নম্বর বদলায়, নাম বদলায় না।
  4. লেখা থেকে সংখ্যা। হিসাব করতে হলে কমা মুছে সংখ্যায় রূপান্তর লাগে। এখানে float দেখানো হলো সরলতার জন্য; আসল টাকার হিসাবে পর্ব ০৭-এর Decimal ব্যবহার করুন, যাতে 0.1 + 0.2-এর মতো ফাঁদ না থাকে।
  5. groupdict(). সব named group একসাথে একটি dict-এ — সরাসরি JSON-এ (পর্ব ২৬) বা ডেটাবেসে পাঠানোর উপযোগী। regex-এর কাজ এখানেই শেষ: লেখা থেকে কাঠামোবদ্ধ ডেটা। এরপর যাচাই (অঙ্কটি কি যুক্তিসঙ্গত?) আপনার সাধারণ Python কোডের কাজ।

re.sub — group দিয়ে আর ফাংশন দিয়ে বদলানো

re.sub-এর বদলি লেখার ভেতরে \1, \2 বা \g<name> লিখে মেলা group ফিরিয়ে আনা যায়। তারিখের ক্রম উল্টাতে: re.sub(r"(\d{2})/(\d{2})/(\d{4})", r"\3-\2-\1", "17/10/2026") দেয় '2026-10-17'. (মনে রাখুন, বদলি লেখাটিও raw string — না হলে "\1" হয়ে যায় একটি অদৃশ্য অক্ষর।)

বদলির নিয়ম আরও জটিল হলে লেখার জায়গায় একটি ফাংশন দিন। re প্রতিটি মিলের match object দিয়ে ফাংশনটি ডাকবে, আর ফাংশন যা ফেরত দেবে সেটাই বসবে: re.sub(r"\d+", lambda m: str(int(m.group()) * 2), "3 আম 10 কলা") দেয় '6 আম 20 কলা'. ফাংশন এখানে একটি সাধারণ মান হিসেবে পাঠানো হয়েছে — Python-এ ফাংশনও object (পর্ব ১৩), আর lambda-র বিস্তারিত পর্ব ৩৮-এ। ফোন নম্বর ঢাকার উদাহরণ পরের অংশের কোডে।

Flag — প্যাটার্নের আচরণ বদলানো

পুরো প্যাটার্নের আচরণ বদলানোর সুইচগুলোকে বলে flag. এগুলো দেওয়া হয় flags= argument-এ, বা re.compile-এর দ্বিতীয় argument হিসেবে:

Flagকাজউদাহরণ
re.IGNORECASE (re.I)বড়-ছোট হাত না মেনে মেলানোre.findall(r"tk", "Tk TK tk", re.I) → তিনটিই
re.MULTILINE (re.M)^ আর $ প্রতিটি লাইনের শুরু-শেষে মেলেলগ ফাইলে প্রতিটি লাইনের শুরুর তারিখ
re.DOTALL (re.S). newline-ও মেলায়কয়েক লাইন জুড়ে থাকা অংশ
re.ASCII (re.A)\d \w \s \b শুধু ASCII নিয়মেশুধু 0–9 অঙ্ক চাইলে
re.VERBOSE (re.X)প্যাটার্নে ফাঁকা জায়গা আর # মন্তব্য লেখা যায়লম্বা প্যাটার্ন পড়ার উপযোগী করা

কয়েকটি flag একসাথে দিতে | দিয়ে জুড়ুন: re.I | re.M. আর একটি নতুন নিয়ম: Python 3.13 থেকে re.sub, re.subn আর re.split-এ count, maxsplit বা flags অবস্থান ধরে (positional) দেওয়া deprecated — re.sub(p, r, s, 1) লিখলে DeprecationWarning আসে। নাম দিয়ে লিখুন: count=1, flags=re.I. পুরোনো টিউটোরিয়াল আর AI-এর কোডে এই পুরোনো রূপ প্রায়ই দেখবেন।

মূল ধারণা

re.VERBOSE লম্বা regex-এর ওষুধ. এই flag দিলে প্যাটার্নের ভেতরের ফাঁকা জায়গা উপেক্ষা হয়, আর #-এর পরের অংশ মন্তব্য। ফলে প্যাটার্নটি কয়েক লাইনে ভেঙে প্রতিটি অংশের পাশে বাংলায় লিখে রাখা যায় “এটা কী করে”। ছয় মাস পরে আপনি নিজে, বা আপনার দলের কেউ, ঠিক এই মন্তব্যগুলোর জন্যই কৃতজ্ঞ থাকবেন। (আসল space মেলাতে VERBOSE-এ লিখুন \s বা \ .)

৬ · বাস্তব যাচাই — মোবাইল নম্বর, ইমেইল, তারিখ আর SMS

বাংলাদেশি মোবাইল নম্বর

একটি সাইন-আপ ফর্মে মোবাইল নম্বর নিচ্ছেন। মানুষ লিখবে 01712345678, কেউ +8801712345678, কেউ 8801712345678. লেখার সময় বাংলাদেশে মোবাইল অপারেটরের কোড 013 থেকে 019 (013 ও 017 গ্রামীণফোন, 014 ও 019 বাংলালিংক, 015 টেলিটক, 016 এয়ারটেল, 018 রবি) — এগুলো সময়ের সাথে বদলাতে পারে, তাই প্যাটার্নটি এক জায়গায় নাম দিয়ে রাখা জরুরি। এটি input validation-এর একটি ক্লাসিক উদাহরণ। নিয়মটি বাংলায়: “ঐচ্ছিকভাবে +88 বা 88, তারপর 01, তারপর 3–9-এর একটি অঙ্ক, তারপর ঠিক ৮টি ইংরেজি অঙ্ক — আর কিছু না”। regex-এ: (?:\+?88)?01[3-9][0-9]{8}, আর ব্যবহার fullmatch দিয়ে।

import re

BD_MOBILE = re.compile(r"(?:\+?88)?01[3-9][0-9]{8}")

tests = ["01712345678", "+8801812345678", "8801912345678",
         "01212345678", "0171234567", "017123456789", "০১৭১২৩৪৫৬৭৮"]
for t in tests:
    ok = BD_MOBILE.fullmatch(t) is not None
    print(f"{t:<15} {'ঠিক' if ok else 'ভুল'}")

text = "কল করুন 017123456789 নম্বরে"
print(re.search(r"01[3-9]\d{8}", text))
print(re.fullmatch(r"01[3-9]\d{8}", "017123456789"))
print(re.fullmatch(r"\d{11}", "০১৭১২৩৪৫৬৭৮") is not None)
print(re.fullmatch(r"\d{11}", "০১৭১২৩৪৫৬৭৮", flags=re.ASCII))

def mask(m):
    return "*" * len(m.group(1)) + m.group(2)

print(re.sub(r"^(\d+)(\d{3})$", mask, "01712345678"))

প্রথম তিনটি নম্বর (দেশের কোডসহ ও ছাড়া) ঠিক. 01212345678 ভুল, কারণ 012 অপারেটর কোড নয়; 0171234567 (১০ অঙ্ক) আর 017123456789 (১২ অঙ্ক) ভুল — fullmatch বাড়তি বা কম অঙ্ক সহ্য করে না। বাংলা অঙ্কের নম্বরও ভুল, কারণ প্যাটার্নে আমরা ইচ্ছা করে [0-9] লিখেছি। এরপর আসল শিক্ষা: re.search ১২ অঙ্কের লেখার ভেতর থেকে <re.Match object; span=(8, 19), match='01712345678'> খুঁজে দিয়েছে — খোঁজার জন্য ঠিক, যাচাইয়ের জন্য ভয়ংকর। একই লেখায় fullmatch দেয় None. তারপর প্রমাণ যে \d বাংলা অঙ্কও মেলায় (True), আর re.ASCII দিলে মেলায় না (None). শেষ লাইনে re.sub-কে একটি ফাংশন দেওয়া হয়েছে: প্রথম group (শেষ তিনটি বাদে সব অঙ্ক) যত লম্বা, তত *, তারপর দ্বিতীয় group অপরিবর্তিত — ফল ********678, রসিদ বা স্ক্রিনে নম্বর আংশিক দেখানোর জন্য।

regex-এর আগে পরিষ্কার করুন। মানুষ লেখে 017-1234-5678 বা 01712 345678. একটি বিশাল regex দিয়ে সব রূপ ধরার চেষ্টা না করে আগে space আর ড্যাশ মুছে ফেলুন (re.sub(r"[\s-]", "", s) বা পর্ব ০৯-এর replace), তারপর ছোট, পরিষ্কার প্যাটার্নে যাচাই করুন। আর মনে রাখুন: regex শুধু বলে নম্বরটা দেখতে ঠিক; নম্বরটা আসলেই ব্যবহারকারীর কি না, তা জানার একমাত্র উপায় OTP পাঠিয়ে যাচাই।

ইমেইল — নিখুঁত regex একটি ফাঁদ

ইন্টারনেটে “perfect email regex” খুঁজলে কয়েকশো অক্ষরের দানব পাবেন — Perl-এর একটি পুরোনো module-এ ইমেইলের মান (RFC 822) মেনে লেখা regex কয়েক হাজার অক্ষর লম্বা! কারণ ইমেইলের অফিসিয়াল নিয়ম অবিশ্বাস্য রকম উদার: কোটের ভেতরে space, মন্তব্য, অদ্ভুত চিহ্ন, এমনকি এখন বাংলা অক্ষরের ডোমেইনও বৈধ। বাস্তব পরামর্শ: শুধু একটি সহজ আকৃতি-যাচাই করুন — “@-এর আগে কিছু, পরে কিছু, তারপর একটি ডট আর ডোমেইন” — যেমন [^@\s]+@[^@\s]+\.[A-Za-z]{2,}, fullmatch দিয়ে। ঠিকানাটা আসল কি না, তা জানার একমাত্র নির্ভরযোগ্য উপায় সেখানে একটি যাচাই-লিংক পাঠানো।

তারিখ — regex আকৃতি দেখে, datetime অর্থ দেখে

\d{2}/\d{2}/\d{4} বলে লেখাটা dd/mm/yyyy-এর মতো দেখতে। কিন্তু 31/02/2026-ও এই আকৃতি পাস করে, অথচ ফেব্রুয়ারিতে ৩১ তারিখ নেই। regex দিয়ে মাসভেদে দিনের সংখ্যা আর লিপ ইয়ার যাচাই করা সম্ভব, কিন্তু সেই প্যাটার্ন কেউ পড়তে পারবে না। সঠিক ভাগাভাগি: regex দিয়ে আকৃতি, তারপর পর্ব ২৭-এর datetime.strptime(s, "%d/%m/%Y") দিয়ে অর্থ — অবৈধ তারিখে এটি ValueError দেয় (Python 3.14-এ বার্তাটি এমন: day 31 must be in range 1..28 for month 2 in year 2026).

SMS থেকে টাকা — finditer আর VERBOSE

অংশ ৫-এর stepper-এ একটি SMS থেকে টাকার অঙ্ক তোলা দেখেছেন। বাস্তবে একটি ফাইলে শত শত SMS থাকে, আর প্রতিটির জন্য একটি করে মিল চাই — এখানে finditer সেরা, কারণ প্রতিটি মিলের পুরো match object দেয়, named group-সহ। পুরো উদাহরণটি অংশ ৭-এর শেষের কোডে, বাংলা লেখার কয়েকটি পরীক্ষার সাথে।

৭ · বাংলা লেখায় regex — \w আর \b-এর ফাঁদ

পর্ব ০৯-এ দেখেছেন, বাংলা লেখায় চোখের একটি অক্ষর কয়েকটি code point হতে পারে: “কি” = ক + ি, “ক্ষ” = ক + হসন্ত + ষ. regex ইঞ্জিনও code point ধরে চলে — আর এখানেই বাংলার জন্য বড় ফাঁদ।

Python-এর ডকুমেন্টেশন অনুযায়ী \w মেলায় এমন অক্ষর, যার জন্য str.isalnum() সত্য, আর _. বাংলা ব্যঞ্জন আর স্বরবর্ণ (ক, আ) এর মধ্যে পড়ে। কিন্তু কার-চিহ্ন (া ি ী ু…), হসন্ত, অনুস্বার, চন্দ্রবিন্দু — Unicode-এর হিসাবে এরা অক্ষর নয়, combining mark. ফলে:

>>> re.findall(r"\w+", "আমি কাল ঢাকা যাব")
['আম', 'ক', 'ল', 'ঢ', 'ক', 'য', 'ব']

প্রতিটি কার-চিহ্নে শব্দ ভেঙে গেছে! ইংরেজিতে যে প্যাটার্ন নিখুঁত শব্দ গোনে, বাংলায় সেটা আবর্জনা দেয়। একই কারণে \b-ও বাংলায় ভুল জায়গায় বসে। \b হলো \w আর অ-\w-এর মাঝের সীমানা; “কি”-এর ক আর ি-এর মাঝেই একটি সীমানা তৈরি হয়ে যায়। পরীক্ষা করে দেখা গেছে: re.findall(r"\bকি\b", "কি খবর, কিন্তু") একটি 'কি' দেয় — কিন্তু সেটা “কিন্তু”-র ভেতরের, ঘর 8 থেকে! আলাদা শব্দ “কি”-টি বাদ পড়ে, কারণ ি আর পরের space দুটোই অ-\w, তাদের মাঝে কোনো সীমানা নেই। আর “কিন্তু”-তে ি-এর পর ন আসে, যা \w — সেখানে সীমানা আছে। ঠিক উল্টো ফল।

বাংলা শব্দ খুঁজতে \w বা \b-এর ওপর ভরসা করবেন না। নিরাপদ উপায় হলো বাংলা Unicode খণ্ডটি সরাসরি লেখা: [\u0980-\u09FF]+ — U+0980 থেকে U+09FF পর্যন্ত সব code point, কার-চিহ্ন আর হসন্তসহ। (re নিজেই প্যাটার্নের ভেতরের \u0980 বোঝে, তাই raw string-এও এটি কাজ করে।) মনে রাখুন এই খণ্ডে বাংলা অঙ্ক আর ৳-ও আছে; আর “র‍্যাব”-এর মতো লেখায় থাকা অদৃশ্য ZWJ (U+200D) এই খণ্ডের বাইরে, তাই দরকার হলে সেটাও class-এ যোগ করুন। শব্দের সীমানার জন্য \b-এর বদলে স্পষ্ট শর্ত দিন — যেমন আগে-পরে space বা লেখার শুরু-শেষ।

আরেকটি বিষয় পর্ব ০৯ থেকে মনে রাখুন: একই দেখতে লেখা ভিন্ন code point-এ থাকতে পারে (যেমন “ড়” একক U+09DC, অথবা ড + নুক্তা)। আপনার প্যাটার্নে এক রূপ, ইনপুটে আরেক রূপ থাকলে মিলবে না। তাই বাইরে থেকে আসা বাংলা লেখায় regex চালানোর আগে normalization (unicodedata.normalize("NFC", s)) করে নিন। আর grapheme cluster ধরে কাজ করতে (চোখের অক্ষর ধরে গোনা বা কাটা) standard library-র re যথেষ্ট নয়; তৃতীয় পক্ষের regex package-এর \X প্যাটার্ন এই কাজ করে।

এবার সব একসাথে — VERBOSE প্যাটার্নে দুটি কাল্পনিক SMS থেকে TrxID ও টাকা, তারপর বাংলা লেখায় \w বনাম Unicode খণ্ড, একাধিক চিহ্নে ভাঙা, বাড়তি ফাঁকা পরিষ্কার আর তারিখের ক্রম বদল:

import re

sms = """You have received Tk 1,250.50 from 01712345678. TrxID BK7Q2M9XA1 at 17/10/2026 10:15
Cash Out Tk 500.00 successful. Fee Tk 9.25. TrxID NG4H8T2ZP0 at 17/10/2026 18:40"""

TXN = re.compile(r"""
    Tk\s(?P<amount>[0-9,]+\.[0-9]{2})   # টাকার অঙ্ক, কমাসহ
    .*?                                 # মাঝের লেখা — যত কম পারা যায়
    TrxID\s(?P<trx>[A-Z0-9]{10})        # ১০ ঘরের transaction ID
""", re.VERBOSE)

for m in TXN.finditer(sms):
    amount = float(m["amount"].replace(",", ""))
    print(m["trx"], amount)

print(re.findall(r"\w+", "আমি কাল ঢাকা যাব"))
print(re.findall(r"[\u0980-\u09FF]+", "আমি কাল ঢাকা যাব"))
print(re.split(r"\s*[,;|]\s*", "আম, কলা;লিচু | জাম"))
print(re.sub(r"\s+", " ", "  অনেক   ফাঁকা \t জায়গা  ").strip())
print(re.sub(r"(\d{2})/(\d{2})/(\d{4})", r"\3-\2-\1", "তারিখ 17/10/2026"))

প্রথম দুই লাইনের আউটপুট BK7Q2M9XA1 1250.5 আর NG4H8T2ZP0 500.0. দ্বিতীয় SMS-এ দুটি “Tk” আছে (500.00 আর ফি 9.25); প্যাটার্ন প্রথমটি থেকে শুরু করে lazy .*? দিয়ে TrxID পর্যন্ত গেছে, তাই আসল লেনদেনের অঙ্ক 500.0 এসেছে। আর . newline মেলায় না বলে একটি মিল কখনো এক SMS-এর লাইন পেরিয়ে আরেকটিতে যায়নি। VERBOSE-এর কারণে প্যাটার্নের ফাঁকা জায়গা আর #-এর পরের বাংলা মন্তব্য উপেক্ষা হয়েছে। এরপর বাংলার ফাঁদ: \w+ দেয় ['আম', 'ক', 'ল', 'ঢ', 'ক', 'য', 'ব'], আর Unicode খণ্ড দিয়ে ঠিকঠাক ['আমি', 'কাল', 'ঢাকা', 'যাব']. re.split কমা, সেমিকোলন বা খাড়া দাগ — যেকোনোটিতে, আশপাশের ফাঁকাসহ — ভেঙে দেয় ['আম', 'কলা', 'লিচু', 'জাম']; একাধিক ভিন্ন চিহ্নে ভাঙা str.split পারে না, এখানে regex সত্যিই কাজের। \s+ সব রকম ফাঁকা (tab-সহ) একটি space-এ নামিয়ে আনে: অনেক ফাঁকা জায়গা. (শুধু এই কাজের জন্য অবশ্য " ".join(s.split())-ও সমান ভালো।) শেষে group reference দিয়ে তারিখ 2026-10-17.

C/C++ জানলে

C-র standard library-তে regex নেই; Unix-জাতীয় সিস্টেমে POSIX-এর <regex.h> (regcomp, regexec) ব্যবহার হয়, যা C মানের অংশ নয়। C++11 থেকে <regex>-এ std::regex আছে, কিন্তু অনেক implementation-এ এটি ধীর বলে পরিচিত, তাই বাস্তবে অনেকে RE2 বা PCRE2-এর মতো library নেয়। Python-এর re C++-এর std::regex-এর মতোই একটি backtracking ইঞ্জিন, কিন্তু str-এর ওপর Unicode-সচেতন — যেখানে C-তে লেখা মানে বাইট (C কোর্সের string পর্ব), আর C++-এ লেখা নিয়ে কাজের মূল ভিত্তি string ও string_view পর্ব।

৮ · নিরাপত্তা আর কখন regex নয় — catastrophic backtracking

অংশ ৪-এ দেখেছেন, Python-এর ইঞ্জিন মেলাতে না পারলে পেছনে ফিরে অন্য উপায়ে চেষ্টা করে (backtracking). বেশিরভাগ প্যাটার্নে এই ফিরে যাওয়া সামান্য। কিন্তু কিছু প্যাটার্নে একই লেখা মেলানোর অসংখ্য উপায় থাকে, আর মিল শেষ পর্যন্ত ব্যর্থ হলে ইঞ্জিন প্রতিটি উপায় চেষ্টা করে দেখে। একে বলে catastrophic backtracking. সবচেয়ে ছোট উদাহরণ: (a+)+$ — “এক বা একাধিক a-এর দল, এমন এক বা একাধিক দল, তারপর শেষ”। লেখা aaaa…a! হলে (শেষে একটি !, তাই মিল অসম্ভব) কী ঘটে, দেখুন — শুধু হিসাব, চালানো নয়:

লেখা: aaaa!n = 4
৪টি a-কে দলে ভাগ করার উপায়:
(aaaa)  (aaa)(a)  (a)(aaa)  (aa)(aa)
(aa)(a)(a)  (a)(aa)(a)  (a)(a)(aa)  (a)(a)(a)(a)
প্রতিটির শেষে '!' — $ মেলে না ✘
n = 10নিয়ম: 2^(n−1)
প্রতি নতুন a: আগের দলে জুড়বে, নাকি নতুন দল?
→ প্রতিবার ২টি পথ, উপায় দ্বিগুণ
n = 10 → 2^9 = 512 উপায়
n = 20এখনো চোখে পড়ে না
n = 20 → 2^19 = 524,288 উপায়
প্রায় সোয়া পাঁচ লাখ চেষ্টা — একটিমাত্র ছোট লেখায়
n = 30সার্ভার আটকে গেল
n = 30 → 2^29 = 536,870,912 উপায়
প্রায় ৫৪ কোটি — একটি ৩১ অক্ষরের ইনপুটে!
সমাধান: a+$একই অর্থ, একটি পথ
a+$ : a-গুলো ভাগ করার একটিমাত্র উপায়
n = 30 → মোটামুটি n-এর অনুপাতে ধাপ
নিয়ম: quantifier-এর ভেতরে quantifier নয়
ব্যর্থ হওয়ার আগে চেষ্টার সংখ্যা (শুরুর ঘর 0 থেকেই)
a-এর সংখ্যা
(a+)+$
a+$
  1. চারটি a, আটটি উপায়। ভেতরের a+ কয়েকটি a খায়, বাইরের + সেই দল কয়েকবার নেয়। ফলে aaaa-কে দলে ভাগ করার ৮টি উপায় আছে। শেষে ! থাকায় $ কখনো মেলে না, আর ইঞ্জিন “ব্যর্থ” বলার আগে প্রতিটি উপায় চেষ্টা করে।
  2. প্রতি a-তে দ্বিগুণ। প্রতিটি নতুন a হয় আগের দলের সাথে জুড়বে, নয়তো নতুন দল শুরু করবে — দুটি পথ। তাই উপায়ের সংখ্যা 2^(n−1): দশটি a-তে 512.
  3. ২০টি a — সোয়া পাঁচ লাখ। এখনো এক সেকেন্ডের কম লাগতে পারে, তাই টেস্টে কেউ টের পায় না। এটাই বিপদ: ছোট টেস্ট ইনপুটে প্যাটার্নটা নিরীহ দেখায়।
  4. ৩০টি a — প্রায় ৫৪ কোটি। আর এটা শুধু শুরুর ঘর 0-এর হিসাব; search প্রতিটি শুরুর ঘর থেকে আবার চেষ্টা করে। প্রতিটি a যোগ করলে সময় দ্বিগুণ — কয়েকটি অক্ষর বাড়ালেই সেকেন্ড থেকে মিনিট, ঘণ্টা। কেউ ইচ্ছা করে এমন ইনপুট পাঠিয়ে সার্ভার আটকে দিলে তাকে বলে ReDoS (Regular expression Denial of Service). এই কোড কোনো runnable উদাহরণে রাখা হয়নি — লম্বা ইনপুটে চালালে আপনার কম্পিউটারও আটকে যাবে।
  5. সমাধান: একই অর্থ, একটিমাত্র পথ। (a+)+ আর a+ হুবহু একই লেখা মেলায়, কিন্তু দ্বিতীয়টিতে ভাগ করার একটিই উপায়। মূল নিয়ম: quantifier-এর ভেতরে quantifier ((x+)+, (x*)*, (.*)*) বা একই লেখা মেলাতে পারে এমন বিকল্প ((a|a)+) এড়িয়ে চলুন. Python 3.11 থেকে re-তে atomic group (?>...) আর possessive quantifier (a++) এসেছে, যা ফেরত দেওয়াই বন্ধ করে — তবে আগে প্যাটার্নটাই সরল করুন।

প্রতিরক্ষার নিয়ম

  • সরল প্যাটার্ন: nested quantifier নয়; .*-এর বদলে যতটা সম্ভব নির্দিষ্ট class ([^,]*, [0-9]{8}).
  • Anchor দিন: fullmatch বা ^...$ ইঞ্জিনকে অসংখ্য শুরুর ঘর থেকে চেষ্টা করা থেকে বাঁচায়।
  • ইনপুটের দৈর্ঘ্য সীমিত করুন: regex চালানোর আগে if len(s) > 100: reject. একটি মোবাইল নম্বর ২০ অক্ষরের বেশি হওয়ার কোনো কারণ নেই।
  • ব্যবহারকারীর লেখা দিয়ে প্যাটার্ন বানাবেন না: কারও সার্চ-বক্সের লেখা সরাসরি প্যাটার্নে বসালে সে নিজেই বিপজ্জনক প্যাটার্ন পাঠাতে পারে। আক্ষরিক খোঁজার জন্য re.escape(text), বা আরও সহজে text in s.
  • বিশ্বাস নেই এমন ইনপুটে বড় সিস্টেম: সেখানে RE2-র মতো linear-time ইঞ্জিন (ভাবনার প্রশ্ন ০৫) বিবেচনা করুন।

কখন regex নয়

কাজregex দিয়ে (এড়িয়ে চলুন)ভালো উপায়
শব্দটি আছে কি নাre.search("OTP", sms)"OTP" in sms
শুরু বা শেষ যাচাইre.match(r"017", p)p.startswith(("013", "017"))
এক চিহ্নে ভাঙাre.split(",", s)s.split(",")
HTML থেকে লিংক বা লেখা<a href="(.*?)">html.parser বা BeautifulSoup-এর মতো parser
JSON বা CSV পড়াকমা ধরে regexjson, csv module (পর্ব ২৬)
তারিখের অর্থ যাচাইদিন-মাস-লিপ ইয়ারের দানব regexdatetime.strptime (পর্ব ২৭)

HTML কেন regex দিয়ে নয়? কারণ HTML-এর ট্যাগ একটির ভেতরে আরেকটি যত গভীরে খুশি থাকতে পারে, attribute-এর ক্রম বদলাতে পারে, কোট এক বা জোড়া হতে পারে, মন্তব্যের ভেতরে নকল ট্যাগ থাকতে পারে। গণিতের দিক থেকেও, আসল “regular” expression ইচ্ছামতো গভীর বাসা-বাঁধা কাঠামো (যেমন মেলানো বন্ধনী) চিনতে পারে না — এটা theory of computation-এর একটি প্রমাণিত সীমা। parser এই কাজের জন্যই তৈরি।

regex লেখা আর পরীক্ষার অভ্যাস

regex101.com-এর মতো অনলাইন টেস্টারে প্যাটার্ন বসিয়ে কয়েকটি উদাহরণ লেখা দিলে সেটি প্রতিটি অংশের ব্যাখ্যা দেখায়, কোন group কী ধরল তা রঙ দিয়ে দেখায়। সেখানে “Python” flavor বেছে নিতে ভুলবেন না — বিভিন্ন ভাষার regex-এ ছোটখাটো পার্থক্য আছে। তবে টেস্টারে পাস করাই শেষ কথা নয়: আসল যাচাই হলো আপনার কোডে একগুচ্ছ “ঠিক” আর “ভুল” উদাহরণের টেস্ট, যা প্রতিবার প্যাটার্ন বদলালে চলবে (pytest শিখবেন পর্ব ৩৯-এ)। আর প্রতিটি অ-সরল প্যাটার্ন re.VERBOSE-এ মন্তব্যসহ লিখুন।

পরীক্ষা ও ইন্টারভিউতে
  • re.match, re.search আর re.fullmatch-এর পার্থক্য? — match শুধু শুরু থেকে, search যেকোনো জায়গায় প্রথম মিল, fullmatch পুরো লেখা; যাচাইয়ে fullmatch.
  • greedy আর lazy quantifier কী? — .* যত বেশি সম্ভব মেলায়, .*? যত কম সম্ভব; "<b>bold</b>"-এ প্রথমটি পুরো লেখা, দ্বিতীয়টি শুধু <b>.
  • regex-এ raw string কেন? — যাতে Python নিজে backslash ব্যাখ্যা না করে; "\b" সাধারণ string-এ backspace অক্ষর।
  • group থাকলে re.findall কী দেয়? — পুরো মিল নয়, group-গুলো: একটি group হলে str-এর list, একাধিক হলে tuple-এর list.
  • ReDoS কী, কীভাবে এড়াবেন? — nested quantifier-এর কারণে exponential backtracking দিয়ে সার্ভার আটকানো; সরল ও anchored প্যাটার্ন, ইনপুটের দৈর্ঘ্য সীমা।

৯ · AI-এর যুগে — AI-এর লেখা regex যাচাই

regex এমন একটি জায়গা, যেখানে AI সবচেয়ে আত্মবিশ্বাসী আর সবচেয়ে বিপজ্জনক দুটোই। কয়েক সেকেন্ডে সে একটি লম্বা প্যাটার্ন দেয়, যা দেখতে পেশাদার — কিন্তু কেউ পড়ে যাচাই করতে পারে না, তাই ভুলগুলো চুপচাপ প্রোডাকশনে চলে যায়। নিচের ভুলগুলো বাস্তবে বারবার দেখা যায়:

AI প্রায়ই যা লেখেসমস্যাভালো উপায়
যাচাইয়ে re.search বা re.matchবাড়তি অক্ষরসহ ভুল ইনপুটও পাসre.fullmatch
“শুধু অঙ্ক” বোঝাতে \dবাংলা ও অন্য লিপির অঙ্কও পাস[0-9] বা re.ASCII
বাংলা শব্দে \w+ বা \bকার-চিহ্নে শব্দ ভাঙে, সীমানা ভুল জায়গায়[\u0980-\u09FF]+, বাংলা উদাহরণে টেস্ট
এক লাইনের ২০০ অক্ষরের প্যাটার্নকেউ পড়তে বা বদলাতে পারে নাre.VERBOSE আর মন্তব্য, বা কয়েক ধাপে ভাগ
HTML বা JSON regex দিয়ে পার্সবাসা-বাঁধা কাঠামোয় ভাঙেparser, json module
(\w+\s?)+$-এর মতো nested quantifierলম্বা ইনপুটে ReDoSসরল প্যাটার্ন, দৈর্ঘ্য সীমা
সাধারণ string-এ প্যাটার্ন ("\bword")\b backspace হয়ে যায়সবসময় r"..."
re.sub(p, r, s, 1)Python 3.13 থেকে DeprecationWarningcount=1 নাম দিয়ে

AI-কে regex চাইলে শুধু “একটি regex দাও” নয় — ব্যাখ্যা, টেস্ট আর বিকল্প চেয়ে নিন। নিচের কাঠামোটি কপি করে ব্যবহার করুন:

আমি Python 3.14-এর re module ব্যবহার করি। একটি সাইন-আপ ফর্মে বাংলাদেশি মোবাইল নম্বর যাচাই করতে হবে (01 দিয়ে শুরু ১১ অঙ্ক, ঐচ্ছিক +88 বা 88). শর্ত: ১. আগে বলো, regex ছাড়া সাধারণ string method দিয়ে কাজটা সহজে হয় কি না। ২. প্যাটার্নটি re.VERBOSE-এ লেখো, প্রতিটি অংশের পাশে মন্তব্যসহ, আর raw string-এ। ৩. যাচাইয়ে re.fullmatch ব্যবহার করো; search বা match কেন নয়, এক লাইনে বলো। ৪. শুধু ইংরেজি অঙ্ক চাই — \d ব্যবহার করলে বলো Python 3-এ সেটা বাংলা অঙ্কেও মেলে কি না। ৫. nested quantifier আছে কি না দেখো, আর ইনপুটের দৈর্ঘ্যের সীমা রাখো। ৬. অন্তত ৫টি ঠিক আর ৫টি ভুল উদাহরণ দাও (১০ ও ১২ অঙ্ক, 012, বাংলা অঙ্ক, ভেতরে space), প্রতিটির প্রত্যাশিত ফলসহ।

AI-এর regex পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১স্নায়ুকোষের গণিত থেকে grep — regular expression কীভাবে তত্ত্ব থেকে প্রতিদিনের যন্ত্র হলো?

গল্পের শুরু ১৯৪৩ সালে, যখন Warren McCulloch আর Walter Pitts মস্তিষ্কের স্নায়ুকোষকে একটি সরল গাণিতিক মডেলে বর্ণনা করেন — প্রতিটি কোষ হয় জ্বলে, নয় নেভে। প্রশ্ন উঠল: এমন কোষের জাল কোন ধরনের ঘটনার ধারা (sequence of events) “চিনতে” পারে? গণিতবিদ Stephen Kleene এর উত্তর দেন একটি গবেষণাপত্রে, যা ১৯৫১ সালে RAND Corporation-এর একটি অভ্যন্তরীণ প্রতিবেদন হিসেবে লেখা হয় আর ১৯৫৬ সালে “Automata Studies” বইয়ে প্রকাশিত হয়। তিনি দেখান, এই জালগুলো ঠিক সেই ধারাগুলো চিনতে পারে, যেগুলো তিনটি সরল ক্রিয়া দিয়ে বানানো যায়: পাশাপাশি বসানো, “এটা অথবা ওটা”, আর “যতবার খুশি পুনরাবৃত্তি”। এগুলোর নাম দেন “regular events”, আর পুনরাবৃত্তির চিহ্ন * আজও “Kleene star” নামে পরিচিত। এই তত্ত্ব থেকে finite automaton-এর তত্ত্ব বিকশিত হয় — সীমিত সংখ্যক অবস্থা নিয়ে চলা একটি যন্ত্র, যা প্রতিটি অক্ষর পড়ে এক অবস্থা থেকে আরেক অবস্থায় যায়।

তত্ত্বকে যন্ত্রে নিয়ে আসেন Ken Thompson. ১৯৬৮ সালে Communications of the ACM পত্রিকায় তাঁর “Regular Expression Search Algorithm” প্রবন্ধে তিনি দেখান কীভাবে একটি regex-কে সরাসরি কম্পিউটারের যন্ত্রভাষায় (IBM 7094) রূপান্তর করে খুব দ্রুত খোঁজা যায় — একসাথে সম্ভাব্য সব অবস্থা অনুসরণ করে, পেছনে ফেরা ছাড়াই। তিনি এটি ব্যবহার করেন MIT-এর CTSS সিস্টেমে QED নামের টেক্সট এডিটরের নিজের সংস্করণে। পরে Bell Labs-এ Unix তৈরির সময় তাঁর লেখা এডিটর ed-এ regex রাখা হয়। ed-এ একটি কমান্ড ছিল g/re/p — “global, regular expression, print”, মানে যেসব লাইনে প্যাটার্ন মেলে সেগুলো দেখাও। এই কাজটি এত দরকারি ছিল যে Thompson এটিকে আলাদা একটি প্রোগ্রাম বানান, নাম হয় grep (১৯৭০-এর দশকের শুরুতে)। ইংরেজিতে “to grep” এখন একটি ক্রিয়াপদ।

এরপর sed, awk, vi, Perl, আর শেষে প্রায় সব প্রোগ্রামিং ভাষা regex গ্রহণ করে। মজার বিষয়, পথে আসল তত্ত্ব থেকে অনেকটা সরে আসা হয়েছে — আজকের অনেক “regex” ফিচার (যেমন backreference) গাণিতিক অর্থে আর “regular” নয়। এটি theory of computation-এর একটি কেন্দ্রীয় বিষয়: কোন ভাষা finite automaton চিনতে পারে, কোনটা পারে না (যেমন মেলানো বন্ধনী), আর কেন।

মূল উপলব্ধি: একটি বিমূর্ত গাণিতিক প্রশ্ন — “স্নায়ুকোষের জাল কী চিনতে পারে?” — দুই দশক পর প্রতিটি প্রোগ্রামারের হাতের রোজকার যন্ত্র হয়েছে। তত্ত্ব আর প্রকৌশলের মিলনেই সবচেয়ে টেকসই যন্ত্রগুলো জন্মায়, আর regex-এর তত্ত্ব জানলে বোঝা যায় এটি কী পারে, আর কী কখনোই পারবে না।

প্র ০২“Now they have two problems” — Jamie Zawinski-র বিখ্যাত উক্তিটি কী বোঝায়? regex কি তাহলে খারাপ?

১৯৯৭ সালে Netscape-এর প্রোগ্রামার Jamie Zawinski Usenet-এর একটি আলোচনায় লেখেন: “Some people, when confronted with a problem, think ‘I know, I’ll use regular expressions.’ Now they have two problems.” — কিছু মানুষ সমস্যা দেখলেই ভাবে “regex দিয়ে করে ফেলি”, আর এখন তাদের সমস্যা দুটি। উক্তিটি প্রোগ্রামিং জগতের সবচেয়ে বেশি উদ্ধৃত রসিকতাগুলোর একটি হয়ে যায়। Jeffrey Friedl (“Mastering Regular Expressions” বইয়ের লেখক) পরে উক্তিটির উৎস খুঁজে দেখান যে এর আগেও একই ছাঁচের রসিকতা অন্য বিষয় নিয়ে প্রচলিত ছিল।

“দ্বিতীয় সমস্যা” আসলে কী? প্রথমত, পড়ার অযোগ্যতা: regex এত ঘন যে লেখার সময় যা স্পষ্ট মনে হয়, এক সপ্তাহ পরে তা ধাঁধা। কোড পড়া হয় লেখার চেয়ে অনেক বেশিবার, আর যে কোড পড়া যায় না তা নিরাপদে বদলানো যায় না। দ্বিতীয়ত, লুকানো কিনারা: একটি প্যাটার্ন দশটি উদাহরণে ঠিক কাজ করে, এগারোতমটিতে চুপচাপ ভুল — কোনো error ছাড়াই, ঠিক যেমন \d বাংলা অঙ্ক মেনে নেয় বা search বাড়তি অক্ষর মেনে নেয়। তৃতীয়ত, ভুল যন্ত্র: HTML, ইমেইলের পুরো নিয়ম বা তারিখের অর্থ — এমন সমস্যা, যা regex-এর গাণিতিক ক্ষমতার বাইরে বা প্রায় বাইরে; জোর করে করতে গেলে দানব প্যাটার্ন তৈরি হয়। চতুর্থত, পারফরম্যান্সের ফাঁদ, যা প্রশ্ন ০৩-এ দেখবেন।

তবে উক্তিটি regex-বিরোধী নয়, অপব্যবহার-বিরোধী। একটি লগ ফাইল থেকে সব IP ঠিকানা তোলা, একাধিক চিহ্নে লেখা ভাঙা, কোড এডিটরে একসাথে শত জায়গায় নাম বদলানো — এসব কাজে regex অসাধারণ, আর বিকল্প কোড অনেক লম্বা ও ভুলপ্রবণ হতো। পার্থক্যটা বিচারবোধে: সমস্যাটা কি সত্যিই “লেখার আকৃতি”? প্যাটার্নটা কি ছোট আর পরীক্ষাযোগ্য? নাকি সমস্যাটা আসলে কাঠামোর (nested data), অর্থের (বৈধ তারিখ) বা নির্দিষ্ট শব্দের (in)? Python-এর দর্শনের “Readability counts” (PEP 20) এখানে সরাসরি প্রযোজ্য — re.VERBOSE এই দর্শনেরই একটি যন্ত্র।

মূল উপলব্ধি: যেকোনো শক্তিশালী যন্ত্রের মতো regex-এরও একটি সঠিক জায়গা আছে। সমস্যাটা regex নয়, বরং চিন্তা না করে সবকিছুতে regex টেনে আনা। ভালো প্রোগ্রামার আগে জিজ্ঞেস করেন “সবচেয়ে সরল যন্ত্র কোনটি?” — আর regex প্রায়ই তার উত্তর নয়, কিন্তু যখন উত্তর, তখন কোনো বিকল্প তার ধারেকাছে নেই।

প্র ০৩একটি regex কীভাবে পুরো একটি বিশ্বব্যাপী সেবা বন্ধ করে দিতে পারে? Stack Overflow (২০১৬) আর Cloudflare (২০১৯)-এর ঘটনা থেকে কী শেখার আছে?

Stack Overflow, জুলাই ২০১৬: প্রোগ্রামারদের সবচেয়ে বড় প্রশ্নোত্তর সাইটটি প্রায় আধা ঘণ্টার মতো বন্ধ ছিল। তাদের প্রকাশিত ব্যাখ্যা অনুযায়ী কারণ ছিল লেখার শুরু-শেষের ফাঁকা জায়গা ছাঁটার একটি সাধারণ-দেখতে regex. একটি পোস্টে কয়েক হাজার ফাঁকা অক্ষর পরপর ছিল, আর প্যাটার্নটি সেখানে প্রতিটি শুরুর ঘর থেকে সব ফাঁকা অক্ষর পার হয়ে তারপর ব্যর্থ হচ্ছিল — কাজের পরিমাণ ইনপুটের দৈর্ঘ্যের বর্গের অনুপাতে বেড়ে গিয়েছিল। পোস্টটি হোম পেজে থাকায় প্রতিটি অনুরোধে সার্ভার আটকে যাচ্ছিল। লক্ষ করুন, এটা exponential নয়, “শুধু” quadratic — তবু যথেষ্ট লম্বা ইনপুটে সেবা বন্ধ করার জন্য যথেষ্ট।

Cloudflare, ২ জুলাই ২০১৯: ইন্টারনেটের বড় একটি অংশের নিরাপত্তা ও গতির সেবাদাতা Cloudflare-এর সারা বিশ্বের সার্ভারে CPU ব্যবহার হঠাৎ প্রায় একশো শতাংশে পৌঁছায়, আর তাদের সেবা নেওয়া অসংখ্য ওয়েবসাইট প্রায় আধা ঘণ্টা ধরে (তাদের হিসাবে ২৭ মিনিটের মতো) ত্রুটি দেখায়। কারণ: তাদের web application firewall-এ নতুন একটি নিয়ম যোগ করা হয়েছিল, যার regex-এ .*(?:.*=.*)-এর মতো একাধিক .* পরপর ছিল — ঠিক এই পর্বের catastrophic backtracking-এর ধাঁচ। নিয়মটি একসাথে সারা বিশ্বে চালু করা হয়েছিল, ধাপে ধাপে নয়। Cloudflare-এর বিস্তারিত পোস্ট-মর্টেমে তারা নিজেরাই স্বীকার করে যে backtracking ইঞ্জিনের এই ঝুঁকি, আর পরিবর্তন ধীরে ধীরে চালু না করা — দুটোই দায়ী, আর পরে তারা linear-time গ্যারান্টি দেওয়া ইঞ্জিনের দিকে যাওয়ার পরিকল্পনা জানায়।

শিক্ষা কয়েক স্তরে। প্যাটার্নের স্তরে: .* পরপর বা ভেতরে-বাইরে quantifier বিপদের সংকেত। ইনপুটের স্তরে: যে ইনপুট বাইরে থেকে আসে, তার দৈর্ঘ্যের সীমা দিন। ইঞ্জিনের স্তরে: অবিশ্বস্ত ইনপুটে linear-time ইঞ্জিন (প্রশ্ন ০৫)। আর প্রক্রিয়ার স্তরে — সম্ভবত সবচেয়ে গুরুত্বপূর্ণ — কোনো পরিবর্তন একসাথে সবখানে নয়, আগে ছোট অংশে চালিয়ে দেখা (staged rollout), আর CPU বা সময়ের সীমা বসানো, যাতে একটি খারাপ regex পুরো সিস্টেম না থামায়। ReDoS এখন OWASP-এর মতো নিরাপত্তা প্রতিষ্ঠানের তালিকায় একটি স্বীকৃত দুর্বলতা, আর বিভিন্ন জনপ্রিয় library-তে প্রতি বছর এমন দুর্বলতা খুঁজে পাওয়া যায়।

মূল উপলব্ধি: regex শুধু “লেখা খোঁজার” বিষয় নয়, এটি এমন একটি ছোট প্রোগ্রাম, যার চলার সময় ইনপুটের ওপর নাটকীয়ভাবে নির্ভর করতে পারে। প্রোডাকশনে প্রতিটি regex-কে কোডের মতোই পর্যালোচনা, টেস্ট আর সীমাবদ্ধ করতে হয় — বিশেষ করে যেটা বাইরের ইনপুট দেখে।

প্র ০৪আজকের regex-এর চেহারা — \d, .*?, (?:...) — কোথা থেকে এল? Perl-এর প্রভাব কী, আর Python কী যোগ করেছে?

Thompson-এর ed আর grep-এর regex ছিল খুব ছোট: অক্ষর, ., [...], *, ^, $. পরে egrep আর awk যোগ করে +, ?, |. POSIX মান দুই ধরনের regex নির্ধারণ করে — “basic” আর “extended” — আর আজও Linux-এর grep আর grep -E-এ সেই পার্থক্য দেখা যায়। ১৯৮৬ সালে কানাডার প্রোগ্রামার Henry Spencer একটি মুক্ত regex library লেখেন, যা পরে অনেক প্রোগ্রামে ছড়িয়ে পড়ে।

১৯৮৭ সালে Larry Wall তৈরি করেন Perl — টেক্সট প্রক্রিয়াকরণের ভাষা, যার কেন্দ্রেই regex. Perl regex-কে ভাষার একেবারে ভেতরে বসিয়ে দেয় ($line =~ /pattern/), আর বিশেষ করে ১৯৯৪ সালের Perl 5-এ অনেক নতুন ফিচার আসে: lazy quantifier (*?, +?), non-capturing group (?:...), lookahead (?=...), আর /x modifier, যা মন্তব্যসহ প্যাটার্ন লিখতে দেয় — Python-এর re.VERBOSE-এর পূর্বসূরি। (? দিয়ে শুরু করার কৌশলটি চতুর ছিল: পুরোনো নিয়মে (? অর্থহীন, তাই এই রূপে নতুন ফিচার যোগ করলে পুরোনো কোনো প্যাটার্ন ভাঙে না। ১৯৯৭ সালে Philip Hazel PCRE (Perl Compatible Regular Expressions) library লেখেন, যা PHP, Apache-সহ অসংখ্য সফটওয়্যারে Perl-ধাঁচের regex পৌঁছে দেয়। ফলে আজকের প্রায় সব ভাষার regex মূলত “Perl-ধাঁচ”।

Python-এর re module (Python 1.5-এ আসে, পুরোনো regex module-এর জায়গায়) সচেতনভাবে Perl-এর সিনট্যাক্স গ্রহণ করে, আর Python 2.0 থেকে Fredrik Lundh-এর লেখা SRE ইঞ্জিনে চলে, যার উত্তরসূরি আজও ব্যবহৃত হয়। Python একটি নিজস্ব সংযোজনও করে: named group-এর (?P<name>...) রূপ — এখানে P অক্ষরটি Python-নির্দিষ্ট সংযোজন বোঝাত। পরে PCRE-সহ আরও কয়েকটি ইঞ্জিন এই রূপ গ্রহণ করে, আর অন্যরা (?<name>...) রূপ চালু করে। Python 3-এ str প্যাটার্নে Unicode আচরণ ডিফল্ট হয় — যে কারণে \d বাংলা অঙ্ক মেলায় — আর Python 3.11-এ atomic group ও possessive quantifier যোগ হয়, যা Perl-ধারার ইঞ্জিনগুলোতে আগে থেকেই ছিল।

মূল উপলব্ধি: regex-এর সিনট্যাক্স কোনো একক নকশা নয়, পঞ্চাশ বছরের স্তরে স্তরে জমা হওয়া সংযোজন — আর প্রতিটি নতুন স্তর পুরোনো প্যাটার্ন না ভাঙার শর্তে এসেছে। এজন্যই এটি এত ঘন ও অসংগত, আর এজন্যই বিভিন্ন ভাষার regex “প্রায় এক”, কিন্তু পুরোপুরি এক নয়।

প্র ০৫RE2-এর মতো linear-time ইঞ্জিন backtracking ইঞ্জিন থেকে কীভাবে আলাদা? তাহলে Python সবসময় সেটা ব্যবহার করে না কেন?

regex মেলানোর দুটি মৌলিক কৌশল আছে। Backtracking (Python-এর re, Perl, PCRE, Java, JavaScript-এর প্রচলিত ইঞ্জিন): একটি পথ বেছে নাও, শেষ পর্যন্ত যাও, ব্যর্থ হলে পেছনে ফিরে পরের পথ — গোলকধাঁধায় একজন মানুষ যেভাবে হাঁটে। সহজ, নমনীয়, আর বেশিরভাগ প্যাটার্নে দ্রুত; কিন্তু অংশ ৮-এ দেখেছেন, কিছু প্যাটার্নে পথের সংখ্যা exponential. Automaton-ভিত্তিক (Thompson-এর ১৯৬৮-এর কৌশল): সব সম্ভাব্য পথ একসাথে অনুসরণ করো — প্রতিটি অক্ষর পড়ে “এখন কোন কোন অবস্থায় থাকা সম্ভব” সেই সেটটি হালনাগাদ করো। অবস্থার সংখ্যা প্যাটার্নের আকারে সীমিত, তাই প্রতিটি অক্ষরে কাজ সীমিত, আর মোট সময় ইনপুটের দৈর্ঘ্যের সাথে সরলরেখায় বাড়ে। গোলকধাঁধায় এ যেন প্রতিটি মোড়ে নিজের একটি করে কপি পাঠানো, আর একই জায়গায় দুটি কপি পৌঁছালে একটিকে বাদ দেওয়া।

২০০৭ সালে Google-এর প্রকৌশলী Russ Cox “Regular Expression Matching Can Be Simple And Fast” নামের একটি প্রভাবশালী প্রবন্ধে দেখান যে Thompson-এর পুরোনো কৌশলটি প্যাথলজিক্যাল প্যাটার্নে তখনকার জনপ্রিয় backtracking ইঞ্জিনগুলোর চেয়ে বহুগুণ দ্রুত। তিনি Google Code Search-এর জন্য তৈরি করেন RE2, যা ২০১০ সালে মুক্ত সফটওয়্যার হিসেবে প্রকাশিত হয় — এর নকশার মূল প্রতিশ্রুতি হলো ইনপুটের দৈর্ঘ্যে linear সময়, যাতে অবিশ্বস্ত মানুষের দেওয়া প্যাটার্নও নিরাপদে চালানো যায়। Go ভাষার regexp আর Rust-এর জনপ্রিয় regex crate একই দর্শনে তৈরি। Python-এ RE2 ব্যবহারের জন্য তৃতীয় পক্ষের wrapper package আছে।

তাহলে সবাই কেন এটা ব্যবহার করে না? কারণ দাম আছে। backreference ((\w+) \1 — “একই শব্দ পরপর দুবার”) গাণিতিক অর্থে regular নয়; কোনো finite automaton এটা চিনতে পারে না, তাই RE2 এটা সমর্থনই করে না। lookbehind-এর মতো কিছু ফিচারও এই ইঞ্জিনগুলোতে সীমিত বা অনুপস্থিত। Python-এর re backtracking ইঞ্জিন বলে এসব ফিচার দিতে পারে, আর পুরোনো সব কোডের সাথে মিল রাখতে হয়। বাস্তব ভাগাভাগি তাই: নিজের লেখা, পরীক্ষিত প্যাটার্ন আর সীমিত দৈর্ঘ্যের ইনপুটে re পুরোপুরি ঠিক; ব্যবহারকারী নিজেই প্যাটার্ন দিতে পারে, বা বিশাল অবিশ্বস্ত ইনপুট — সেখানে RE2-ধাঁচের ইঞ্জিন।

মূল উপলব্ধি: প্রকৌশলের প্রায় প্রতিটি সিদ্ধান্ত একটি বিনিময়: এখানে ক্ষমতা (backreference, lookaround) বনাম নিশ্চয়তা (সবসময় linear সময়)। কোন ইঞ্জিন “ভালো”, তা নির্ভর করে কে প্যাটার্ন লেখে আর কে ইনপুট দেয় তার ওপর — এবং এই প্রশ্ন করতে শেখাই নিরাপদ সিস্টেম নকশার শুরু।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — আগে অনুমান, তারপর চালিয়ে মিলিয়ে নেওয়াই আসল শেখা।

  1. ফল অনুমান: খাতায় লিখুন এগুলোর ফল কী হবে, তারপর sms_parse.py-এর শেষে যোগ করে মিলিয়ে নিন: re.findall(r"\d+", "Tk 1,250.50"), re.findall(r"[0-9,]+\.\d{2}", "Tk 1,250.50"), re.match(r"\d+", "abc123"), re.findall(r"(\d+)-(\d+)", "1-2 33-44"), re.findall(r"a|ab", "ab").

    প্রথমটি ['1', '250', '50'] — কমা আর ডট \d নয়, তাই সংখ্যাটি তিন টুকরো। দ্বিতীয়টি ['1,250.50'] — class-এ কমা রাখা হয়েছে, আর \. আসল ডট। তৃতীয়টি None, কারণ match শুধু শুরু থেকে দেখে, আর শুরুতে a. চতুর্থটি [('1', '2'), ('33', '44')] — group থাকলে findall পুরো মিল নয়, group-গুলোর tuple দেয়। পঞ্চমটি ['a'] — বিকল্পগুলো বাম থেকে চেষ্টা হয়, আর প্রথম যেটা মেলে সেটাই নেওয়া হয়; ab পেতে লিখতে হতো ab|a.

  2. যাচাইয়ের নকশা: একটি ফর্মে জাতীয় পরিচয়পত্রের মতো একটি ১০ বা ১৭ অঙ্কের নম্বর নিতে হবে (ধরে নিন শুধু এই দুই দৈর্ঘ্য বৈধ)। কেউ AI থেকে পেল re.search(r"\d{10,17}", s). এতে অন্তত তিনটি সমস্যা খুঁজে বের করুন, আর ঠিক প্যাটার্ন ও ফাংশন লিখুন।

    (১) search বাড়তি অক্ষর মেনে নেয় — "abc1234567890xyz"-ও পাস; চাই fullmatch. (২) {10,17} মানে ১০ থেকে ১৭-এর যেকোনো দৈর্ঘ্য, ১৩ বা ১৫ অঙ্কও পাস; চাই ঠিক ১০ অথবা ঠিক ১৭. (৩) \d বাংলা অঙ্কও মেলায়; ডেটাবেসে শুধু ইংরেজি অঙ্ক চাইলে [0-9]. ঠিক রূপ: re.fullmatch(r"[0-9]{10}|[0-9]{17}", s) — fullmatch পুরো প্যাটার্নকে পুরো লেখার সাথে মেলায়, তাই এখানে | নিরাপদ; re.search আর ^...$ দিয়ে লিখলে বন্ধনী লাগত: ^(?:[0-9]{10}|[0-9]{17})$. চাইলে আগে space বা ড্যাশ মুছে নিন, আর দৈর্ঘ্যের সীমা দিন। এটাও মনে রাখুন, regex শুধু আকৃতি যাচাই করে; নম্বরটি আসলে কারও কি না, তা অন্য ব্যবস্থার কাজ।

  3. regex নাকি method: নিচের প্রতিটি কাজে regex লাগবে কি না সিদ্ধান্ত নিন, কারণসহ: (ক) SMS-এ “OTP” শব্দটি আছে কি না; (খ) একটি লেখা কমা, সেমিকোলন বা স্ল্যাশে ভাঙা; (গ) একটি ওয়েবপেজ থেকে সব লিংক বের করা; (ঘ) একটি লগ ফাইলে ERROR-এর পর থাকা ৪ অঙ্কের কোড তোলা; (ঙ) ফাইলের নাম .csv-এ শেষ কি না।

    (ক) regex নয় — "OTP" in sms. (খ) regex মানানসই — re.split(r"[,;/]", s); str.split একবারে একটিমাত্র চিহ্নে ভাঙে। (গ) regex নয় — HTML-এর জন্য parser (html.parser বা BeautifulSoup); attribute-এর ক্রম, কোট আর মন্তব্য regex-কে ভাঙবে। (ঘ) regex মানানসই — re.findall(r"ERROR\s+([0-9]{4})", log); এটি সত্যিই “আকৃতি” খোঁজা, আর group দিয়ে শুধু কোডটুকু। (ঙ) regex নয় — name.endswith(".csv"), বা আরও ভালো pathlib.Path(name).suffix == ".csv" (পর্ব ২৫)। মোট পাঁচটির মধ্যে দুটিতে regex — বাস্তবেও অনুপাতটা মোটামুটি এমনই।

  4. AI অডিট: যেকোনো AI-কে বলুন: “Python-এ একটি regex দাও যা একটি বাংলা বাক্যের সব শব্দ বের করে, আর আরেকটি যা ইমেইল ঠিকানা যাচাই করে।” তার কোড "আমি কাল ঢাকা যাব" আর "rahim@example.com", "rahim@@x.com", "rahim @x.com" দিয়ে চালান। কী কী সমস্যা পেলেন?

    বাংলা শব্দের জন্য AI প্রায়ই re.findall(r"\w+", s) বা \b\w+\b দেয় — ফল ['আম', 'ক', 'ল', …], কার-চিহ্নে ভাঙা শব্দ। ঠিক উপায় [\u0980-\u09FF]+, বা বাংলা-ইংরেজি মিশ্র লেখায় s.split(). ইমেইলের জন্য দুটি চরম দেখতে পারেন: একটি বিশাল “RFC-সম্মত” প্যাটার্ন, যা কেউ পড়তে পারে না, অথবা re.match দিয়ে চালানো একটি প্যাটার্ন, যা শেষে বাড়তি অক্ষর মেনে নেয়। দেখুন সে fullmatch ব্যবহার করেছে কি না, @@ আর space-ওয়ালা ঠিকানা বাতিল হয় কি না। এরপর AI-কে বলুন VERBOSE-এ মন্তব্যসহ লিখতে আর ঠিক-ভুল উদাহরণের টেস্ট দিতে। আসল শিক্ষা: AI-এর regex যতই পেশাদার দেখাক, আপনার নিজের বাংলা আর কিনারার উদাহরণ দিয়ে না চালিয়ে বিশ্বাস করবেন না।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমফর্মের একটি ইনপুট যাচাইয়ের regex বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📝 বাংলায় লিখুন বৈধ লেখার আকৃতি কেমন
  2. 🧹 ইনপুট থেকে space-ড্যাশ মুছুন আর দৈর্ঘ্য সীমিত করুন
  3. 🔤 raw string-এ, দরকারে VERBOSE-এ প্যাটার্ন লিখে compile করুন
  4. ✅ re.fullmatch দিয়ে মেলান
  5. 🧪 ঠিক, ভুল আর কিনারার উদাহরণ দিয়ে টেস্ট চালান

Python-এ একটি ফর্মের পুরো ইনপুট একটি প্যাটার্নের সাথে হুবহু মেলে কি না যাচাই করতে কোন ফাংশন সবচেয়ে উপযুক্ত?

  • re.search
  • re.match
  • re.fullmatch
  • re.findall

Python-এ re.findall(r"<.*?>", "<b>bold</b>")-এর ফল কী?

  • ['<b>bold</b>']
  • ['<b>', '</b>']
  • ['<b>']
  • ['bold']

Python 3-এ re.fullmatch(r"\d+", "১২৩") কী ফেরত দেয়?

  • একটি match object, কারণ \d বাংলা অঙ্কও মেলায়
  • None, কারণ \d শুধু 0–9 মেলায়
  • UnicodeError দেখা দেয়
  • পূর্ণসংখ্যা 123

Python-এ regex প্যাটার্ন r"\bcat\b"-এর মতো raw string-এ লেখার মূল কারণ কী?

  • raw string-এ regex দ্রুত চলে
  • re module শুধু raw string গ্রহণ করে
  • raw string বড়-ছোট হাতের পার্থক্য উপেক্ষা করে
  • যাতে Python নিজে backslash-কে escape sequence হিসেবে না বদলায়

Python-এ re.findall(r"(\d+)-(\d+)", "1-2 33-44")-এর ফল কী?

  • ['1-2', '33-44']
  • [('1', '2'), ('33', '44')]
  • ['1', '2', '33', '44']
  • দুটি match object-এর list

নিচের কোন প্যাটার্ন লম্বা, না-মেলা ইনপুটে catastrophic backtracking (ReDoS)-এর ঝুঁকি সবচেয়ে বেশি তৈরি করে?

  • ^01[3-9][0-9]{8}$
  • a+$
  • (a+)+$
  • <[^>]*>

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. regex হলো লেখার আকৃতির ছোট ভাষা — খোঁজা, যাচাই, বের করা, বদলানো। কিন্তু in, startswith, split দিয়ে হলে regex নয়।
  2. প্যাটার্ন সবসময় raw string-এ; খোঁজায় search/finditer, যাচাইয়ে fullmatch; নাম দিয়ে রাখতে re.compile.
  3. [...] একটি ঘরের বিকল্প, * + ? {n,m} কতবার, ^ $ \b অবস্থান; .* greedy, .*? lazy; (?P<name>...) নাম ধরে টুকরো তোলে।
  4. Python 3-এ \d বাংলা অঙ্কও মেলায় (শুধু 0–9: [0-9] বা re.ASCII); \w কার-চিহ্ন মেলায় না — বাংলা শব্দে [\u0980-\u09FF]+.
  5. nested quantifier ((a+)+) = ReDoS-এর ঝুঁকি: সরল, anchored প্যাটার্ন আর দৈর্ঘ্য সীমা। HTML-এ parser, তারিখের অর্থে datetime.

পরের পর্বে: মডিউল ৪ শেষ — বড় প্রোগ্রাম গোছানোর সব যন্ত্র এখন আপনার হাতে। এবার মডিউল ৫-এ শুরু হচ্ছে অবজেক্ট-ওরিয়েন্টেড Python: নিজের type বানানো। এতদিন যে str, list, match object ব্যবহার করেছেন, সেগুলো কীভাবে class থেকে তৈরি হয় — আর আপনি নিজে কীভাবে একটি BankAccount বা Student class লিখবেন।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান