Python দিয়ে এলোমেলো CSV পরিষ্কার: নাম, তারিখ, ফোন ও ডুপ্লিকেট
আসল data এলোমেলো। standard library দিয়ে contacts পরিষ্কার করা, প্রতিটি বাতিল row ব্যাখ্যা করা ও test-সহ একটি script বানান।

প্রতিটি data প্রজেক্ট শুরু হয় একই অনাকর্ষণীয় কাজে: পরিষ্কার করা। নামে অদ্ভুত ফাঁকা, তারিখ পাঁচ ধরনে, ফোন নম্বর দশভাবে টাইপ করা, আর একই মানুষ দুবার। এই প্রজেক্ট শুধু Python-এর standard library দিয়ে contacts CSV পরিষ্কার করার script বানায়, এবং ঠিক কোন row কেন বাতিল হলো তা জানায়। কোড ও নিচের আউটপুট প্রকাশের আগে Python 3-এ চালানো হয়েছে।
এলোমেলো ইনপুট
messy.csv:
name,email,phone,joined
rahim UDDIN ,Rahim@Example.com ,017-1234 5678,15/03/2025
KARIM hossain,karim@example.com,+8801812345678,2025-04-02
Nusrat jahan,nusrat@example,01912345678,"March 5, 2025"
Rahim Uddin,rahim@example.com,01712345678,16-03-2025
Sadia Islam,sadia@example.com,12345,10 Jan 2025
Tanvir Ahmed,tanvir@example.com,8801612345678,31/02/2025
এই ৬ row-এ লুকানো সমস্যা: বাড়তি ফাঁকা ও ভুল বড় হাতের অক্ষর, বড় হাতের অক্ষরের email, তিন ধরনের ফোন, 15/03/2025, 16-03-2025, March 5, 2025 ও 10 Jan 2025 ধাঁচের তারিখ, অবৈধ email, ডুপ্লিকেট মানুষ, খুব ছোট ফোন নম্বর এবং এমন তারিখ যা নেই (৩১ ফেব্রুয়ারি)।
Script
clean.py নামে সংরক্ষণ করুন:
#!/usr/bin/env python3
"""Clean a messy CSV of contacts: tidy names, dates and phone numbers, drop duplicates, report rejects."""
import argparse
import csv
import re
from datetime import datetime
DATE_FORMATS = ["%Y-%m-%d", "%d/%m/%Y", "%d-%m-%Y", "%d %b %Y", "%B %d, %Y"]
def clean_name(value):
return " ".join(value.split()).title()
def clean_email(value):
email = value.strip().lower()
return email if re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+", email) else None
def clean_phone(value):
"""Return a Bangladesh mobile number as +8801XXXXXXXXX, or None if it does not look valid."""
digits = re.sub(r"\D", "", value)
if digits.startswith("880"):
digits = digits[3:]
digits = digits.lstrip("0")
return "+880" + digits if re.fullmatch(r"1[3-9]\d{8}", digits) else None
def clean_date(value):
for fmt in DATE_FORMATS:
try:
return datetime.strptime(value.strip(), fmt).date().isoformat()
except ValueError:
continue
return None
def clean_rows(rows):
good, rejected, seen = [], [], set()
for number, row in enumerate(rows, start=2): # row 1 is the header
cleaned = {
"name": clean_name(row.get("name", "")),
"email": clean_email(row.get("email", "")),
"phone": clean_phone(row.get("phone", "")),
"joined": clean_date(row.get("joined", "")),
}
problems = [key for key, value in cleaned.items() if not value]
if problems:
rejected.append((number, ", ".join(problems), row))
elif cleaned["email"] in seen:
rejected.append((number, "duplicate email", row))
else:
seen.add(cleaned["email"])
good.append(cleaned)
return good, rejected
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("source")
parser.add_argument("output")
args = parser.parse_args(argv)
with open(args.source, newline="", encoding="utf-8-sig") as f:
good, rejected = clean_rows(csv.DictReader(f))
with open(args.output, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["name", "email", "phone", "joined"])
writer.writeheader()
writer.writerows(good)
print(f"{len(good)} rows kept, {len(rejected)} rejected")
for number, reason, row in rejected:
print(f" line {number}: {reason} {dict(row)}")
if __name__ == "__main__":
main()
চালান
$ python3 clean.py messy.csv clean.csv
2 rows kept, 4 rejected
line 4: email {'name': 'Nusrat jahan', 'email': 'nusrat@example', ...}
line 5: duplicate email {'name': 'Rahim Uddin', 'email': 'rahim@example.com', ...}
line 6: phone {'name': 'Sadia Islam', 'email': 'sadia@example.com', 'phone': '12345', ...}
line 7: joined {'name': 'Tanvir Ahmed', ..., 'joined': '31/02/2025'}
clean.csv-এ এখন দুটি ভালো row:
name,email,phone,joined
Rahim Uddin,rahim@example.com,+8801712345678,2025-03-15
Karim Hossain,karim@example.com,+8801812345678,2025-04-02
Rahim-এর প্রথম row পরিষ্কার হয়েছে (নাম, email, ফোন ও তারিখ সবই মানসম্মত), আর দ্বিতীয় row ডুপ্লিকেট হিসেবে বাতিল, কারণ ছোট হাতের করলে email একই।
প্রতিটি cleaner কীভাবে কাজ করে
নাম। " ".join(value.split()) যেকোনো ফাঁকার সারি এক ফাঁকায় আনে, আর .title() বড় হাতের অক্ষর ঠিক করে। সতর্কতা: .title() স্থূল। এটি McDonald-কে Mcdonald করে, o'neil-কে O'Neil করে এবং অনেক আসল নাম ভুল সামলায়। আসল ব্যক্তির data-য় স্বয়ংক্রিয়ভাবে বড় হাতের অক্ষর বদলানোর আগে সাবধান হন।
Email। ছেঁটে ফেলুন, ছোট হাতের করুন, তারপর সাধারণ প্যাটার্ন: কিছু, @, কিছু, একটি বিন্দু, কিছু। এটি nusrat@example-এর মতো স্পষ্ট ভুল ধরে। মেইলবক্স যে আছে তা এটি প্রমাণ করে না। একমাত্র সত্যিকারের প্রমাণ হলো নিশ্চিতকরণ email পাঠানো।
ফোন। অঙ্ক নয় এমন সবকিছু সরান, শুরুর 880 ও শুরুর শূন্য বাদ দিন, এবং বাকিটা বাংলাদেশি মোবাইল নম্বরের মতো দেখালেই গ্রহণ করুন: 1, তারপর ৩ থেকে ৯, তারপর আরও ৮ অঙ্ক। তারপর একটি মানসম্মত রূপ +8801712345678 দিই। এটি এই অনুশীলনের সরল ফরম্যাট পরীক্ষা, পূর্ণ ফোন validator নয়, আর অপারেটরের পরিসর বদলাতে পারে। আন্তর্জাতিক data-য় Google-এর libphonenumber-ভিত্তিক library ব্যবহার করুন।
তারিখ। datetime.strptime দিয়ে ক্রমে কয়েকটি পরিচিত ফরম্যাট চেষ্টা করি। 31/02/2025 প্রতিটিতে ব্যর্থ হয় কারণ ওই দিন নেই, তাই চুপচাপ ভুল কিছুতে না বদলে বাতিল হয়। আসল বিপদ: 03/04/2025 মানে ৩ এপ্রিল বা ৪ মার্চ হতে পারে। এখানে ইচ্ছা করে শুধু দিন-আগের ফরম্যাট দিয়েছি। data-র মালিকের সঙ্গে নিয়ম ঠিক করুন; কখনো আন্দাজ করবেন না।
ডুপ্লিকেট। প্রতিটি email set-এ (দ্রুত খোঁজ) মনে রাখি এবং একই email-এর দ্বিতীয় row বাতিল করি।
সবচেয়ে গুরুত্বপূর্ণ নকশা সিদ্ধান্ত: বাতিলগুলো জানান, লুকাবেন না
Script খারাপ row ঠিক করতে কখনো মান বানায় না এবং চুপচাপ বাদও দেয় না। এটি (লাইন নম্বর, কারণ, আসল row)-এর তালিকা রাখে ও ছাপে। এতে আপনি হাতে যাচাই করতে পারেন বা যিনি ফাইল বানিয়েছেন তাকে ফেরত দিতে পারেন। নীরব পরিষ্কার বিপজ্জনক কারণ কেউ না জেনেই ভুল হারিয়ে যায়।
Test
পরিষ্কারের function ছোট ও pure, তাই আসল এলোমেলো থেকে নেওয়া উদাহরণে test করা সহজ। test_clean.py নামে সংরক্ষণ করুন:
import unittest
import clean
class CleanTest(unittest.TestCase):
def test_phone(self):
self.assertEqual(clean.clean_phone("017-1234 5678"), "+8801712345678")
self.assertEqual(clean.clean_phone("+8801812345678"), "+8801812345678")
self.assertEqual(clean.clean_phone("8801612345678"), "+8801612345678")
self.assertIsNone(clean.clean_phone("12345"))
def test_date_formats(self):
self.assertEqual(clean.clean_date("15/03/2025"), "2025-03-15")
self.assertEqual(clean.clean_date("March 5, 2025"), "2025-03-05")
self.assertIsNone(clean.clean_date("31/02/2025")) # not a real date
def test_name_and_email(self):
self.assertEqual(clean.clean_name(" rahim UDDIN "), "Rahim Uddin")
self.assertEqual(clean.clean_email(" A@B.com "), "a@b.com")
self.assertIsNone(clean.clean_email("nope@x"))
def test_duplicates_and_rejects(self):
rows = [
{"name": "A", "email": "a@x.com", "phone": "01712345678", "joined": "2025-01-01"},
{"name": "A again", "email": "A@x.com", "phone": "01712345678", "joined": "2025-01-02"},
{"name": "B", "email": "b@x.com", "phone": "bad", "joined": "2025-01-03"},
]
good, rejected = clean.clean_rows(rows)
self.assertEqual(len(good), 1)
self.assertEqual([r[1] for r in rejected], ["duplicate email", "phone"])
if __name__ == "__main__":
unittest.main()
$ python3 -m unittest -v
Ran 4 tests in 0.013s
OK
আসল data-য় নতুন ধরনের গোলমাল পেলে আগে test হিসেবে যোগ করুন, তারপর কোড ঠিক করুন।
Data পরিষ্কারের ভালো অভ্যাস
- মূল ফাইল কখনো বদলাবেন না। আমরা যেমন করলাম, নতুন ফাইল লিখুন।
- কী বদলেছে ও কী বাতিল হয়েছে তার log রাখুন।
- আগে ও পরে গুনুন। ৬ row ঢুকল, ২ থাকল, ৪ বাতিল: হিসাব মেলে।
- Encoding সামলান।
utf-8-sigExcel-এর CSV ফাইলের শুরুতে যোগ করা অদৃশ্য চিহ্ন সামলায়। - আন্দাজ করবেন না। দ্ব্যর্থক data বাতিল করে জিজ্ঞেস করুন।
- ব্যক্তিগত data-য় সাবধান। Contact তালিকা ব্যক্তিগত তথ্য। নিরাপদে রাখুন, শুধু দরকারিটুকু রাখুন এবং আপনার প্রযোজ্য আইন মানুন।
সীমা
- এটি পুরো ফাইল মেমরিতে পড়ে, যা RAM-এ আরামে ধরে এমন ফাইলের জন্য ঠিক, কিন্তু খুব বড় ফাইলে row ধরে ধরে প্রক্রিয়া করে লিখতে লিখতে যান।
- শুধু একটি ফোন অঞ্চল ও অল্প কয়েকটি তারিখ ফরম্যাট।
- Fuzzy matching নেই: ভিন্ন email-সহ
Rahim UddinওRahim Udinভিন্ন মানুষ গণ্য হয়।
চ্যালেঞ্জ
১. বাতিল row কারণসহ ফাইলে লেখে এমন --report rejects.csv option যোগ করুন।
২. country কলাম ও প্রতি দেশের নিয়ম যোগ করুন।
৩. non-breaking space-এর মতো অদৃশ্য অক্ষর সরান।
৪. email-এর পাশাপাশি ফোন নম্বর দিয়েও ডুপ্লিকেট ধরুন।
৫. পুরো ফাইল না এনে row ধরে ধরে stream করুন।
৬. একই পরিষ্কার pandas library দিয়ে করে কোড তুলনা করুন।
মূল কথা: পরিষ্কার হলো ছোট, পরীক্ষিত function-এর pipeline। যা পারেন মানসম্মত করুন, যা পারেন না বাতিল করুন, কোনো বাতিল row লুকাবেন না, এবং মূল ফাইল অক্ষত রাখুন।
হাতে-কলমে প্রজেক্ট সিরিজ, পর্ব ২ (পুনর্ব্যবহারযোগ্য টুল): 2 নম্বর (মোট 5)। আগের লেখা: Python ও SQLite দিয়ে URL shortener বানান (test সহ).


