پروژه مدیریت مخاطبان با پایتون؛ ساخت برنامه کامل با JSON و OOP

🗓 آخرین بهروزرسانی:
در پروژه پایانی دوره پایتون سطح متوسط، یک برنامه مدیریت مخاطبان میسازیم که اطلاعات را دریافت، اعتبارسنجی، جستوجو، ویرایش و حذف میکند و پس از بستهشدن برنامه نیز آنها را در فایل JSON نگه میدارد. این پروژه ترکیبی از کلاسها، متدها، ماژولها، فایلها، مدیریت خطا، تاریخوزمان و ساختارهای داده است.
این مطلب، درس هجدهم و پایانی دوره رایگان پایتون سطح متوسط است. در درس قبلی سازنده __init__، پارامتر self، متدها و وراثت را یاد گرفتیم. اکنون تمام مهارتهای اصلی دوره را در یک برنامه واقعی به کار میبریم.
قابلیتهای پروژه مدیریت مخاطبان
- افزودن مخاطب جدید
- نمایش همه مخاطبان
- جستوجو براساس نام، شماره، ایمیل یا گروه
- ویرایش اطلاعات مخاطب
- حذف مخاطب با تأیید کاربر
- جلوگیری از ثبت شماره تلفن تکراری
- اعتبارسنجی نام، شماره تلفن و ایمیل
- ذخیره اطلاعات در فایل JSON
- بازیابی خودکار اطلاعات هنگام اجرای برنامه
- مدیریت فایل خراب یا داده نامعتبر
- ثبت شناسه یکتا و زمان ایجاد هر مخاطب
مهارتهایی که در این پروژه استفاده میشوند
| مبحث | کاربرد در پروژه |
|---|---|
| لیست و دیکشنری | نگهداری مجموعه مخاطبان و تبدیل اطلاعات |
| توابع | ساخت منو و رابط خط فرمان |
| مدیریت خطا | کنترل خطاهای فایل و ورودی نامعتبر |
| فایل JSON | ذخیره دائمی اطلاعات |
| ماژولها | تقسیم پروژه به چند فایل |
| pathlib | مدیریت مسیر فایل داده |
| datetime | ثبت زمان ایجاد مخاطب در UTC |
| کلاس و شیء | مدلسازی مخاطب و دفترچه مخاطبان |
| classmethod | ساخت شیء مخاطب از دیکشنری JSON |
ساختار پوشههای پروژه
یک پوشه با نام contact_manager بسازید و فایلها را به این شکل قرار دهید:
contact_manager/
├── main.py
├── contact.py
├── contact_book.py
└── data/
└── contacts.jsonفایل contacts.json لازم نیست از ابتدا وجود داشته باشد؛ برنامه هنگام اولین ذخیره، پوشه و فایل موردنیاز را میسازد.
مرحله اول: طراحی کلاس Contact
فایل contact.py مسئول مدلسازی یک مخاطب است. هر مخاطب نام، شماره تلفن، ایمیل، گروه، شناسه یکتا و زمان ایجاد دارد.
from datetime import datetime, timezone
from uuid import uuid4
class Contact:
def __init__(
self,
name,
phone,
email="",
group="عمومی",
contact_id=None,
created_at=None,
):
self.name = self.validate_name(name)
self.phone = self.validate_phone(phone)
self.email = self.validate_email(email)
self.group = group.strip() or "عمومی"
self.id = contact_id or str(uuid4())
self.created_at = created_at or datetime.now(
timezone.utc
).isoformat(timespec="seconds")
@staticmethod
def validate_name(value):
name = value.strip()
if len(name) < 2:
raise ValueError(
"نام مخاطب باید حداقل دو نویسه داشته باشد."
)
return name
@staticmethod
def validate_phone(value):
phone = (
value.strip()
.replace(" ", "")
.replace("-", "")
.replace("(", "")
.replace(")", "")
)
digits = phone[1:] if phone.startswith("+") else phone
if not digits.isdigit():
raise ValueError(
"شماره تلفن فقط میتواند شامل عدد و + ابتدایی باشد."
)
if not 7 <= len(digits) <= 15:
raise ValueError(
"شماره تلفن باید بین ۷ تا ۱۵ رقم داشته باشد."
)
return phone
@staticmethod
def validate_email(value):
email = value.strip().lower()
if not email:
return ""
if (
"@" not in email
or email.startswith("@")
or email.endswith("@")
):
raise ValueError("ساختار ایمیل معتبر نیست.")
local_part, domain = email.rsplit("@", 1)
if not local_part or "." not in domain:
raise ValueError("ساختار ایمیل معتبر نیست.")
return email
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"phone": self.phone,
"email": self.email,
"group": self.group,
"created_at": self.created_at,
}
@classmethod
def from_dict(cls, data):
return cls(
name=data["name"],
phone=data["phone"],
email=data.get("email", ""),
group=data.get("group", "عمومی"),
contact_id=data.get("id"),
created_at=data.get("created_at"),
)
def __str__(self):
email_text = self.email or "بدون ایمیل"
return (
f"[{self.id[:8]}] {self.name} | "
f"{self.phone} | {email_text} | {self.group}"
)تحلیل کلاس Contact
سازنده کلاس تمام مقادیر ورودی را پیش از ذخیره اعتبارسنجی میکند. به این ترتیب، یک شیء مخاطب نامعتبر وارد برنامه نمیشود.
uuid4()برای هر مخاطب یک شناسه یکتا میسازد.datetime.now(timezone.utc)زمان ایجاد را بهصورت UTC ثبت میکند.to_dict()شیء را برای ذخیره در JSON به دیکشنری تبدیل میکند.from_dict()دیکشنری خواندهشده از فایل را دوباره به شیء تبدیل میکند.__str__()نمایش خوانای مخاطب را تولید میکند.
اعتبارسنجی ایمیل در این پروژه
اعتبارسنجی ایمیل در این نسخه ساده و آموزشی است و فقط ساختار کلی را بررسی میکند. تشخیص کامل همه حالتهای مجاز ایمیل بسیار پیچیدهتر است. در پروژههای واقعی معمولاً از کتابخانه معتبر یا ارسال ایمیل تأیید استفاده میشود.
مرحله دوم: ساخت کلاس ContactBook
فایل contact_book.py مدیریت مجموعه مخاطبان و ذخیرهسازی JSON را برعهده دارد:
import json
from pathlib import Path
from contact import Contact
class ContactBook:
def __init__(self, file_path="data/contacts.json"):
self.file_path = Path(file_path)
self.contacts = []
def load(self):
if not self.file_path.exists():
self.contacts = []
return
try:
with self.file_path.open(
"r", encoding="utf-8"
) as file:
raw_data = json.load(file)
if not isinstance(raw_data, list):
raise ValueError(
"ساختار اصلی فایل باید یک لیست باشد."
)
loaded_contacts = []
for index, item in enumerate(raw_data, start=1):
try:
loaded_contacts.append(
Contact.from_dict(item)
)
except (KeyError, TypeError, ValueError) as error:
print(
f"رکورد نامعتبر شماره {index} رد شد: "
f"{error}"
)
self.contacts = loaded_contacts
except json.JSONDecodeError:
print("فایل JSON خراب یا ناقص است.")
self.contacts = []
except (OSError, ValueError) as error:
print(f"خواندن اطلاعات ممکن نشد: {error}")
self.contacts = []
def save(self):
self.file_path.parent.mkdir(
parents=True,
exist_ok=True,
)
data = [
contact.to_dict()
for contact in self.contacts
]
try:
with self.file_path.open(
"w", encoding="utf-8"
) as file:
json.dump(
data,
file,
ensure_ascii=False,
indent=2,
)
except OSError as error:
raise OSError(
f"ذخیره اطلاعات ممکن نشد: {error}"
) from error
def _phone_exists(self, phone, ignored_id=None):
normalized_phone = Contact.validate_phone(phone)
return any(
contact.phone == normalized_phone
and contact.id != ignored_id
for contact in self.contacts
)
def add_contact(self, name, phone, email="", group="عمومی"):
contact = Contact(name, phone, email, group)
if self._phone_exists(contact.phone):
raise ValueError(
"مخاطبی با این شماره تلفن وجود دارد."
)
self.contacts.append(contact)
return contact
def get_all(self):
return sorted(
self.contacts,
key=lambda contact: contact.name.casefold(),
)
def find_by_id(self, value):
value = value.strip().lower()
matches = [
contact
for contact in self.contacts
if contact.id.lower() == value
or contact.id.lower().startswith(value)
]
if len(matches) == 1:
return matches[0]
return None
def search(self, query):
query = query.strip().casefold()
if not query:
return []
results = []
for contact in self.contacts:
searchable_values = (
contact.name,
contact.phone,
contact.email,
contact.group,
)
if any(
query in value.casefold()
for value in searchable_values
):
results.append(contact)
return sorted(
results,
key=lambda contact: contact.name.casefold(),
)
def update_contact(
self,
contact_id,
name=None,
phone=None,
email=None,
group=None,
):
current = self.find_by_id(contact_id)
if current is None:
raise ValueError("مخاطب پیدا نشد.")
new_name = current.name if name is None else name
new_phone = current.phone if phone is None else phone
new_email = current.email if email is None else email
new_group = current.group if group is None else group
updated = Contact(
name=new_name,
phone=new_phone,
email=new_email,
group=new_group,
contact_id=current.id,
created_at=current.created_at,
)
if self._phone_exists(
updated.phone,
ignored_id=current.id,
):
raise ValueError(
"مخاطب دیگری با این شماره وجود دارد."
)
index = self.contacts.index(current)
self.contacts[index] = updated
return updated
def delete_contact(self, contact_id):
contact = self.find_by_id(contact_id)
if contact is None:
raise ValueError("مخاطب پیدا نشد.")
self.contacts.remove(contact)
return contactچرا مسئولیتها را در دو کلاس جدا کردیم؟
کلاس Contact فقط مسئول اطلاعات و اعتبار یک مخاطب است. کلاس ContactBook مجموعه مخاطبان، جستوجو، جلوگیری از تکرار و ذخیره فایل را مدیریت میکند.
این جداسازی باعث میشود هر کلاس یک مسئولیت اصلی داشته باشد و تغییر یا آزمایش بخشهای برنامه سادهتر شود.
جستوجو با casefold
متد casefold() برای مقایسه متن بدون حساسیت به حروف بزرگ و کوچک استفاده شده است. این متد برای متنهای بینالمللی از lower() قویتر است.
شناسه کوتاه مخاطب
شناسه اصلی یک UUID کامل است، اما هنگام نمایش فقط هشت نویسه نخست آن نشان داده میشود. متد find_by_id() هم شناسه کامل و هم ابتدای یکتای آن را میپذیرد.
اگر چند شناسه با پیشوند واردشده مطابقت داشته باشند، متد نتیجهای برنمیگرداند تا مخاطب اشتباهی ویرایش یا حذف نشود.
مرحله سوم: ساخت رابط خط فرمان
فایل main.py منو و ارتباط کاربر با کلاسها را مدیریت میکند:
from contact_book import ContactBook
def show_menu():
print("\n" + "=" * 45)
print("مدیریت مخاطبان")
print("=" * 45)
print("1. افزودن مخاطب")
print("2. نمایش همه مخاطبان")
print("3. جستوجوی مخاطب")
print("4. ویرایش مخاطب")
print("5. حذف مخاطب")
print("0. خروج")
def print_contacts(contacts):
if not contacts:
print("مخاطبی برای نمایش وجود ندارد.")
return
print("\n" + "-" * 70)
for contact in contacts:
print(contact)
print("-" * 70)
def add_contact_ui(book):
print("\nافزودن مخاطب جدید")
name = input("نام: ").strip()
phone = input("شماره تلفن: ").strip()
email = input("ایمیل اختیاری: ").strip()
group = input("گروه اختیاری: ").strip()
contact = book.add_contact(
name=name,
phone=phone,
email=email,
group=group or "عمومی",
)
book.save()
print(f"مخاطب {contact.name} با موفقیت ثبت شد.")
def list_contacts_ui(book):
print_contacts(book.get_all())
def search_contacts_ui(book):
query = input(
"نام، شماره، ایمیل یا گروه را وارد کنید: "
)
print_contacts(book.search(query))
def update_contact_ui(book):
print_contacts(book.get_all())
contact_id = input(
"شناسه کامل یا ۸ نویسه نخست مخاطب: "
).strip()
contact = book.find_by_id(contact_id)
if contact is None:
raise ValueError(
"مخاطب پیدا نشد یا شناسه واردشده یکتا نیست."
)
print("برای نگهداشتن مقدار قبلی، Enter بزنید.")
print("برای حذف ایمیل، یک خط تیره وارد کنید.")
name = input(f"نام [{contact.name}]: ").strip()
phone = input(f"شماره [{contact.phone}]: ").strip()
email = input(
f"ایمیل [{contact.email or 'خالی'}]: "
).strip()
group = input(f"گروه [{contact.group}]: ").strip()
if email == "-":
email_value = ""
elif email:
email_value = email
else:
email_value = None
updated = book.update_contact(
contact_id=contact.id,
name=name or None,
phone=phone or None,
email=email_value,
group=group or None,
)
book.save()
print(f"اطلاعات {updated.name} بهروزرسانی شد.")
def delete_contact_ui(book):
print_contacts(book.get_all())
contact_id = input(
"شناسه کامل یا ۸ نویسه نخست مخاطب: "
).strip()
contact = book.find_by_id(contact_id)
if contact is None:
raise ValueError(
"مخاطب پیدا نشد یا شناسه واردشده یکتا نیست."
)
confirmation = input(
f"حذف {contact.name}؟ (y/n): "
).strip().lower()
if confirmation != "y":
print("حذف لغو شد.")
return
deleted = book.delete_contact(contact.id)
book.save()
print(f"مخاطب {deleted.name} حذف شد.")
def main():
book = ContactBook()
book.load()
actions = {
"1": add_contact_ui,
"2": list_contacts_ui,
"3": search_contacts_ui,
"4": update_contact_ui,
"5": delete_contact_ui,
}
while True:
show_menu()
choice = input("انتخاب شما: ").strip()
if choice == "0":
print("اطلاعات ذخیره شد. خداحافظ.")
break
action = actions.get(choice)
if action is None:
print("گزینه نامعتبر است.")
continue
try:
action(book)
except (ValueError, OSError) as error:
print(f"خطا: {error}")
except KeyboardInterrupt:
print("\nعملیات توسط کاربر لغو شد.")
if __name__ == "__main__":
main()اجرای پروژه
ترمینال را در پوشه اصلی پروژه باز و برنامه را اجرا کنید:
python main.pyدر بعضی سیستمها باید از یکی از دستورهای زیر استفاده کنید:
py main.py
python3 main.pyنمونه اجرای برنامه
=============================================
مدیریت مخاطبان
=============================================
1. افزودن مخاطب
2. نمایش همه مخاطبان
3. جستوجوی مخاطب
4. ویرایش مخاطب
5. حذف مخاطب
0. خروج
انتخاب شما: 1
افزودن مخاطب جدید
نام: سارا احمدی
شماره تلفن: +905551234567
ایمیل اختیاری: sara@example.com
گروه اختیاری: همکاران
مخاطب سارا احمدی با موفقیت ثبت شد.نمونه فایل contacts.json
پس از افزودن مخاطب، فایل داده تقریباً چنین ساختاری خواهد داشت:
[
{
"id": "d8aaf8ca-75de-4b0e-bc8c-037a21ea7adc",
"name": "سارا احمدی",
"phone": "+905551234567",
"email": "sara@example.com",
"group": "همکاران",
"created_at": "2026-07-20T08:30:00+00:00"
}
]شناسه و زمان واقعی در هر اجرا متفاوت خواهند بود.
جریان اجرای برنامه
- تابع
main()یک شیءContactBookمیسازد. - متد
load()اطلاعات قبلی را از JSON بازیابی میکند. - منو در یک حلقه نمایش داده میشود.
- انتخاب کاربر با دیکشنری
actionsبه تابع مناسب متصل میشود. - پس از افزودن، ویرایش یا حذف، متد
save()اجرا میشود. - هنگام خروج، اطلاعات از قبل در فایل ثبت شدهاند.
چرا پس از هر تغییر ذخیره میکنیم؟
اگر فقط هنگام خروج ذخیره کنیم، بستهشدن ناگهانی برنامه یا قطع برق میتواند تغییرات نشست جاری را از بین ببرد. ذخیره پس از هر عملیات تغییردهنده، ریسک ازدسترفتن داده را کاهش میدهد.
در برنامههای بزرگتر میتوان از پایگاه داده، تراکنش و ذخیره اتمیک استفاده کرد.
مدیریت فایل JSON خراب
اگر فایل JSON ناقص یا خراب باشد، json.load() خطای JSONDecodeError ایجاد میکند. برنامه این خطا را کنترل میکند و با فهرست خالی ادامه میدهد.
در نسخه حرفهایتر بهتر است پیش از جایگزینی اطلاعات، از فایل قبلی نسخه پشتیبان تهیه شود و فایل خراب برای بررسی حفظ گردد.
چرا رکوردهای نامعتبر را جداگانه رد میکنیم؟
ممکن است فایل JSON از نظر ساختاری سالم باشد، اما یکی از مخاطبان شماره یا نام نامعتبر داشته باشد. حلقه بارگذاری هر رکورد را جداگانه تبدیل میکند؛ بنابراین خراببودن یک رکورد باعث ازدسترفتن همه مخاطبان سالم نمیشود.
بهبود امنیت و حریم خصوصی
فایل JSON این پروژه رمزگذاری نشده است. هر فردی که به فایل دسترسی داشته باشد میتواند شمارهها و ایمیلها را بخواند. برای اطلاعات واقعی و حساس باید موارد زیر بررسی شوند:
- محدودکردن دسترسی فایل در سیستمعامل
- رمزگذاری اطلاعات حساس
- استفاده از پایگاه داده با کنترل دسترسی
- تهیه نسخه پشتیبان امن
- جمعآوری حداقل اطلاعات موردنیاز
- رعایت قوانین حریم خصوصی محل فعالیت
آزمایشهای ضروری پروژه
پیش از تکمیل پروژه، این سناریوها را بررسی کنید:
- افزودن مخاطب معتبر
- ثبت نام خالی یا یکحرفی
- ثبت شماره دارای حروف
- ثبت دوباره یک شماره تلفن
- ثبت ایمیل نامعتبر
- جستوجو با بخشی از نام
- جستوجو با گروه
- ویرایش شماره به شماره یک مخاطب دیگر
- حذف با شناسه صحیح و غلط
- لغو حذف با پاسخ غیر از y
- اجرای برنامه بدون وجود فایل JSON
- اجرای برنامه با فایل JSON خراب
- بستن و اجرای مجدد برای اطمینان از ماندگاری اطلاعات
اشتباهات رایج در پروژه
- فراموشکردن
ensure_ascii=Falseو ناخواناشدن متن فارسی در JSON - بازکردن فایل بدون
encoding="utf-8" - ذخیره مستقیم اشیاء در JSON بدون تبدیل به دیکشنری
- اعتماد کامل به ورودی کاربر بدون اعتبارسنجی
- استفاده از نام مخاطب بهعنوان شناسه یکتا
- ویرایش یا حذف براساس شماره فهرست متغیر
- ثبت شماره تکراری
- قرار دادن تمام کدها در یک تابع بسیار بزرگ
- مدیریتنکردن خطای فایل
- ذخیره اطلاعات حساس بدون توجه به دسترسی فایل
تمرینهای توسعه پروژه
- مرتبسازی مخاطبان براساس گروه یا زمان ایجاد را اضافه کنید.
- امکان نمایش فقط مخاطبان یک گروه را بسازید.
- خروجی CSV برای انتقال اطلاعات ایجاد کنید.
- نسخه پشتیبان خودکار از فایل JSON بسازید.
- ویژگی تاریخ آخرین ویرایش را اضافه کنید.
- شمارههای ضروری را بهعنوان مخاطب محبوب علامتگذاری کنید.
- آزمایشهای واحد برای کلاسهای
ContactوContactBookبنویسید. - ذخیرهسازی را از JSON به SQLite منتقل کنید.
- رابط گرافیکی پروژه را با Tkinter بسازید.
- یک API برای مخاطبان با FastAPI طراحی کنید.
معیارهای تکمیل پروژه
پروژه زمانی کامل محسوب میشود که:
- بدون فایل اولیه اجرا شود.
- مخاطب معتبر را ثبت کند.
- ورودی نامعتبر را با پیام مناسب رد کند.
- شماره تکراری را نپذیرد.
- اطلاعات را پس از اجرای مجدد بازیابی کند.
- جستوجو، ویرایش و حذف بهدرستی کار کنند.
- کد به فایلها و مسئولیتهای واضح تقسیم شده باشد.
- خطاهای متداول باعث بستهشدن ناگهانی برنامه نشوند.
جمعبندی پروژه
در پروژه مدیریت مخاطبان، یک برنامه کامل خط فرمان ساختیم که اطلاعات را با کلاسها مدلسازی میکند، با متدها رفتارهای مرتبط را مدیریت میکند، دادهها را در JSON ذخیره میکند و خطاهای فایل و ورودی را کنترل میکند.
کلاس Contact مسئول اعتبار و تبدیل اطلاعات هر مخاطب است، کلاس ContactBook مجموعه مخاطبان و عملیات اصلی را مدیریت میکند و فایل main.py رابط کاربر را در اختیار دارد. این تقسیم مسئولیت، پایه طراحی برنامههای قابل نگهداری و توسعهپذیر است.
پایان دوره پایتون سطح متوسط
با تکمیل این پروژه، تمام هجده درس دوره پایتون سطح متوسط را پشت سر گذاشتهاید. در این دوره با ساختارهای داده پیشرفتهتر، توابع، مدیریت خطا، فایلها، JSON، ماژولها، محیط مجازی، تاریخوزمان و برنامهنویسی شیگرا کار کردید.
مرحله بعدی میتواند انتخاب یکی از مسیرهای تخصصی مانند توسعه وب با Django یا FastAPI، تحلیل داده، خودکارسازی، ساخت ابزارهای دسکتاپ یا یادگیری عمیقتر طراحی نرمافزار و آزمایشنویسی باشد.
ادامه مسیر یادگیری پایتون
برای یادگیری منظم، آموزشهای مرتبط زیر را بهترتیب دنبال کنید:
📌 همچنین بخوانید: بازار کار پایتون در ایران، دستورهای break، continue و pass در پایتون و ساخت ربات تلگرام با پایتون
برای ذخیره پیشرفت در همه دستگاهها و دریافت گواهی پایان مسیر، رایگان ثبتنام کن یا وارد شو.