এই পর্বে যা শিখবেন
- Regex engine একটি লেখার ওপর অক্ষর ধরে ধরে কীভাবে হাঁটে — আর কোথায় পিছিয়ে এসে আবার চেষ্টা করে
- Metacharacter, character class, quantifier, anchor, group আর lookaround দিয়ে নিজে নকশা লেখা ও পড়া
Pattern,Matcher,find()-এর লুপ,replaceAllআরsplit-এর আসল নিয়ম- বাংলাদেশি মোবাইল নম্বর, SMS থেকে টাকা আর TrxID, লগ লাইন, কার্ড নম্বর ঢাকা — বাস্তব উদাহরণ
- বাংলা লেখায় regex-এর ফাঁদ, catastrophic backtracking থেকে বাঁচা, আর AI-এর লেখা regex যাচাইয়ের পদ্ধতি
১ · regular expression কী — এক পাতা if-এর বদলে এক লাইনের নকশা
ধরুন একটি ফর্মে ব্যবহারকারী মোবাইল নম্বর লিখবেন। আপনাকে যাচাই করতে হবে নম্বরটি বাংলাদেশি মোবাইল নম্বরের মতো দেখতে কি না। নিয়মটি মুখে বললে এরকম: “প্রথমে ০, তারপর ১, তারপর ৩ থেকে ৯-এর যেকোনো একটি অঙ্ক, তারপর ঠিক আটটি অঙ্ক, আর কিছু নয়।”
এখন পর্যন্ত যা শিখেছেন তা দিয়েই এটি লেখা যায়: length() ১১ কি না, charAt(0) '0' কি না, charAt(2) '3' থেকে '9'-এর মধ্যে কি না, তারপর একটি লুপে বাকি আটটি ঘর অঙ্ক কি না। প্রায় পনেরো লাইন। এবার নিয়মে যোগ হলো “শুরুতে +880 থাকতেও পারে।” আরও দশ লাইন। তারপর “মাঝে হাইফেন বা ফাঁকা থাকলে মুছে দাও।” কোড বাড়তেই থাকে, আর প্রতিটি নতুন if-এর সাথে ভুলের সুযোগও বাড়ে।
একই নিয়ম regular expression (সংক্ষেপে regex) দিয়ে লিখলে:
01[3-9]\d{8}০ · ১ · [৩ থেকে ৯-এর একটি] · [অঙ্ক, ঠিক ৮ বার]
Regex হলো লেখার আকৃতি বর্ণনা করার একটি ছোট ভাষা। এটি নিজে কোনো প্রোগ্রাম নয়; এটি একটি নকশা, যা একটি “ইঞ্জিন” লেখার সাথে মিলিয়ে দেখে। এই নকশা দিয়ে চার রকম কাজ হয়:
যাচাই
পুরো লেখাটি নকশার সাথে মেলে কি না — মোবাইল নম্বর, পোস্ট কোড, পাসওয়ার্ডের নিয়ম।
matches()খোঁজা
লম্বা লেখার ভেতরে নকশার মতো অংশ কোথায় কোথায় আছে — লগ ফাইলে সব ERROR লাইন।
find()তথ্য তোলা
মিলের ভেতর থেকে নির্দিষ্ট টুকরো বের করা — SMS থেকে টাকার অঙ্ক আর TrxID.
group()বদলানো ও কাটা
নকশার মতো অংশ বদলে দেওয়া বা সেখানে কেটে টুকরো করা — নম্বর ঢাকা, তালিকা ভাঙা।
replaceAll() · split()পর্ব ০৮-এ split, replaceAll আর matches-এর ভেতরে লুকানো regex-এর সাথে প্রথম দেখা হয়েছিল — সেখানে split(".") শূন্য দৈর্ঘ্যের array দিয়ে চমকে দিয়েছিল। এই পর্বে সেই লুকানো ভাষাটিকে সামনে এনে পুরোপুরি শিখব।
Java-তে দুই চরিত্র — Pattern আর Matcher
Java-র regex থাকে java.util.regex package-এ, Java 1.4 (২০০২) থেকে। সেখানে দুটি মূল class:
নকশা থেকে মিল পর্যন্ত
String pattern → Pattern (compiled once) → Matcher (one search over one text)
- নকশার লেখা
"01[3-9]\\d{8}"একটি সাধারণ String - Pattern
Pattern.compile(…)একবার compile, বারবার ব্যবহার - Matcher
p.matcher(text)একটি লেখার ওপর একটি খোঁজা - ফল
matches() · find()true/false, group, অবস্থান
মেট্রো রেলের টিকিট গেটের কথা ভাবুন। গেটের নিয়ম (কোন কার্ড বৈধ) একবারই সেট করা হয় — এটি Pattern. প্রতিটি যাত্রী কার্ড ছোঁয়ালে গেট সেই একটি কার্ড পরীক্ষা করে — এটি একটি Matcher. নিয়ম সারাদিন একই থাকে আর সব গেট একসাথে ব্যবহার করতে পারে; কিন্তু একটি কার্ডের পরীক্ষা চলাকালীন সেই গেটে আরেকজনের কার্ড একসাথে ঢোকানো যায় না। Java-তেও তাই: Pattern immutable আর thread-safe, কিন্তু একটি Matcher একসাথে একাধিক thread-এ ব্যবহার করা নিরাপদ নয়।
১৯৫১ সালে গণিতবিদ Stephen Kleene RAND Corporation-এর একটি গবেষণা-প্রতিবেদনে স্নায়ুকোষের নেটওয়ার্কের “ঘটনা” বর্ণনা করতে গিয়ে “regular events” ধারণা দেন; লেখাটি ১৯৫৬ সালে বই আকারে প্রকাশিত হয়। আজকের * চিহ্নটির নামই তাঁর নামে — Kleene star. ১৯৬৮ সালে Ken Thompson এই গণিতকে প্রথম কাজের হাতিয়ার বানান: QED টেক্সট এডিটরে regex দিয়ে লেখা খোঁজা। পরে Unix-এর ed এডিটরের g/re/p কমান্ড (“global regular expression print”) থেকে জন্ম নেয় বিখ্যাত টুল grep.
২ · নকশার বর্ণমালা — metacharacter, escaping আর Java-র ডাবল backslash
Regex-এ বেশিরভাগ অক্ষর নিজেকেই বোঝায়: a মানে a, 7 মানে 7, Tk মানে হুবহু “Tk”. কিন্তু ১৪টি অক্ষরের বিশেষ মানে আছে। এদের বলে metacharacter:
| চিহ্ন | মানে | উদাহরণ |
|---|---|---|
. | যেকোনো একটি অক্ষর (ডিফল্টে newline বাদে) | a.c মেলে abc, a7c |
* + ? | আগের জিনিস ০ বা বেশি · ১ বা বেশি · ০ বা ১ বার | colou?r মেলে color, colour |
{ } | ঠিক কতবার: {8}, {2,5}, {3,} | \d{8} ঠিক আটটি অঙ্ক |
[ ] | এই অক্ষরগুলোর যেকোনো একটি | [3-9] একটি অঙ্ক, ৩ থেকে ৯ |
( ) | group — কয়েকটি জিনিসকে এক ধরা, আর মিলের টুকরো মনে রাখা | (ab)+ মেলে ab, abab |
| | অথবা | INFO|WARN|ERROR |
^ $ | লেখার শুরু · লেখার শেষ | ^01 মানে শুরুতেই 01 |
\ | escape — পরের অক্ষরের বিশেষ মানে বাতিল, বা বিশেষ মানে দেওয়া | \. আসল ডট, \d অঙ্ক |
কোনো metacharacter-কে তার সাধারণ অর্থে চাইলে সামনে \ বসান: আসল ডট \., আসল যোগ চিহ্ন \+, আসল ব্র্যাকেট \(. এই কাজটিকে বলে escape করা।
Java-র বাড়তি ঝামেলা — backslash দুবার
এখানেই Java শিক্ষার্থীরা সবচেয়ে বেশি হোঁচট খান। regex-এর ভাষায় অঙ্ক মানে \d. কিন্তু আপনি regex লিখছেন একটি Java string literal-এর ভেতরে, আর Java String-এ \ নিজেই একটি escape sequence শুরু করে (\n, \t, পর্ব ০৪)। \d নামে কোনো Java escape নেই, তাই compiler আপত্তি করে (OpenJDK 21-এ যাচাই করা):
Main.java:3: error: illegal escape character
System.out.println("01712345678".matches("01[3-9]\d{8}"));
^
সমাধান: Java String-এ একটি আসল \ লিখতে হয় \\. ফলে দুই স্তরের অনুবাদ ঘটে — Java compiler "\\d" পড়ে মেমরিতে রাখে দুই অক্ষর \d, তারপর regex engine সেটি পড়ে বোঝে “একটি অঙ্ক”। আসল ডট চাইলে regex-এ \., Java-তে "\\.". আর একটি আসল backslash খুঁজতে চাইলে regex-এ \\, Java-তে চারটি: "\\\\".
| যা খুঁজতে চান | regex-এ | Java String-এ |
|---|---|---|
| একটি অঙ্ক | \d | "\\d" |
| আসল ডট | \. | "\\." |
আসল + (যেমন +880) | \+ | "\\+" |
| একটি আসল backslash | \\ | "\\\\" |
Text block এই সমস্যা মেটায় না। অনেকে ভাবেন Java 15-এর text block ("""…""", পর্ব ০৯) Python-এর raw string-এর মতো — backslash যেমন আছে তেমন থাকবে। তা নয়। Text block-এ উদ্ধৃতি চিহ্ন আর নতুন লাইন সহজ হয়, কিন্তু escape-এর নিয়ম একই। text block-এর ভেতরে \d লিখলেও একই illegal escape character error আসে (যাচাই করা)। Java-তে raw string literal নেই; regex-এ \\ লিখতেই হবে।
লেখাটিকে হুবহু চাইলে — Pattern.quote
কখনো ব্যবহারকারীর দেওয়া লেখাকেই খুঁজতে হয়, যেমন কেউ সার্চ বক্সে লিখলেন 1+1. সেটি সরাসরি নকশা হিসেবে দিলে + quantifier হয়ে যাবে। Pattern.quote("1+1") লেখাটিকে \Q1+1\E-এ মুড়ে দেয় — \Q থেকে \E পর্যন্ত সবকিছু হুবহু অর্থে। তাই "192.168.0.1".split(Pattern.quote(".")) দেয় [192, 168, 0, 1]. বাইরে থেকে আসা লেখা দিয়ে নকশা বানালে সবসময় quote করুন; নইলে কেউ ইচ্ছা করে বা ভুলে এমন নকশা পাঠাতে পারেন, যা আপনার সার্ভারকে আটকে রাখে (অংশ ৮)।
ভুল নকশা — PatternSyntaxException
নকশার ব্যাকরণ ভুল হলে Java compile-এর সময় ধরে না, কারণ compiler-এর কাছে নকশাটি শুধুই একটি String. ধরা পড়ে চলার সময়, Pattern.compile (বা matches, split) ডাকার মুহূর্তে — PatternSyntaxException, একটি unchecked exception. যেমন Pattern.compile("01[3-9"), যেখানে ] বন্ধ করা হয়নি:
java.util.regex.PatternSyntaxException: Unclosed character class near index 5
01[3-9
^
বার্তাটি তিন অংশে: সমস্যার বিবরণ (Unclosed character class), কোন index-এর কাছে (৫ — নকশার শেষ অক্ষর 9), আর নকশার নিচে ^ দিয়ে জায়গাটি দেখানো। আরও দুটি পরিচিত বার্তা: "(abc" দিলে Unclosed group near index 4, আর "*abc" দিলে Dangling meta character '*' near index 0 — অর্থাৎ *-এর আগে এমন কিছু নেই যাকে সে পুনরাবৃত্তি করবে। (তিনটিই OpenJDK 21-এ যাচাই করা।)
৩ · character class আর quantifier — কোন অক্ষর, কতবার
Regex-এর প্রায় সব নকশা দুটি প্রশ্নের উত্তর: এখানে কোন অক্ষর বসতে পারে? আর কতবার? প্রথমটির উত্তর দেয় character class, দ্বিতীয়টির quantifier.
| নকশা | মেলে | খেয়াল রাখুন |
|---|---|---|
[abc] | a, b বা c — যেকোনো একটি | একটি ঘর, তিনটি বিকল্প |
[a-z], [0-9], [A-Za-z0-9] | পরিসর (range) | [3-9] মানে ৩, ৪ … ৯ |
[^0-9] | অঙ্ক ছাড়া যেকোনো অক্ষর | [-এর ঠিক পরে ^ মানে “না”; বাইরে ^ মানে শুরু |
\d · \D | অঙ্ক · অঙ্ক নয় | ডিফল্টে শুধু [0-9] |
\w · \W | “শব্দের অক্ষর” · নয় | ডিফল্টে শুধু [a-zA-Z_0-9] — বাংলা নয়! |
\s · \S | ফাঁকা (space, tab, newline…) · নয় | \s+ একাধিক ফাঁকা একসাথে |
. | যেকোনো একটি অক্ষর | ডিফল্টে newline বাদে |
Character class-এর ভেতরে বেশিরভাগ metacharacter সাধারণ অক্ষর হয়ে যায়: [.+] মানে আসল ডট বা আসল যোগ চিহ্ন। ব্যতিক্রম চারটি — ], \, শুরুতে ^, আর মাঝখানে -. তাই হাইফেন রাখতে চাইলে শুরুতে বা শেষে রাখুন: [\s-].
| Quantifier | কতবার | উদাহরণ ও ফল |
|---|---|---|
? | ০ বা ১ (ঐচ্ছিক) | ab?c মেলে ac, abc |
* | ০ বা তার বেশি | ab*c মেলে ac, abbbc |
+ | ১ বা তার বেশি | ab+c ac-এর সাথে মেলে না |
{n} | ঠিক n বার | \d{8} |
{n,m} · {n,} | n থেকে m বার · অন্তত n বার | a{2,3} একটি a-র সাথে মেলে না |
Quantifier শুধু তার ঠিক আগের জিনিসটির ওপর কাজ করে। ab+ মানে “a, তারপর এক বা একাধিক b” — “ab” পুনরাবৃত্তি নয়। পুরো “ab” পুনরাবৃত্তি চাইলে group: (ab)+.
Engine আসলে কী করে — অক্ষর ধরে ধরে হাঁটা
Regex engine একজন অত্যন্ত ধৈর্যশীল পাহারাদারের মতো। সে লেখার শুরুতে দাঁড়ায়, নকশার প্রথম টুকরোটি দেখে, লেখার বর্তমান অক্ষর মেলে কি না যাচাই করে, মিললে দুজনেই এক ধাপ এগোয়। নিচে "01712345678".matches("^01[3-9]\\d{8}$") চলার সময় ঠিক কী হয়, দেখুন। “পরে” চাপুন:
1^ লেখার শুরু20 হুবহু 031 হুবহু 14[3-9] 3 থেকে 9-এর একটি5\d{8} ঠিক ৮টি অঙ্ক6$ লেখার শেষ
| লেখা | |
| index | |
| এ পর্যন্ত মিলেছে | |
| এই ধাপ |
^— শুরুতে আছি তো?^কোনো অক্ষর খায় না; সে শুধু একটি অবস্থান যাচাই করে। একে বলে anchor. Engine index ০-তে, অর্থাৎ লেখার শুরুতে — শর্ত পূরণ, index এখনো ০।0— হুবহু অক্ষর। index ০-এর অক্ষর '0', নকশাও চায় '0'. মিলল, দুজনেই এক ঘর এগোল।1— আবার হুবহু। index ১-এ '1'. মিলল। এ পর্যন্ত মিলেছে “01”.[3-9]— একটি ঘর, অনেক বিকল্প। index ২-এ '7'. সেটি ৩ থেকে ৯-এর মধ্যে, তাই মিলল। খেয়াল করুন, পুরো character class মাত্র একটি অক্ষর খায়। এই ধাপটিই 012… বা 011… দিয়ে শুরু ভুল নম্বর বাদ দেয়।\d{8}— ঠিক আটবার অঙ্ক। Engine পরপর আটটি অক্ষর দেখে: 1, 2, 3, 4, 5, 6, 7, 8 — সবই অঙ্ক। index ৩ থেকে এগিয়ে ১১-তে পৌঁছাল। সাতটি হলে বা মাঝে একটি হাইফেন থাকলে এখানেই ব্যর্থ হতো।$— লেখা কি শেষ? index ১১ মানে লেখার ঠিক শেষে (দৈর্ঘ্য ১১)। শর্ত পূরণ, পুরো নকশা মিলেছে —true. যদি লেখাটি হতো 017123456789 (১২ অঙ্ক), তাহলে এখানে আরেকটি '9' পড়ে থাকত আর$ব্যর্থ হতো।- এবার ভুল নম্বর: 01212345678. ধাপ ১–৩ আগের মতোই, কিন্তু index ২-এ '2', যা ৩–৯-এ নেই। নকশার অন্য কোনো বিকল্প নেই, আর
matches()শুধু শুরু থেকেই চেষ্টা করে — তাই সাথে সাথেfalse. Engine-কে কখনো “বোঝাতে” হয় না; সে শুধু নিয়ম মানে।
String.matches() পুরো লেখাটিকে মেলাতে চায়, তাই এখানে ^ আর $ না দিলেও একই ফল আসত। তবু অভ্যাসটা রাখা ভালো: নকশাটি পরে find()-এ ব্যবহার হলে anchor ছাড়া সেটি “লেখার ভেতরে কোথাও একটি নম্বর আছে কি না” দেখবে, যা যাচাই নয়। অংশ ৫-এ এই পার্থক্য বিস্তারে দেখব।
৪ · greedy, reluctant আর possessive — quantifier কতটুকু খাবে
.+ মানে “এক বা একাধিক যেকোনো অক্ষর”। কিন্তু ঠিক কতগুলো? যতগুলো সম্ভব, নাকি যতগুলো দরকার? এই প্রশ্নের তিন রকম উত্তর আছে, আর ভুল উত্তর বেছে নেওয়াই regex-এর সবচেয়ে প্রচলিত বাগ।
- Greedy (
*,+,?,{n,m}— ডিফল্ট): greedy quantifier প্রথমে যত বেশি সম্ভব খায়, তারপর বাকি নকশা না মিললে একটি একটি করে ফেরত দেয়। - Reluctant (পেছনে
?:*?,+?): reluctant quantifier (অন্য ভাষায় lazy) যত কম সম্ভব খায়, দরকার হলে একটি একটি করে বাড়ায়। - Possessive (পেছনে
+:*+,++): possessive quantifier যত বেশি সম্ভব খায় এবং কখনো ফেরত দেয় না।
এই “ফেরত দেওয়া” বা পিছিয়ে এসে আবার চেষ্টা করাকে বলে backtracking. HTML-এর মতো লেখায় < আর >-এর মাঝের ট্যাগ খুঁজতে গিয়ে পার্থক্যটি চোখে পড়ে:
1Pattern.compile("<.+>") // greedy2Pattern.compile("<.+?>") // reluctant3Pattern.compile("<[^>]+>") // negated class4Pattern.compile("<.++>") // possessive
| quantifier ধরেছে | |
| পরের দাবি |
- শুরু। চারটি নকশাই প্রথমে
<খোঁজে, আর index ০-তেই পায়। এবার.+-এর পালা। - Greedy খেয়ে ফেলল সব।
.+লোভীর মতো লেখার শেষ পর্যন্ত সবকিছু নিল:b>bold</b>. এখন নকশার শেষ টুকরো>একটি অক্ষর চায়, কিন্তু লেখায় আর কিছু বাকি নেই। ব্যর্থ — তাই backtracking. - একটি ফেরত দিল — মিলে গেল।
.+শেষ অক্ষর>ছেড়ে দিল; এখন>টুকরোটি সেটি পেল। মিল:<b>bold</b>— পুরো লেখা! আপনি চেয়েছিলেন দুটি ট্যাগ, পেলেন একটি বিশাল টুকরো।m.start()= 0,m.end()= 11 (শেষ index-এর পরের ঘর,substring-এর মতোই)। - Reluctant — যত কম, তত ভালো।
.+?কমপক্ষে একটি অক্ষর নিতে বাধ্য, তাই নিল শুধুb. তারপর সাথে সাথে জিজ্ঞেস করল, “>কি মেলে?” index ২-এ>— মিলল। মিল:<b>, index ০–৩। - দ্বিতীয়
find(). খোঁজা শুরু হলো আগের মিলের শেষ (index ৩) থেকে। b, o, l, d — কোনোটি<নয়। index ৭-এ<..+?নিল/, কিন্তু পরের অক্ষরb,>নয় — তাই আরেকটি বাড়িয়ে নিল/b; এবার>মিলল। দ্বিতীয় মিল:</b>. - আরও ভালো উপায় আর possessive-এর শিক্ষা।
<[^>]+>বলে “>ছাড়া যেকোনো অক্ষর” — সে কখনো>পেরিয়ে যায় না, তাই backtracking ছাড়াই সরাসরি দুটি ট্যাগ পায়। এটি reluctant-এর চেয়ে স্পষ্ট আর সাধারণত দ্রুত। অন্যদিকে<.++>: possessive.++সব খেয়ে আর কিছু ফেরত দেয় না, তাই>কখনো কিছু পায় না — কোনো মিলই নেই (যাচাই করা)। Possessive তখনই কাজের, যখন আপনি নিশ্চিত যে ফেরত দেওয়ার দরকার নেই; সেখানে সে অপ্রয়োজনীয় backtracking বন্ধ করে।
Greedy বনাম reluctant প্রশ্নের উত্তর প্রায়ই তৃতীয় একটি পথ: যা চান না, সেটি সরাসরি বাদ দিন। “কোট চিহ্নের ভেতরের লেখা” চাইলে "[^"]*", “পরের কমা পর্যন্ত” চাইলে [^,]*. এতে নকশা নিজের উদ্দেশ্য নিজেই বলে, আর engine-কে অন্ধের মতো পিছিয়ে আসতে হয় না।
৫ · anchor, group আর তিন রকম মেলানো — matches, lookingAt, find
Anchor আর word boundary
^ আর $ অক্ষর নয়, অবস্থান। একইভাবে \b হলো word boundary — যেখানে একদিকে “শব্দের অক্ষর” (\w) আর অন্যদিকে নয়। \bcat\b “cat” শব্দটি খোঁজে, কিন্তু “category”-র ভেতরের cat নয়। একটি সূক্ষ্ম কথা: $ লেখার একেবারে শেষে তো মেলেই, শেষে একটি newline থাকলে তার আগেও মেলে। তাই find() দিয়ে ^\d+$ খুঁজলে "123\n"-এ মিল পাওয়া যায়; একদম শেষ চাইলে \z (যাচাই করা)। matches() অবশ্য পুরো লেখা চায়, তাই সেখানে newline-সহ লেখা মেলে না।
Group — কয়েকটি জিনিস এক করা, আর টুকরো মনে রাখা
বন্ধনী ( ) দুটি কাজ করে। এক, ভেতরের জিনিসগুলোকে একটি একক বানায়, যাতে quantifier বা | পুরোটার ওপর খাটে। দুই, মিলের সেই অংশটুকু মনে রাখে — একে বলে capturing group. বাঁ দিক থেকে খোলা বন্ধনী গুনে নম্বর হয় ১, ২, ৩…; group(0) বা শুধু group() মানে পুরো মিল।
| লেখার ধরন | নাম | কখন |
|---|---|---|
(\d{4}) | capturing group | টুকরোটি পরে group(1) দিয়ে চাই |
(?<year>\d{4}) | named group (Java 7 থেকে) | নম্বরের বদলে নাম: group("year") |
(?:\+?880|0) | non-capturing group | শুধু একক বানাতে, মনে রাখার দরকার নেই |
(\w)\1 | backreference | আগে ধরা টুকরো আবার হুবহু চাই |
INFO|WARN|ERROR | alternation | কয়েকটি বিকল্পের যেকোনো একটি |
Backreference-এর দুটি ছোট উদাহরণ (যাচাই করা): (\w)\1 “bookkeeper”-এ পায় oo, kk, ee — একই অক্ষর পরপর দুবার। আর "this is is a test test".replaceAll("\\b(\\w+)\\s+\\1\\b", "$1") দেয় this is a test — ভুলে দুবার লেখা শব্দ মুছে গেল। Alternation-এর অগ্রাধিকার সবচেয়ে কম: ^INFO|WARN$ মানে “শুরুতে INFO” অথবা “শেষে WARN”, তাই বিকল্পগুলো group-এ রাখুন: ^(?:INFO|WARN)$.
বাস্তব উদাহরণ — লগ লাইন ভাঙা, named group দিয়ে
সার্ভারের লগ ফাইলে লাইনগুলো এরকম: 2026-10-18 ERROR Payment gateway timeout. তারিখ, log level আর বার্তা আলাদা করতে:
Pattern LOG = Pattern.compile(
"(?<date>\\d{4}-\\d{2}-\\d{2}) (?<level>INFO|WARN|ERROR) (?<msg>.*)");
Matcher m = LOG.matcher("2026-10-18 ERROR Payment gateway timeout");
if (m.matches()) {
m.group("date"); // "2026-10-18"
m.group("level"); // "ERROR"
m.group("msg"); // "Payment gateway timeout"
m.groupCount(); // 3
}Named group-এর বড় সুবিধা হলো নকশায় নতুন group যোগ করলে নম্বর সরে যায়, নাম সরে না। m.groupCount() named group-ও গোনে, non-capturing গোনে না। আর মিল না পেয়ে group() ডাকলে IllegalStateException: No match found — তাই আগে matches() বা find()-এর ফল দেখে নিন।
তিন রকম মেলানো — সবচেয়ে বেশি ভুল হয় এখানে
| Method | প্রশ্ন | 01[3-9]\d{8} দিয়ে “01712345678 call me” | একই নকশায় “call 01712345678” |
|---|---|---|---|
matches() | পুরো লেখা কি নকশা? | false | false |
lookingAt() | লেখার শুরু কি নকশা? | true | false |
find() | লেখার কোথাও নকশা আছে? | true | true |
(সব ফল OpenJDK 21-এ যাচাই করা।) String.matches(regex) হলো Pattern.matches-এর সংক্ষিপ্ত রূপ — পুরো লেখা। ফর্ম যাচাইয়ে সাধারণত এটিই চান। কিন্তু AI বা অন্য ভাষা থেকে আসা কোডে প্রায়ই দেখবেন যাচাইয়ের জন্য find() লেখা, নকশায় ^/$ নেই — ফলে “আমার নম্বর 01712345678, টাকা পাঠান” পুরো বাক্যটিই “বৈধ নম্বর” হয়ে যায়।
৬ · Matcher-এর লুপ, replaceAll আর split — তথ্য তোলা, বদলানো, কাটা
একটি মোবাইল ব্যাংকিং SMS-এর মতো লেখা ধরুন (বানানো নমুনা): You have received Tk 1,500.00 from 01712345678. Fee Tk 0.00. Balance Tk 12,340.50. TrxID 9JK4L2M8NP …. এখান থেকে সব টাকার অঙ্ক বের করতে চাই। নকশা: Tk ([\d,]+\.\d{2}) — “Tk, ফাঁকা, তারপর অঙ্ক বা কমা এক বা একাধিক, তারপর আসল ডট আর দুটি অঙ্ক”, বন্ধনীর ভেতরের অংশটি group 1. find() প্রতিবার ডাকলে আগের মিলের শেষ থেকে পরের মিল খোঁজে — তাই এটি একটি while লুপে চলে:
1Matcher m = AMOUNT.matcher(sms);2while (m.find()) {3 print(m.group(1), m.start(), m.end());4}5// লুপ শেষ
| find() ফেরত দিল | |
| group() | |
| group(1) | |
| start – end | |
| পরের খোঁজা শুরু |
- Matcher তৈরি।
AMOUNTহলো আগে থেকে compile করাPattern;matcher(sms)এই একটি লেখার জন্য একটি নতুন খোঁজার অবস্থা তৈরি করল। এখনো কিছু খোঁজা হয়নি — Matcher index ০-তে দাঁড়িয়ে। - প্রথম
find()— true. Engine index ০ থেকে প্রতিটি অবস্থানে নকশা মেলানোর চেষ্টা করল; index ১৮-তে “Tk 1,500.00” মিলল।group()পুরো মিল,group(1)শুধু বন্ধনীর অংশ।start()= 18,end()= 29 — শেষ index-এর পরের ঘর। পরের খোঁজা শুরু হবে ২৯ থেকে। - দ্বিতীয়
find(). “from 01712345678.”-এর অঙ্কগুলো নকশায় মেলে না, কারণ সামনে “Tk ” নেই। index ৫২-তে “Tk 0.00” মিলল। ফি শূন্য হলেও এটি একটি বৈধ মিল — নকশা তো শুধু আকৃতি দেখে, মান নয়। - তৃতীয়
find(). “Tk 12,340.50” — index ৬৯ থেকে ৮১। কমা-সহ অঙ্ক পেলাম;Double.parseDouble-এর আগেreplace(",", "")দিয়ে কমা মুছতে হবে, নইলেNumberFormatException. - চতুর্থ
find()— false, লুপ শেষ। TrxID বা তারিখে আর “Tk ” নেই।find()false দিলে লুপ থামে। এই প্যাটার্ন —while (m.find()) { … m.group(…) … }— regex দিয়ে তথ্য তোলার সবচেয়ে প্রচলিত কাঠামো; মুখস্থ রাখার মতো।
এবার পুরো কাজটি চালিয়ে দেখুন — টাকার অঙ্ক, named group দিয়ে TrxID, আর ফোন নম্বরের মাঝের অঙ্কগুলো ঢেকে দেওয়া:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class Main {
static final Pattern AMOUNT = Pattern.compile("Tk ([\\d,]+\\.\\d{2})");
static final Pattern TRX = Pattern.compile("TrxID (?<id>[A-Z0-9]{10})");
public static void main(String[] args) {
String sms = "You have received Tk 1,500.00 from 01712345678. Fee Tk 0.00. "
+ "Balance Tk 12,340.50. TrxID 9JK4L2M8NP at 14/10/2026 10:15";
Matcher m = AMOUNT.matcher(sms);
while (m.find()) {
double tk = Double.parseDouble(m.group(1).replace(",", ""));
System.out.printf("%-9s index %2d-%2d -> %.2f%n", m.group(1), m.start(), m.end(), tk);
}
Matcher t = TRX.matcher(sms);
if (t.find()) System.out.println("TrxID = " + t.group("id"));
String safe = sms.replaceAll("(01[3-9])\\d{5}(\\d{3})", "$1*****$2");
System.out.println(safe.split("\\. ")[0]);
}
}আউটপুট পাঁচ লাইন: 1,500.00 index 18-29 -> 1500.00, 0.00 index 52-59 -> 0.00, 12,340.50 index 69-81 -> 12340.50, TrxID = 9JK4L2M8NP, আর You have received Tk 1,500.00 from 017*****678. দুটি নকশাই static final — class লোড হওয়ার সময় একবার compile হয়, প্রতিবার নয় (অংশ ৮)। replaceAll-এর বদলি লেখায় $1 আর $2 মানে group 1 আর 2-এর মিল; তাই অপারেটরের তিন অঙ্ক আর শেষ তিন অঙ্ক রইল, মাঝের পাঁচটি তারা হলো, আর দৈর্ঘ্য ১১-ই থাকল। শেষ লাইনে split("\\. ") — আসল ডট আর একটি ফাঁকা — লেখাটিকে বাক্যে ভাঙল; 1,500.00-এর ডটের পরে ফাঁকা নেই বলে সেখানে কাটেনি। (টাকার আসল হিসাবে double-এর বদলে BigDecimal নিরাপদ; এখানে শুধু দেখানোর জন্য double. নিজের প্রজেক্টে main class সাধারণত public class Main; এই পাতার রানারের জন্য public বাদ রাখা হয়েছে।)
replaceAll — $ আর \-এর বিশেষ মানে
replaceAll(regex, replacement)-এর প্রথম argument regex, আর দ্বিতীয়টিও পুরোপুরি সাধারণ লেখা নয়: সেখানে $1 মানে group 1, ${name} মানে named group, আর \ escape. ফলে বদলি লেখায় একটি আসল $ থাকলে বিপদ (OpenJDK 21-এ যাচাই করা):
"Tk 50".replaceAll("Tk", "$") → IllegalArgumentException: Illegal group reference: group index is missing
"Tk 50".replaceAll("Tk", "$5") → IndexOutOfBoundsException: No group 5
বদলি লেখাটি যদি বাইরে থেকে আসে (ব্যবহারকারী, ডেটাবেস, মুদ্রার চিহ্ন), তাহলে Matcher.quoteReplacement(text) দিয়ে মুড়ে দিন: "Tk 50".replaceAll("Tk", Matcher.quoteReplacement("$")) দেয় $ 50. আর যদি regex-এর দরকারই না থাকে — হুবহু একটি লেখা বদলাতে চান — তাহলে replace ব্যবহার করুন; সেটি দুটি argument-কেই হুবহু অর্থে নেয়।
split — regex দিয়ে কাটা, আর ফাঁকা টুকরোর নিয়ম
split-এর argument-ও regex, তাই একসাথে কয়েক রকম বিভাজক সামলানো যায়: "01711-111111, 01811 222222 ; 01911333333".split("\\s*[,;]\\s*") — “কমা বা সেমিকোলন, দুপাশে যত খুশি ফাঁকা” — দেয় তিনটি পরিষ্কার টুকরো। ফাঁকা টুকরো নিয়ে নিয়মটি অবশ্য মনে রাখতে হয় (সব যাচাই করা):
| কোড | ফল | নিয়ম |
|---|---|---|
"a,b,,c,,".split(",") | [a, b, , c] — দৈর্ঘ্য ৪ | মাঝের ফাঁকা টুকরো থাকে, শেষের ফাঁকাগুলো বাদ |
"a,b,,c,,".split(",", -1) | দৈর্ঘ্য ৬ | ঋণাত্মক limit: কিছুই বাদ নয় — CSV-তে এটিই চান |
"a,b,,c,,".split(",", 2) | [a, b,,c,,] | limit n: সর্বোচ্চ n টুকরো, বাকিটা শেষ টুকরোয় |
",a,b".split(",") | [, a, b] | শুরুর ফাঁকা টুকরো থাকে |
"".split(",") | দৈর্ঘ্য ১ (একটি ফাঁকা String) | খালি লেখা = এক টুকরো, শূন্য নয় |
৭ · বাংলা, Unicode আর flag — \w কেন বাংলা চেনে না
এই অংশটি বাংলাভাষী প্রোগ্রামারদের জন্য বিশেষ গুরুত্বপূর্ণ। Java-র \w, \d, \b ডিফল্টে শুধু ASCII — অর্থাৎ ইংরেজি অক্ষর, ০–৯ অঙ্ক আর _. OpenJDK 21-এ যাচাই করা ফল:
| পরীক্ষা | ডিফল্ট | UNICODE_CHARACTER_CLASS দিয়ে |
|---|---|---|
"বাংলা" কি \w+? | false | true |
"০১৭" কি \d+? | false | true |
"আমি ভাত খাই"-এ \bভাত\b পাওয়া যায়? | false | true |
"আমি ভাত"-কে \s+ দিয়ে split | ২ টুকরো | ২ টুকরো |
অর্থাৎ ফাঁকা (\s) বাংলা লেখাতেও কাজ করে, কিন্তু “শব্দ”, “অঙ্ক” আর “শব্দের সীমা” ডিফল্টে বাংলাকে চেনে না। সমাধান Pattern.UNICODE_CHARACTER_CLASS flag (Java 7 থেকে), বা নকশার শুরুতে (?U). তখন \w Unicode-এর অক্ষর, কার-চিহ্ন, অঙ্ক সবই ধরে।
\p{…} — Unicode-এর নিজস্ব শ্রেণি
Unicode প্রতিটি code point-কে একটি শ্রেণি আর একটি লিপি দেয়। Regex-এ এগুলো ধরা যায় Unicode property দিয়ে: \p{L} যেকোনো লিপির অক্ষর (Letter), \p{M} চিহ্ন (Mark — কার, হসন্ত, অনুস্বার), \p{Nd} দশমিক অঙ্ক, \p{IsBengali} বাংলা লিপি। এখানেই বাংলার আসল জটিলতা চোখে পড়ে (যাচাই করা):
"বাংলা".matches("\\p{L}+")→ false! কারণ “া” আর “ং” Unicode-এ Letter নয়, Mark. চোখে যা অক্ষর, Unicode-এর হিসাবে তা অক্ষর আর চিহ্নের জোড়া।"বাংলা".matches("[\\p{L}\\p{M}]+")→ true. বাংলা শব্দ ধরতে অক্ষর আর চিহ্ন দুটোই লাগে।"বাংলা".matches("\\p{IsBengali}+")→ true; কিন্তু দাঁড়ি “।”\p{IsBengali}-এ পড়ে না — এটি দেবনাগরী ব্লকের অক্ষর, যা বাংলা আর হিন্দি দুই লিপিই ব্যবহার করে।- “র্যাব”-এর মতো শব্দে র-ফলা ঠিক দেখাতে অদৃশ্য একটি অক্ষর থাকে — zero-width joiner (U+200D). এমন শব্দ
[\p{L}\p{M}]+বা\p{IsBengali}+-এ মেলে না, কিন্তুUNICODE_CHARACTER_CLASS-সহ\w+-এ মেলে, কারণ Java-র Unicode\w-এ এই join control অক্ষরও ধরা আছে।
পর্ব ০৮-এ দেখেছিলেন length() চোখের অক্ষর গোনে না; regex-এও একই কথা — . একটি code point ধরে, একটি grapheme cluster নয়। তাই "ক্ষ".matches("...") true: ক, হসন্ত, ষ — তিনটি code point.
Flag — নকশার আচরণ বদলানো
| Flag | নকশার ভেতরে | কী বদলায় |
|---|---|---|
CASE_INSENSITIVE | (?i) | বড়-ছোট হাতের পার্থক্য উপেক্ষা (ডিফল্টে শুধু ASCII; Unicode-এর জন্য সাথে UNICODE_CASE) |
MULTILINE | (?m) | ^/$ প্রতিটি লাইনের শুরু-শেষে মেলে |
DOTALL | (?s) | . newline-ও ধরে |
COMMENTS | (?x) | নকশার ফাঁকা উপেক্ষা, # থেকে লাইনের শেষ পর্যন্ত মন্তব্য |
UNICODE_CHARACTER_CLASS | (?U) | \w \d \b ইত্যাদি Unicode অনুযায়ী |
একাধিক flag জোড়া যায় | দিয়ে: Pattern.compile(regex, Pattern.MULTILINE | Pattern.CASE_INSENSITIVE). উদাহরণ: তিন লাইনের লেখা "rice 50\nOil 180\nsalt 40"-এ ^[a-z]+ শুধু MULTILINE দিয়ে পায় [rice, salt], flag ছাড়া শুধু [rice], আর দুটো flag একসাথে দিলে [rice, Oil, salt] (যাচাই করা)।
Flag-গুলোর মধ্যে COMMENTS সবচেয়ে কম ব্যবহৃত, অথচ সবচেয়ে উপকারী। Text block-এর সাথে মিলিয়ে লম্বা নকশাকে পড়ার মতো করা যায়:
static final Pattern BD_MOBILE = Pattern.compile("""
^(?:\\+?880|0) # দেশের কোড +880/880, নয়তো শুরুর 0
1[3-9] # অপারেটর অংশ: 13 থেকে 19
\\d{8}$ # বাকি ৮টি অঙ্ক
""", Pattern.COMMENTS);
// +8801712345678 → true, 8801912345678 → true, 01512345678 → true
// 01212345678 → false, +88001712345678 → falseখেয়াল করুন, text block-এর ভেতরেও \\+ আর \\d — দুটি backslash. আর নকশার যুক্তিটিও খেয়াল করুন: আন্তর্জাতিক রূপে শুরুর ০ বাদ যায় (+880 1712…), তাই (?:\+?880|0) — “+880 অথবা 0”, তারপর 1[3-9]. অনেকে (এবং অনেক AI) লেখেন (\+?880)?01[3-9]\d{8}, যা “+8801712345678”-কে বাতিল করে দেয় — আমরা নিজেরাও প্রথম খসড়ায় এই ভুল করেছিলাম, আর পরীক্ষা চালিয়ে ধরেছি।
Lookahead আর lookbehind — দেখে, কিন্তু খায় না
Lookaround হলো এমন শর্ত, যা বর্তমান অবস্থান থেকে সামনে বা পেছনে তাকিয়ে কিছু আছে কি না দেখে, কিন্তু কোনো অক্ষর খায় না। (?=…) সামনে আছে, (?!…) সামনে নেই, (?<=…) পেছনে আছে, (?<!…) পেছনে নেই। যেমন (?<=Tk )\d+ “Tk 500 for 3 items”-এ শুধু 500 পায়, 3 নয়।
পাসওয়ার্ড নীতি এর ক্লাসিক ব্যবহার: “অন্তত ৮ অক্ষর, একটি বড় হাতের, একটি ছোট হাতের, একটি অঙ্ক, একটি বিশেষ চিহ্ন।” প্রতিটি শর্ত একটি lookahead, সবগুলো লেখার শুরুতে দাঁড়িয়ে পুরো লেখায় চোখ বোলায়:
Pattern PASSWORD = Pattern.compile(
"^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[^A-Za-z0-9]).{8,}$");
// "Dhaka@2026" → true "dhaka2026" → false (বড় হাতের ও চিহ্ন নেই)
// "Dhaka2026" → false "Dk@1" → false (৮ অক্ষরের কম)একই কৌশলে কার্ড বা NID নম্বর ঢাকা যায়: \d(?=(?:\D*\d){4}) মানে “এমন অঙ্ক, যার পরে আরও অন্তত চারটি অঙ্ক বাকি”। সেগুলোকে * দিয়ে বদলালে শুধু শেষ চারটি দেখা যায়, আর হাইফেন অক্ষত থাকে। নিচের প্রোগ্রামে এটি আর বাংলার ফলগুলো একসাথে চালিয়ে দেখুন, সাথে একটি এলোমেলো ফোন-তালিকা পরিষ্কার করা:
import java.util.Arrays;
import java.util.regex.Pattern;
class Main {
static final Pattern BD_MOBILE = Pattern.compile("(?:\\+?880|0)1[3-9]\\d{8}");
public static void main(String[] args) {
String raw = "01712-345678, +880 1812 345678 ; 01212345678,,8801912345678, 0171234";
String[] parts = raw.split("\\s*[,;]\\s*");
System.out.println(parts.length + " parts: " + Arrays.toString(parts));
for (String p : parts) {
String clean = p.replaceAll("[\\s-]", "");
String verdict = BD_MOBILE.matcher(clean).matches() ? "OK " : "BAD";
System.out.println(verdict + " [" + clean + "]");
}
Pattern uniWord = Pattern.compile("\\w+", Pattern.UNICODE_CHARACTER_CLASS);
System.out.println("\\w+ : " + "বাংলা".matches("\\w+") + " / UNICODE: " + uniWord.matcher("বাংলা").matches());
System.out.println("\\p{L}+ : " + "বাংলা".matches("\\p{L}+") + " / [\\p{L}\\p{M}]+ : " + "বাংলা".matches("[\\p{L}\\p{M}]+"));
System.out.println("card : " + "4111-2222-3333-4444".replaceAll("\\d(?=(?:\\D*\\d){4})", "*"));
}
}প্রথম লাইন: 6 parts: [01712-345678, +880 1812 345678, 01212345678, , 8801912345678, 0171234] — দুটি পাশাপাশি কমা (,,) থেকে মাঝখানে একটি ফাঁকা টুকরো এসেছে, যা split রেখে দেয়। তারপর প্রতিটি টুকরো থেকে ফাঁকা আর হাইফেন মুছে যাচাই: OK [01712345678], OK [+8801812345678], BAD [01212345678] (অপারেটর অংশ 12), BAD [] (ফাঁকা টুকরো), OK [8801912345678], BAD [0171234] (খুব ছোট)। এরপর বাংলার ফল: \w+ : false / UNICODE: true আর \p{L}+ : false / [\p{L}\p{M}]+ : true. শেষ লাইন card : ****-****-****-4444. একটি কথা খেয়াল রাখুন: “পরিষ্কার” করার নকশা [\s-] শুধু ফাঁকা আর হাইফেন মোছে; কেউ বাংলা অঙ্কে নম্বর লিখলে সেটি BAD হবে, কারণ \d ডিফল্টে বাংলা অঙ্ক চেনে না। আপনার ফর্ম বাংলা অঙ্ক মানবে কি না, সেটি আগে ঠিক করুন।
ইমেইল — একটি সৎ সতর্কতা
একটি সাধারণ ইমেইল যাচাই: ^[\w.+-]+@[\w-]+(\.[\w-]+)+$. এটি rahim.uddin@gmail.com আর info@abc.com.bd মেনে নেয়; rahim@gmail, a@b..com, rahim@@x.com বাতিল করে। কিন্তু .rahim@x.com-এর মতো নিয়মবিরুদ্ধ ঠিকানাও মেনে নেয়, আর বাংলা অক্ষরের ঠিকানা (যা আন্তর্জাতিক ইমেইলে সম্ভব) বাতিল করে (সব যাচাই করা)। ইমেইলের পূর্ণ নিয়ম (RFC 5322) এত জটিল যে একে পুরোপুরি ধরা regex কয়েক হাজার অক্ষরের হয়। বাস্তব পরামর্শ: regex দিয়ে শুধু “দেখতে মোটামুটি ইমেইলের মতো” কি না দেখুন, আর আসল যাচাই করুন একটি নিশ্চিতকরণ ইমেইল পাঠিয়ে।
Python-এর re-তে raw string (r"\d") আছে বলে backslash একবার লিখলেই চলে; Java-তে এর বিকল্প নেই, তাই "\\d". Python-এর re.fullmatch, re.match, re.search মোটামুটি Java-র matches(), lookingAt(), find(); আর Python 3-এ \w ডিফল্টেই Unicode, Java-তে নয় (Python regex পর্ব)। C-র standard library-তে regex নেই (POSIX-এর regcomp আছে), আর C++-এর std::regex অনেক বাস্তবায়নে ধীর বলে পরিচিত (C++ লাইব্রেরি ভ্রমণ পর্ব)।
৮ · গতি, নিরাপত্তা আর কখন regex নয়
একবার compile, বহুবার ব্যবহার
"…".matches(regex), replaceAll বা split প্রতিবার ডাকলে ভেতরে নকশাটি নতুন করে compile হয় (এক অক্ষরের সাধারণ বিভাজকে split-এর একটি দ্রুত পথ আছে, সেটি ব্যতিক্রম)। একটি লুপে লাখ লাইনের ওপর এমন ডাকলে লাখবার compile. সমাধান সহজ: নকশাটিকে একটি static final constant বানান, যেমন এই পর্বের BD_MOBILE বা AMOUNT. Pattern immutable আর thread-safe, তাই পুরো প্রোগ্রাম একটি object ভাগ করে নিতে পারে; প্রতিটি লেখার জন্য শুধু নতুন Matcher তৈরি হয়, যা হালকা। static আর final-এর পূর্ণ অর্থ দেখবেন পর্ব ১৮-এ।
আগাম দেখা — stream দিয়ে মিল: Java 9 থেকে matcher.results() সব মিলকে একটি stream হিসেবে দেয়, আর Pattern.splitAsStream (Java 8) টুকরোগুলোকে। যেমন Pattern.compile("\\d+").matcher("a1b22c333").results().count() দেয় 3. Stream-এর পুরো গল্প পর্ব ৩৫-এ; এখনকার জন্য while (m.find()) লুপই যথেষ্ট আর পরিষ্কার।
Catastrophic backtracking — যখন একটি regex সার্ভার আটকে দেয়
অংশ ৪-এ দেখেছেন, মিল না পেলে engine পিছিয়ে এসে অন্য ভাগে চেষ্টা করে। সাধারণ নকশায় এই বিকল্প অল্প। কিন্তু একটি quantifier-এর ভেতরে আরেকটি quantifier থাকলে — যেমন (a+)+ — একই লেখাকে ভাগ করার উপায় বিস্ফোরকভাবে বাড়ে। আর মিল না থাকলে, engine প্রতিটি ভাগ চেষ্টা না করে “না” বলতে পারে না। একে বলে catastrophic backtracking. নিচে শুধু গুনে দেখুন (ধারণাগত হিসাব, কোনো সময় মাপা নয়):
1নকশা : (a+)+b2লেখা : a × n (শেষে কোনো b নেই)3নিরাপদ: a+b
| n | |
| ভাগের উপায় ≈ 2^(n−1) | |
| উদাহরণ |
- n = ৩, “aaa”. ভেতরের
a+কতগুলো a নেবে আর বাইরের+কতবার ঘুরবে — এই দুইয়ের মিলে চার রকম ভাগ: (aaa), (aa)(a), (a)(aa), (a)(a)(a). প্রতিটির পরে enginebখোঁজে, পায় না, পরের ভাগে যায়। চারটিই ব্যর্থ হলে তবেই উত্তর “মিল নেই”। - n = ৪ — দ্বিগুণ। চারটি a-র মাঝে তিনটি ফাঁক; প্রতিটি ফাঁকে “কাটব কি কাটব না” — ২ × ২ × ২ = ৮ রকম ভাগ। সাধারণভাবে n-টি a-র জন্য 2^(n−1). প্রতিটি নতুন a কাজ দ্বিগুণ করে।
- n = ১০ — ৫১২। এখনো এক মুহূর্তের কাজ। পরীক্ষার ছোট ইনপুটে তাই কিছুই ধরা পড়ে না — এটাই এই বাগের সবচেয়ে বিপজ্জনক দিক।
- n = ২০ — পাঁচ লাখের বেশি। মাত্র বিশ অক্ষর! ফর্মের একটি ঘরেই এর চেয়ে লম্বা লেখা আসে।
- n = ৩০ — পঞ্চাশ কোটির বেশি। সহজ backtracking engine-এর জন্য এটি সেকেন্ড, মিনিট, তারপর আরও বেশি — প্রতিটি অক্ষর সময় দ্বিগুণ করে। সার্ভারে এমন একটি অনুরোধ একটি CPU কোর দখল করে রাখে; অনেকগুলো এলে পুরো সার্ভিস বসে যায়। ইচ্ছাকৃতভাবে এমন ইনপুট পাঠিয়ে সার্ভিস বসিয়ে দেওয়ার ঝুঁকিকে বলে ReDoS.
- সমাধান: নকশাটাই ঠিক করা।
(a+)+bআরa+bএকই লেখা মেলায়, কিন্তু দ্বিতীয়টিতে ভাগের উপায় একটিই। নীতি: quantifier-এর ভেতরে quantifier এড়ান; বিকল্পগুলো যেন একই লেখা দুভাবে মেলাতে না পারে ((a|aa)+নয়); দরকারে possessive (a++) বা atomic group(?>…)দিয়ে ফেরত দেওয়া বন্ধ করুন।
OpenJDK 21-এ যা দেখা গেল: নতুন JDK-গুলো ঠিক (a+)+b-এর মতো কিছু পরিচিত নকশায় একটি অপটিমাইজেশন ব্যবহার করে — Wandbox-এ ৪০টি a-তেও এটি সাথে সাথে শেষ হয়েছে। কিন্তু এতে নিশ্চিন্ত হবেন না। একটু ভিন্ন আকারের নকশা — যেমন আরেক স্তর নেস্টিং ((a+)+)+b, reluctant (a+)+?b, বা backreference-যুক্ত নকশা — একই JDK-তে প্রতি দুটি বাড়তি a-তে কয়েক গুণ ধীর হয়েছে: ২০টি a-তে কয়েকশো মিলিসেকেন্ড, ২৬টিতে কয়েক সেকেন্ড (সময় মেশিনভেদে বদলায়)। অপটিমাইজেশন একটি সুরক্ষা জাল, নিশ্চয়তা নয়। নিরাপত্তা আসে নকশা ঠিক করা থেকে।
প্রতিরক্ষার বাস্তব চেকলিস্ট: (১) ব্যবহারকারীর লেখা দিয়ে কখনো সরাসরি নকশা বানাবেন না — Pattern.quote; (২) regex চালানোর আগে লেখার দৈর্ঘ্য সীমিত করুন (মোবাইল নম্বরের ঘরে ১০০০ অক্ষর আসার কারণ নেই); (৩) নেস্টেড quantifier আর ওভারল্যাপ করা বিকল্প খুঁজে দেখুন; (৪) শুধু মিলে এমন লেখা নয়, প্রায় মেলে কিন্তু শেষে ব্যর্থ হয় এমন লম্বা লেখা দিয়েও পরীক্ষা করুন — বিস্ফোরণ ঘটে ব্যর্থ হওয়ার পথেই।
কখন regex নয়
Regex লেখার আকৃতি চেনে, গঠন নয়। গণিতের ভাষায়, regular expression শুধু “regular language” চিনতে পারে, আর এমন ভাষা ইচ্ছামতো গভীর নেস্টিং গুনতে পারে না। HTML, JSON, XML, প্রোগ্রামিং ভাষা — সবখানেই ট্যাগ বা বন্ধনীর ভেতরে ট্যাগ-বন্ধনী থাকে। যেমন <b>(.*?)</b> “<b>Dhaka <b>Old</b> town</b>”-এ পায় Dhaka <b>Old — ভুল টুকরো (যাচাই করা)। এসব কাজে একটি parser ব্যবহার করুন: JSON-এর জন্য Jackson বা Gson (পর্ব ৩৯), HTML-এর জন্য jsoup-এর মতো library, CSV-র জন্য একটি CSV library. তারিখ পড়তে regex নয়, java.time-এর formatter (পর্ব ৩৮)।
String.matches()আরMatcher.find()-এর পার্থক্য? —matches()পুরো লেখা নকশার সাথে মেলাতে চায়;find()লেখার ভেতরে পরের যেকোনো মিল খোঁজে।- Java-তে
"\d"লিখলে কী হয়? — compile errorillegal escape character; লিখতে হয়"\\d". - Greedy আর reluctant quantifier কী? — greedy (
.*) যত বেশি সম্ভব খায়, reluctant (.*?) যত কম সম্ভব। Patternকি thread-safe? — হ্যাঁ, immutable; কিন্তুMatcherনয়, তাই প্রতি thread বা প্রতি খোঁজায় নতুন Matcher."a,b,,".split(",")-এর দৈর্ঘ্য? — ২; শেষের ফাঁকা টুকরো বাদ পড়ে, রাখতে চাইলেsplit(",", -1).
৯ · AI-এর যুগে — AI-এর লেখা regex যাচাই
Regex এমন একটি কাজ, যা AI খুব দ্রুত করে দেয় — আর যা দেখতে প্রায় সবসময় ঠিক লাগে। সমস্যা হলো regex-এর ভুল চোখে পড়ে না; সেটি ধরা পড়ে একটি নির্দিষ্ট ইনপুটে, প্রায়ই প্রোডাকশনে। AI-এর লেখা regex-কে তাই সন্দেহের চোখে দেখুন, আর পরীক্ষার লেখা দিয়ে প্রমাণ চান।
মেলা উচিত এমন লেখা, না-মেলা উচিত এমন লেখা — দুই তালিকাই চালিয়ে দেখুন।
AI সাধারণত যে ভুলগুলো করে
| AI-এর ভুল | ফল | যা করবেন |
|---|---|---|
যাচাইয়ে find(), নকশায় ^/$ নেই | ভুল লেখার ভেতরে একটি বৈধ টুকরো থাকলেই পাস | matches(), বা anchor যোগ |
বাংলা লেখায় \w, \d, \b | বাংলা শব্দ-অঙ্ক বাদ পড়ে | UNICODE_CHARACTER_CLASS বা [\p{L}\p{M}] |
(\+?880)?01[3-9]\d{8} | আন্তর্জাতিক রূপ +8801712345678 বাতিল | (?:\+?880|0)1[3-9]\d{8}, আর পরীক্ষা |
.* যেখানে [^…]* দরকার | বেশি খেয়ে ফেলে (over-match) | negated class বা reluctant |
(\w+\s?)*-এর মতো নেস্টেড quantifier | লম্বা ব্যর্থ ইনপুটে CPU আটকে যায় | নকশা সরল করা, দৈর্ঘ্যের সীমা |
| HTML বা JSON regex দিয়ে পড়া | নেস্টিং-এ ভুল টুকরো | parser library |
Python থেকে কপি: "\d" বা একটি backslash | compile error বা ভুল নকশা | Java-তে "\\d" |
AI-কে regex চাওয়ার সময় শুধু নকশা নয়, পরীক্ষার লেখাও চান — আর সেগুলো নিজে চালান। নিচের কাঠামোটি কাজে লাগবে:
AI-এর regex হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১স্নায়ুকোষের গবেষণা থেকে grep পর্যন্ত — regular expression কোথা থেকে এলো, আর “regular” শব্দটির মানে কী?
গল্পটি শুরু হয় প্রোগ্রামিং থেকে নয়, মস্তিষ্ক থেকে। ১৯৪৩ সালে Warren McCulloch আর Walter Pitts স্নায়ুকোষকে সরল গাণিতিক “সুইচ” হিসেবে বর্ণনা করেন। ১৯৫১ সালে গণিতবিদ Stephen Kleene RAND Corporation-এর জন্য লেখা একটি প্রতিবেদনে প্রশ্ন করেন: এমন একটি নেটওয়ার্ক কোন কোন “ঘটনার ধারা” চিনতে পারে? উত্তরে তিনি তিনটি মাত্র ক্রিয়া দিয়ে একটি বীজগণিত দাঁড় করান — পরপর বসানো (concatenation), বিকল্প (union, আজকের |), আর যতবার খুশি পুনরাবৃত্তি (আজকের *, যাকে এখনো Kleene star বলা হয়)। এই তিন ক্রিয়ায় যা বর্ণনা করা যায়, তার নাম দেন “regular events”. লেখাটি ১৯৫৬ সালে Automata Studies বইয়ে প্রকাশিত হয়।
Kleene দেখান, এই নকশাগুলো আর finite automaton — সীমিত কয়েকটি অবস্থা আর এক অবস্থা থেকে আরেকটিতে যাওয়ার নিয়ম দিয়ে তৈরি যন্ত্র — একই ক্ষমতার। যা একটি দিয়ে বলা যায়, অন্যটি দিয়েও যায়। এখান থেকে regular language ধারণা, যা আজ তত্ত্বীয় কম্পিউটার বিজ্ঞানের ভিত্তি। এর সীমাও এখান থেকে স্পষ্ট: সীমিত অবস্থার যন্ত্র ইচ্ছামতো গভীর বন্ধনী “গুনে” মনে রাখতে পারে না — তাই বিশুদ্ধ regex দিয়ে মিলযুক্ত বন্ধনী বা নেস্টেড HTML চেনা যায় না।
১৯৬৮ সালে Ken Thompson “Regular Expression Search Algorithm” নামে একটি প্রবন্ধে দেখান কীভাবে একটি regex-কে সরাসরি machine code-এ অনুবাদ করে দ্রুত খোঁজা যায়, আর এটি তিনি QED টেক্সট এডিটরে বসান। পরে তাঁর লেখা Unix ed এডিটরের g/re/p কমান্ড থেকে আলাদা টুল হিসেবে জন্ম নেয় grep. সেখান থেকে sed, awk, Perl, আর শেষে প্রায় সব ভাষায় regex.
মূল উপলব্ধি: আজ আপনি যে 01[3-9]\d{8} লেখেন, তার পেছনে সত্তর বছরের গণিত। “regular” শব্দটি গণিতের — এই নকশা যা পারে আর যা পারে না, দুটোই প্রমাণিত। সীমাটা জানলেই বুঝবেন কোথায় regex, আর কোথায় parser.
প্র ০২“Some people, when confronted with a problem, think ‘I know, I’ll use regular expressions.’ Now they have two problems.” — এই বিখ্যাত রসিকতার পেছনে কী সত্য আছে?
উক্তিটি সাধারণত Jamie Zawinski-এর নামে পরিচিত — Netscape-এর প্রথম দিকের প্রোগ্রামার, যিনি ১৯৯৭ সালে একটি Usenet আলোচনায় এটি লেখেন বলে উল্লেখ পাওয়া যায়। Regex বিষয়ক বিখ্যাত বইয়ের লেখক Jeffrey Friedl পরে এর উৎস খুঁজে দেখান যে একই ধাঁচের রসিকতা আরও আগে অন্য টুল (যেমন awk) নিয়েও চালু ছিল। তাই “Zawinski বলেছিলেন” বলার চেয়ে “Zawinski-এর নামে সবচেয়ে পরিচিত” বলাই নিরাপদ।
রসিকতার মূল সত্যটা কী? Regex খুব ঘন একটি ভাষা — এক লাইনে অনেক নিয়ম। লেখার সময় সেটি বুদ্ধিদীপ্ত মনে হয়, কিন্তু ছয় মাস পরে কেউ (হয়তো আপনিই) ^(?=.*[A-Z])(?=.*\d)(?!.*(.)\1{2}).{8,}$ দেখে বুঝতে পারেন না এটি কী করে, কোন ইনপুটে ভাঙে, বা একটি নতুন নিয়ম কোথায় বসাবেন। ভুলও লুকিয়ে থাকে: একটি অনুপস্থিত anchor, একটি ভুল জায়গার ?. আর সবচেয়ে খারাপ ক্ষেত্রে নকশা নিজেই একটি নিরাপত্তা-ঝুঁকি (catastrophic backtracking). প্রথম সমস্যা ছিল লেখা যাচাই; দ্বিতীয় সমস্যা এখন একটি দুর্বোধ্য, ভঙ্গুর নকশা।
তবে উক্তিটিকে “regex ব্যবহার করবেন না” হিসেবে পড়া ভুল। ঠিক জায়গায় regex অতুলনীয়: লগ থেকে ERROR লাইন খোঁজা, ফোন নম্বরের আকৃতি যাচাই, এডিটরে একসাথে শত জায়গায় বদল। শিক্ষাটা হলো বিচারবুদ্ধি: (১) নকশা ছোট রাখুন, বড় হলে কয়েকটি ছোট নকশা বা সাধারণ কোডে ভাঙুন; (২) COMMENTS flag আর named group দিয়ে নকশাকে নিজেই নিজের ব্যাখ্যা দিতে দিন; (৩) প্রতিটি নকশার সাথে পরীক্ষার লেখার তালিকা রাখুন — পর্ব ৪৭-এর JUnit টেস্ট এখানে আদর্শ; (৪) গঠনযুক্ত ডেটায় parser.
মূল উপলব্ধি: প্রতিটি টুল একটি সমস্যা সমাধান করে আর একটি নতুন খরচ আনে। ভালো প্রকৌশলী জিজ্ঞেস করেন, “এই নকশাটি কি পরের মানুষটি পড়তে, পরীক্ষা করতে আর বদলাতে পারবে?” উত্তর “না” হলে regex নয়, অন্য পথ।
প্র ০৩Go আর Rust-এর regex কখনো বিস্ফোরিত হয় না, Java-র হতে পারে — কেন? Java কেন backtracking engine রাখে?
Regex মেলানোর দুটি মূল পদ্ধতি আছে। প্রথমটি Thompson-এর ১৯৬৮ সালের পথ: নকশাকে একটি automaton-এ রূপ দিয়ে লেখার প্রতিটি অক্ষরে সব সম্ভাব্য অবস্থা একসাথে এগিয়ে নেওয়া। এতে লেখার দৈর্ঘ্যের অনুপাতে সময় লাগে — কোনো বিস্ফোরণ নেই। দ্বিতীয়টি backtracking: একটি পথ বেছে এগোনো, ব্যর্থ হলে পিছিয়ে অন্য পথ। সাধারণ নকশায় এটি দ্রুত, কিন্তু খারাপ নকশায় সময় সূচকীয় হারে বাড়তে পারে।
২০০৭ সালে Russ Cox “Regular Expression Matching Can Be Simple And Fast” নামে একটি প্রবন্ধে দেখান যে Perl, Python, Java-র মতো জনপ্রিয় ভাষার backtracking engine কিছু নকশায় Thompson-এর পদ্ধতির চেয়ে লক্ষ লক্ষ গুণ ধীর হতে পারে। Google-এ তাঁর কাজ থেকে আসে RE2 library (২০১০ সালে প্রকাশিত), যা রৈখিক সময়ের নিশ্চয়তা দেয়। Go ভাষার regexp একই নীতিতে তৈরি, Rust-এর জনপ্রিয় regex crate-ও রৈখিক সময়ের নিশ্চয়তা দেয়।
তাহলে Java কেন backtracking? কারণ ক্ষমতা। Backreference ((\w)\1) আর lookaround ((?=…)) গাণিতিকভাবে “regular” নয় — automaton দিয়ে এগুলো সাধারণভাবে করা যায় না। RE2 আর Go তাই backreference আর lookaround সমর্থনই করে না। Java ২০০২ সালে Perl-এর ধাঁচের সমৃদ্ধ সিনট্যাক্স বেছে নিয়েছিল, আর লক্ষ লক্ষ প্রোগ্রাম এখন এর ওপর নির্ভর করে; engine বদলালে সেগুলো ভাঙবে। এর বদলে Java দিয়েছে possessive quantifier আর atomic group, আর নতুন JDK-গুলো কিছু পরিচিত নকশায় অপটিমাইজেশন যোগ করেছে (অংশ ৮-এ দেখেছেন, সবগুলোতে নয়)। .NET আরেকটি পথ নিয়েছে: .NET 7-এ একটি ঐচ্ছিক non-backtracking মোড যোগ করেছে।
মূল উপলব্ধি: এটি একটি ক্লাসিক trade-off — প্রকাশক্ষমতা বনাম পূর্বানুমেয় গতি। অবিশ্বস্ত ইনপুটে বড় পরিসরে regex চালালে (যেমন ব্যবহারকারীর দেওয়া নকশা) RE2-ধাঁচের engine (Java-তে RE2/J library আছে) বিবেচনা করুন; নিজের লেখা নকশায় Java-র engine যথেষ্ট, যদি নকশা সাবধানে লেখেন।
প্র ০৪২০১৯ সালে একটি মাত্র regex কীভাবে Cloudflare-এর বিশ্বজুড়ে সেবা প্রায় আধা ঘণ্টা বসিয়ে দিয়েছিল? এখান থেকে কী শেখার আছে?
Cloudflare একটি বড় নেটওয়ার্ক কোম্পানি, যা লক্ষ লক্ষ ওয়েবসাইটের সামনে বসে ট্রাফিক দ্রুত করে আর আক্রমণ ঠেকায়। আক্রমণ ঠেকানোর একটি অংশ হলো WAF (Web Application Firewall), যা প্রতিটি অনুরোধকে অনেকগুলো regex নিয়মের সাথে মিলিয়ে দেখে।
২০১৯ সালের ২ জুলাই তারা একটি নতুন WAF নিয়ম চালু করে। নিয়মটির regex-এ .*(?:.*=.*)-এর মতো একটি অংশ ছিল — পরপর কয়েকটি .*, যারা একই লেখা নানাভাবে ভাগ করে নিতে পারে। কোম্পানির প্রকাশিত বিশ্লেষণ (postmortem) অনুযায়ী, এই নকশা backtracking engine-এ এত বেশি কাজ তৈরি করে যে বিশ্বজুড়ে তাদের সার্ভারের CPU প্রায় পুরোপুরি দখল হয়ে যায়। প্রায় ২৭ মিনিট ধরে বহু ওয়েবসাইট ব্যবহারকারীরা error পাতা দেখেন। নিয়মটি সব জায়গায় একসাথে ছড়িয়ে দেওয়া হয়েছিল বলে ক্ষতিও একসাথে সব জায়গায় হয়।
লক্ষ করুন, এখানে কোনো বাইরের আক্রমণকারী ছিল না — কোম্পানির নিজের লেখা একটি নকশা, যা পরীক্ষায় ধরা পড়েনি। ২০১৬ সালে Stack Overflow-ও একটি ছোট সময়ের জন্য বসে গিয়েছিল একটি সাধারণ “লাইনের শেষের ফাঁকা মোছা” regex-এর কারণে, যখন একটি পোস্টে অস্বাভাবিক বেশি ফাঁকা অক্ষর ছিল।
Cloudflare-এর ঘোষিত শিক্ষাগুলো সবার জন্য প্রাসঙ্গিক: (১) regex-এর CPU ব্যবহারের ওপর সীমা রাখা; (২) নতুন নিয়ম ধাপে ধাপে ছড়ানো — আগে অল্প সার্ভারে, তারপর সবখানে; (৩) রৈখিক-সময়ের নিশ্চয়তা দেয় এমন regex engine-এ যাওয়ার পরিকল্পনা; (৪) জরুরি অবস্থায় দ্রুত নিয়ম বন্ধ করার ব্যবস্থা।
মূল উপলব্ধি: regex শুধু একটি স্ট্রিং নয়, এটি একটি ছোট প্রোগ্রাম, যার চলার সময় ইনপুটের ওপর নির্ভর করে। প্রোগ্রামের মতোই একে পরীক্ষা, পর্যালোচনা আর সীমার মধ্যে রাখতে হয় — বিশেষ করে যখন এটি লাখো অনুরোধের পথে বসে।
প্র ০৫“বাংলা অক্ষর” বলতে regex-এ ঠিক কী বোঝাব? Unicode-এ “letter” ধারণাটি বাংলার জন্য এত কঠিন কেন?
ইংরেজিতে “letter” প্রশ্নটি সহজ: a থেকে z, বড় বা ছোট। প্রতিটি দেখা অক্ষর একটি code point. বাংলায় তা নয়। “কি” চোখে একটি অক্ষর, কিন্তু Unicode-এ দুটি: ক (Letter) আর ি (Mark — vowel sign). “ক্ষ” তিনটি: ক, হসন্ত (Mark), ষ. “র্যাব”-এ অদৃশ্য zero-width joiner পর্যন্ত আছে, যার শ্রেণি Letter-ও নয়, Mark-ও নয় (Format).
ফলে regex-এ “বাংলা শব্দ” ধরার প্রতিটি সহজ উত্তরে ফাঁক থাকে (সব OpenJDK 21-এ যাচাই করা): ডিফল্ট \w+ বাংলা একেবারেই চেনে না; \p{L}+ “বাংলা” শব্দটিই মেলায় না, কারণ া আর ং Mark; [\p{L}\p{M}]+ সাধারণ শব্দ ধরে কিন্তু ZWJ-যুক্ত “র্যাব” ছাড়ে; \p{IsBengali}+ লিপি দেখে, কিন্তু দাঁড়ি “।” ধরে না, কারণ দাঁড়ি বাংলা-দেবনাগরী সহ কয়েকটি লিপির সাধারণ চিহ্ন; UNICODE_CHARACTER_CLASS-সহ \w+ সবচেয়ে উদার, কিন্তু সেটি আবার আরবি, চীনা সব লিপির অক্ষরও মেনে নেয় — শুধু বাংলা চাইলে সেটি ঠিক নয়।
আরও গভীর সমস্যা হলো একই দেখা লেখা একাধিকভাবে লেখা যায়। যেমন “য়” একটি একক code point (U+09DF) হিসেবেও লেখা যায়, আবার য + নুকতা (U+09AF U+09BC) হিসেবেও। চোখে এক, কিন্তু regex-এর কাছে আলাদা। তাই গুরুত্বপূর্ণ তুলনা বা খোঁজার আগে লেখাকে একটি নির্দিষ্ট রূপে আনতে হয় — java.text.Normalizer দিয়ে Unicode normalization. আর “দেখা অক্ষর” (grapheme cluster) ধরতে Java-র \X আছে, কিন্তু পর্ব ০৮-এ দেখেছেন বাংলা যুক্তাক্ষরে তারও সীমা আছে।
বাস্তব পরামর্শ: আপনার আসল প্রশ্নটি কী, আগে ঠিক করুন। “শুধু বাংলা লিপি” চাইলে [\p{IsBengali}\u200C\u200D]-এর মতো সুনির্দিষ্ট সেট; “যেকোনো ভাষার শব্দ” চাইলে UNICODE_CHARACTER_CLASS; আর সবক্ষেত্রে আসল বাংলা লেখা দিয়ে পরীক্ষা — “ক্ষমা”, “র্যাব”, “বাংলা।”, বাংলা অঙ্ক।
মূল উপলব্ধি: “অক্ষর” একটি মানুষের ধারণা, code point একটি যন্ত্রের। বাংলার মতো জটিল লিপিতে দুটির মাঝখানের ফাঁক বড়। যে প্রোগ্রামার এই ফাঁক বোঝেন, তাঁর সফটওয়্যারই সত্যিকার অর্থে বাংলাভাষী ব্যবহারকারীর জন্য কাজ করে।
অনুশীলন
প্রথম তিনটিতে শুধু নকশা পড়া আর ভাবা; শেষটিতে নিজে একটু কোড লিখবেন।
-
নকশা পড়ুন: নিচের প্রতিটি নকশা মুখে বলুন, আর একটি করে মেলে এমন লেখা লিখুন — (ক)
^\d{4}$(খ)[A-Z]{2}-\d{4}(গ)^(?:INFO|WARN|ERROR)\b(ঘ)\d+(?:\.\d{1,2})?(ক) শুরু থেকে শেষ পর্যন্ত ঠিক চারটি অঙ্ক — যেমন পোস্ট কোড
1207. (খ) দুটি বড় হাতের ইংরেজি অক্ষর, হাইফেন, চারটি অঙ্ক — যেমনDH-1234; anchor নেই, তাইfind()-এ লম্বা লেখার ভেতরেও পাবে। (গ) লেখার শুরুতে INFO, WARN বা ERROR, তারপর একটি শব্দের সীমা — যেমনERROR disk full;\bথাকায়ERRORSমেলে না। (ঘ) এক বা একাধিক অঙ্ক, তারপর ঐচ্ছিকভাবে একটি আসল ডট আর এক বা দুটি অঙ্ক — যেমন150,150.5,150.75. Java String-এ এগুলো লিখতে প্রতিটি\দুবার। -
Greedy-র ফাঁদ:
"name=\"Rahim\" city=\"Dhaka\""লেখা থেকে কোটের ভেতরের মান বের করতে কেউ লিখলেন"(.*)"(কোট-সহ নকশা)।find()কী পাবে? দুটি আলাদা সমাধান দিন।Greedy
.*প্রথম কোট থেকে লেখার শেষ পর্যন্ত খেয়ে পিছিয়ে আসবে শেষ কোট পর্যন্ত, তাই group 1 হবেRahim" city="Dhaka— দুটি মান জোড়া লেগে একটি। সমাধান ১: reluctant"(.*?)"— প্রতিবার পরের কোটেই থামে, দুবারfind()-এ Rahim আর Dhaka. সমাধান ২ (ভালো): negated class"([^"]*)"— “কোট ছাড়া যেকোনো অক্ষর”, backtracking ছাড়াই একই ফল আর উদ্দেশ্যও স্পষ্ট। Java String-এ লিখতে হবে"\"([^\"]*)\"". -
বিস্ফোরণ খুঁজুন: নিচের কোন নকশাগুলো লম্বা, শেষে ব্যর্থ হওয়া ইনপুটে catastrophic backtracking-এর ঝুঁকিতে আছে, আর কেন? (ক)
^\d+$(খ)^(\d+)*$(গ)^(\w+\s?)*$(ঘ)^[\w\s]*$(খ) আর (গ) ঝুঁকিপূর্ণ। (খ)-তে
\d+-এর বাইরে আবার*— “12345x”-এর মতো লেখায় অঙ্কগুলো ভাগ করার উপায় সূচকীয়। (গ)-তে\s?ঐচ্ছিক বলে বাস্তবে এটিও(\w+)*-এর মতো আচরণ করতে পারে — লম্বা একটি শব্দের পর একটি!থাকলে একই বিস্ফোরণ। (ক) আর (ঘ) নিরাপদ — একটিমাত্র quantifier, ভাগ করার একটিই উপায়। (খ) প্রায় (ক)-এর সমান লেখাই মেলায় (বাড়তি শুধু খালি লেখা), (গ) প্রায় (ঘ)-এর মতো; তাই সরল রূপটি নিন। একটি সৎ তথ্য: Wandbox-এর OpenJDK 21-এ (খ) আর (গ) ৪০ অক্ষরেও দ্রুত শেষ হয়েছে, কারণ নতুন JDK এই পরিচিত আকারগুলো অপটিমাইজ করে; কিন্তু অন্য বহু regex engine-এ (এবং পুরোনো JDK-তে) এগুলো বিপজ্জনক, আর একটু ভিন্ন আকারের নকশা JDK 21-কেও আটকে দেয় (অংশ ৮)। সরল নকশাই আসল সুরক্ষা। -
নিজে লিখুন — NID ঢাকা: একটি method লিখুন
static String maskNid(String nid), যা ১০, ১৩ বা ১৭ অঙ্কের NID নম্বর পেলে শেষ চারটি ছাড়া সব অঙ্ক*করে দেয়, আর অন্য কিছু পেলে"INVALID"ফেরত দেয়।static final Patternব্যবহার করুন।একটি সম্ভাব্য সমাধান:
static final Pattern NID = Pattern.compile("\\d{10}|\\d{13}|\\d{17}"); static String maskNid(String nid) { if (nid == null || !NID.matcher(nid).matches()) return "INVALID"; return nid.replaceAll("\\d(?=\\d{4})", "*"); } // maskNid("1987654321") → "******4321" // maskNid("12345") → "INVALID"matches()পুরো লেখা চায়, তাই alternation-এ anchor লাগেনি (find()ব্যবহার করলে^(?:…)$লাগত)।\d(?=\d{4})— “যে অঙ্কের পরে অন্তত চারটি অঙ্ক আছে” — শেষ চারটি ছাড়া সবগুলো।nullযাচাই আগে, নইলেNullPointerException. ১০ অঙ্কের উদাহরণটি যাচাই করা ফলের সাথে মেলে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমJava-তে regex দিয়ে লেখা থেকে তথ্য তোলার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ✍️ নকশাটি Java String হিসেবে লেখা —
"Tk (\\d+)" - ⚙️
Pattern.compileদিয়ে একবার compile করা - 🔎
pattern.matcher(text)দিয়ে Matcher তৈরি - 🔁
while (m.find())দিয়ে প্রতিটি মিল খোঁজা - 📦
m.group(1)দিয়ে মিলের টুকরো তোলা
Java-তে "01712345678".matches("01[3-9]\d{8}") লিখলে কী ঘটে?
- true ছাপা হয়
- compile error: illegal escape character
- চলার সময় PatternSyntaxException
- false ছাপা হয়
<b>bold</b> লেখায় Pattern.compile("<.+?>") দিয়ে প্রথম find() কোন অংশ পায়?
- <b>bold</b>
- </b>
- <b>
- কিছুই পায় না
Java-তে "a,b,,c,,".split(",").length-এর মান কত?
- 4
- 6
- 3
- 5
OpenJDK 21-এ কোনো flag ছাড়া "বাংলা".matches("\\w+")-এর ফল কী, আর কেন?
- true, কারণ \w সব ভাষার অক্ষর চেনে
- PatternSyntaxException
- false, কারণ শুধু কার-চিহ্ন \w-এ মেলে না
- false, কারণ ডিফল্টে \w শুধু ASCII অক্ষর-অঙ্ক-আন্ডারস্কোর
একটি ফর্মে মোবাইল নম্বর যাচাই করতে Pattern.compile("01[3-9]\\d{8}").matcher(input).find() ব্যবহার হলো। সমস্যা কী?
- নকশাটি compile হবে না
- “abc01712345678xyz”-এর মতো লেখাও বৈধ বলে পাস করবে
- সঠিক নম্বরও বাতিল হবে
- Matcher ছাড়া find() ডাকা যায় না
লম্বা, শেষে ব্যর্থ হওয়া ইনপুটে কোন নকশাটি catastrophic backtracking-এর সবচেয়ে বেশি ঝুঁকিতে?
- \d+
- <[^>]+>
- ((a+)+)+b
- \d{8}
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Regex লেখার আকৃতি বর্ণনার ছোট ভাষা; Java-তে Pattern (একবার compile,
static final) আর Matcher (একটি লেখায় একটি খোঁজা)। - Java String-এ backslash দুবার:
"\\d","\\."— text block-এও; হুবহু লেখার জন্যPattern.quote, বদলি লেখার জন্যMatcher.quoteReplacement. matches()পুরো লেখা,lookingAt()শুরু,find()যেকোনো জায়গা; যাচাইয়েmatches(), তথ্য তুলতেwhile (m.find())আরgroup().- Greedy বেশি খায়, reluctant কম, possessive ফেরত দেয় না; প্রায়ই সবচেয়ে ভালো
[^…].splitশেষের ফাঁকা টুকরো ফেলে — রাখতে-1. \w\d\bডিফল্টে বাংলা চেনে না; নেস্টেড quantifier এড়ান; HTML/JSON-এ parser; AI-এর regex দুই তালিকার পরীক্ষা ছাড়া গ্রহণ নয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৫ · class ও objectপরবর্তী পর্বমডিউল ৩-এর শুরু — নিজের টাইপ বানানো।
- Java প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Python-এ regular expressionতুলনা করুনraw string, re module আর Unicode-ডিফল্ট \w.
- Theory of Computationগভীরে যানregular language, finite automaton আর regex-এর গাণিতিক সীমা।
- Cybersecurityপাশের ট্র্যাকইনপুট যাচাই, ReDoS আর নিরাপদ সফটওয়্যারের নীতি।