تمرین و پروژه‌های پایتون

ساخت برنامه حضور و غیاب با پایتون؛ پروژه کامل با JSON و گزارش حضور

پروژه حضور و غیاب با پایتون

🗓 آخرین به‌روزرسانی:

ساخت برنامه حضور و غیاب با پایتون یکی از پروژه‌های کاربردی برای تمرین مدیریت داده، تاریخ، فایل JSON، جست‌وجو، گزارش‌گیری و اعتبارسنجی ورودی است. در این آموزش یک سیستم ساده اما قابل توسعه می‌سازیم که بتواند دانش‌آموز، کارمند یا عضو یک مجموعه را ثبت کند، حضور و غیاب روزانه را ذخیره کند، از ثبت رکورد تکراری جلوگیری کند، گزارش روزانه و فردی بسازد و درصد حضور هر فرد را محاسبه کند.

این مقاله شانزدهمین پروژه فاز اول دسته تمرین و پروژه‌های پایتون است. اگر پروژه مدیریت نمرات دانش‌آموزان با پایتون را انجام داده باشید، ساختار ثبت افراد با شناسه یکتا برایتان آشناست. همچنین برای ذخیره دائمی داده‌ها از همان الگویی استفاده می‌کنیم که در آموزش کار با فایل‌های JSON در پایتون بررسی شده است.

در این پروژه چه چیزی می‌سازیم؟

  • ثبت فرد جدید با نام و شناسه یکتا
  • نمایش فهرست افراد
  • ثبت وضعیت حاضر برای یک تاریخ مشخص
  • ثبت وضعیت غایب برای یک تاریخ مشخص
  • ثبت خودکار تاریخ امروز
  • اعتبارسنجی تاریخ با فرمت YYYY-MM-DD
  • جلوگیری از ثبت دو رکورد برای یک فرد در یک روز
  • گزارش حضور و غیاب یک روز مشخص
  • گزارش کامل یک فرد
  • محاسبه تعداد روزهای حضور
  • محاسبه تعداد غیبت‌ها
  • محاسبه درصد حضور
  • رتبه‌بندی افراد بر اساس تعداد غیبت
  • حذف فرد و رکوردهای مربوط به او
  • ذخیره دائمی اطلاعات در JSON
  • مدیریت فایل خراب یا نامعتبر

ساختار داده پروژه حضور و غیاب

برای اینکه اطلاعات منظم و قابل توسعه باشند، داده‌ها را به دو بخش تقسیم می‌کنیم: فهرست افراد و فهرست رکوردهای حضور و غیاب.

{
    "people": [
        {
            "id": 1,
            "name": "علی رضایی"
        }
    ],
    "records": [
        {
            "person_id": 1,
            "date": "2026-08-13",
            "status": "حاضر"
        }
    ]
}

به‌جای اینکه نام فرد را داخل هر رکورد دوباره ذخیره کنیم، فقط person_id را نگه می‌داریم. این کار داده‌های تکراری را کاهش می‌دهد و اگر نام فرد تغییر کند، لازم نیست تمام رکوردهای قبلی را ویرایش کنیم.

مرحله اول: ساخت فایل پروژه

یک فایل با نام زیر ایجاد کنید:

attendance_manager.py

اطلاعات برنامه نیز داخل فایل زیر ذخیره خواهند شد:

attendance.json

مرحله دوم: وارد کردن ماژول‌های لازم

import json
from pathlib import Path
from datetime import date

ماژول json برای خواندن و نوشتن اطلاعات، Path برای مدیریت فایل و date برای ثبت تاریخ امروز استفاده می‌شود. برای تمرین بیشتر روی تاریخ می‌توانید مقاله تاریخ و زمان در پایتون با datetime را هم بخوانید.

مرحله سوم: تعریف فایل اطلاعات

DATA_FILE = Path("attendance.json")

مرحله چهارم: بارگذاری داده‌ها از JSON

def load_data():
    if not DATA_FILE.exists():
        return {
            "people": [],
            "records": [],
        }

    try:
        with DATA_FILE.open(
            "r",
            encoding="utf-8",
        ) as file:
            data = json.load(file)
    except (
        json.JSONDecodeError,
        OSError,
    ):
        print(
            "فایل اطلاعات قابل خواندن نیست."
        )
        return {
            "people": [],
            "records": [],
        }

    if not isinstance(data, dict):
        return {
            "people": [],
            "records": [],
        }

    data.setdefault("people", [])
    data.setdefault("records", [])

    return data

تابع setdefault() کمک می‌کند حتی اگر یکی از کلیدهای اصلی داخل فایل وجود نداشته باشد، برنامه ساختار لازم را به‌صورت خودکار ایجاد کند.

مرحله پنجم: ذخیره اطلاعات

def save_data(data):
    with DATA_FILE.open(
        "w",
        encoding="utf-8",
    ) as file:
        json.dump(
            data,
            file,
            ensure_ascii=False,
            indent=2,
        )

گزینه ensure_ascii=False باعث می‌شود نام‌ها و وضعیت‌های فارسی داخل JSON خوانا ذخیره شوند.

مرحله ششم: ایجاد شناسه یکتا برای افراد

def next_person_id(people):
    return max(
        (
            person.get("id", 0)
            for person in people
        ),
        default=0,
    ) + 1

این روش نسبت به len(people) + 1 مطمئن‌تر است، چون حذف یک فرد باعث تکراری شدن شناسه بعدی نمی‌شود.

مرحله هفتم: ثبت فرد جدید

def add_person(data):
    name = input(
        "نام: "
    ).strip()

    if not name:
        print(
            "نام نمی‌تواند خالی باشد."
        )
        return

    person = {
        "id": next_person_id(
            data["people"]
        ),
        "name": name,
    }

    data["people"].append(person)
    save_data(data)

    print(
        f"فرد با شناسه {person['id']} ثبت شد."
    )

مرحله هشتم: پیدا کردن فرد با شناسه

def find_person(data, person_id):
    for person in data["people"]:
        if person.get("id") == person_id:
            return person

    return None

این تابع در ثبت حضور، ثبت غیبت، گزارش فردی و حذف فرد استفاده خواهد شد.

مرحله نهم: دریافت شناسه معتبر

def get_person_from_input(data):
    value = input(
        "شناسه فرد: "
    ).strip()

    try:
        person_id = int(value)
    except ValueError:
        print(
            "شناسه باید عدد صحیح باشد."
        )
        return None

    person = find_person(
        data,
        person_id,
    )

    if person is None:
        print("فرد پیدا نشد.")

    return person

مرحله دهم: جلوگیری از رکورد تکراری

یک فرد نباید در یک تاریخ هم‌زمان دو وضعیت داشته باشد. بنابراین قبل از ثبت، وجود رکورد همان روز را بررسی می‌کنیم:

def attendance_exists(
    data,
    person_id,
    day,
):
    return any(
        record.get("person_id")
        == person_id
        and record.get("date") == day
        for record in data["records"]
    )

مرحله یازدهم: ثبت حضور یا غیبت

به‌جای نوشتن دو تابع تقریباً یکسان، یک تابع عمومی می‌سازیم که وضعیت را به‌عنوان پارامتر دریافت کند:

def mark_attendance(data, status):
    person = get_person_from_input(data)

    if person is None:
        return

    day = input(
        "تاریخ YYYY-MM-DD "
        "(خالی = امروز): "
    ).strip()

    if not day:
        day = date.today().isoformat()
    else:
        try:
            date.fromisoformat(day)
        except ValueError:
            print("تاریخ معتبر نیست.")
            return

    if attendance_exists(
        data,
        person["id"],
        day,
    ):
        print(
            "برای این فرد در این تاریخ "
            "قبلاً رکورد ثبت شده است."
        )
        return

    data["records"].append({
        "person_id": person["id"],
        "date": day,
        "status": status,
    })

    save_data(data)
    print(
        "رکورد حضور و غیاب ثبت شد."
    )

برای ثبت حضور از mark_attendance(data, "حاضر") و برای غیبت از mark_attendance(data, "غایب") استفاده می‌کنیم.

مرحله دوازدهم: نمایش افراد

def list_people(data):
    if not data["people"]:
        print(
            "هنوز فردی ثبت نشده است."
        )
        return

    for person in data["people"]:
        print(
            f"#{person['id']} | "
            f"{person['name']}"
        )

مرحله سیزدهم: گزارش روزانه

def day_report(data):
    day = input(
        "تاریخ گزارش YYYY-MM-DD "
        "(خالی = امروز): "
    ).strip()

    if not day:
        day = date.today().isoformat()

    rows = [
        record
        for record in data["records"]
        if record.get("date") == day
    ]

    if not rows:
        print(
            "برای این تاریخ رکوردی وجود ندارد."
        )
        return

    print(
        f"\n=== گزارش {day} ==="
    )

    for record in rows:
        person = find_person(
            data,
            record["person_id"],
        )

        name = (
            person["name"]
            if person
            else "نامشخص"
        )

        print(
            f"{name} | {record['status']}"
        )

مرحله چهاردهم: محاسبه درصد حضور هر فرد

برای گزارش فردی، تعداد رکوردهای حاضر را بر کل روزهای ثبت‌شده تقسیم می‌کنیم:

def person_report(data):
    person = get_person_from_input(data)

    if person is None:
        return

    rows = [
        record
        for record in data["records"]
        if record.get("person_id")
        == person["id"]
    ]

    if not rows:
        print(
            "برای این فرد رکوردی وجود ندارد."
        )
        return

    present = sum(
        1
        for record in rows
        if record.get("status") == "حاضر"
    )

    absent = sum(
        1
        for record in rows
        if record.get("status") == "غایب"
    )

    total = len(rows)
    percentage = (
        present / total
    ) * 100

    print(
        f"\n=== گزارش {person['name']} ==="
    )
    print(
        f"کل روزهای ثبت‌شده: {total}"
    )
    print(f"حاضر: {present}")
    print(f"غایب: {absent}")
    print(
        f"درصد حضور: {percentage:.1f}%"
    )

برای مثال اگر فردی در ۲۰ روز ثبت‌شده، ۱۸ روز حاضر باشد، درصد حضور او ۹۰ درصد خواهد بود.

مرحله پانزدهم: گزارش بیشترین غیبت

def absence_report(data):
    counts = {}

    for record in data["records"]:
        if record.get("status") == "غایب":
            person_id = record.get(
                "person_id"
            )
            counts[person_id] = (
                counts.get(person_id, 0)
                + 1
            )

    rows = []

    for person_id, count in counts.items():
        person = find_person(
            data,
            person_id,
        )

        name = (
            person["name"]
            if person
            else "نامشخص"
        )

        rows.append((name, count))

    rows.sort(
        key=lambda item: item[1],
        reverse=True,
    )

    if not rows:
        print(
            "هنوز غیبتی ثبت نشده است."
        )
        return

    print("\n=== بیشترین غیبت ===")

    for name, count in rows:
        print(f"{name}: {count}")

مرحله شانزدهم: حذف فرد و رکوردهای او

اگر فردی حذف شود، باقی ماندن رکوردهای قدیمی او باعث ایجاد داده یتیم می‌شود. بنابراین رکوردهای مرتبط را هم حذف می‌کنیم:

def delete_person(data):
    person = get_person_from_input(data)

    if person is None:
        return

    data["people"].remove(person)

    data["records"] = [
        record
        for record in data["records"]
        if record.get("person_id")
        != person["id"]
    ]

    save_data(data)
    print(
        "فرد و رکوردهای مربوط به او حذف شدند."
    )

کد کامل برنامه حضور و غیاب با پایتون

import json
from pathlib import Path
from datetime import date

DATA_FILE = Path("attendance.json")


def load_data():
    if not DATA_FILE.exists():
        return {"people": [], "records": []}

    try:
        with DATA_FILE.open(
            "r",
            encoding="utf-8",
        ) as file:
            data = json.load(file)
    except (
        json.JSONDecodeError,
        OSError,
    ):
        print(
            "فایل اطلاعات قابل خواندن نیست."
        )
        return {"people": [], "records": []}

    if not isinstance(data, dict):
        return {"people": [], "records": []}

    data.setdefault("people", [])
    data.setdefault("records", [])
    return data


def save_data(data):
    with DATA_FILE.open(
        "w",
        encoding="utf-8",
    ) as file:
        json.dump(
            data,
            file,
            ensure_ascii=False,
            indent=2,
        )


def next_person_id(people):
    return max(
        (
            person.get("id", 0)
            for person in people
        ),
        default=0,
    ) + 1


def add_person(data):
    name = input("نام: ").strip()

    if not name:
        print("نام نمی‌تواند خالی باشد.")
        return

    person = {
        "id": next_person_id(data["people"]),
        "name": name,
    }

    data["people"].append(person)
    save_data(data)

    print(
        f"فرد با شناسه {person['id']} ثبت شد."
    )


def find_person(data, person_id):
    for person in data["people"]:
        if person.get("id") == person_id:
            return person

    return None


def get_person_from_input(data):
    value = input("شناسه فرد: ").strip()

    try:
        person_id = int(value)
    except ValueError:
        print("شناسه باید عدد صحیح باشد.")
        return None

    person = find_person(data, person_id)

    if person is None:
        print("فرد پیدا نشد.")

    return person


def attendance_exists(data, person_id, day):
    return any(
        record.get("person_id") == person_id
        and record.get("date") == day
        for record in data["records"]
    )


def mark_attendance(data, status):
    person = get_person_from_input(data)

    if person is None:
        return

    day = input(
        "تاریخ YYYY-MM-DD (خالی = امروز): "
    ).strip()

    if not day:
        day = date.today().isoformat()
    else:
        try:
            date.fromisoformat(day)
        except ValueError:
            print("تاریخ معتبر نیست.")
            return

    if attendance_exists(
        data,
        person["id"],
        day,
    ):
        print(
            "برای این فرد در این تاریخ "
            "قبلاً رکورد ثبت شده است."
        )
        return

    data["records"].append({
        "person_id": person["id"],
        "date": day,
        "status": status,
    })

    save_data(data)
    print("رکورد حضور و غیاب ثبت شد.")


def list_people(data):
    if not data["people"]:
        print("هنوز فردی ثبت نشده است.")
        return

    for person in data["people"]:
        print(
            f"#{person['id']} | {person['name']}"
        )


def day_report(data):
    day = input(
        "تاریخ گزارش YYYY-MM-DD "
        "(خالی = امروز): "
    ).strip()

    if not day:
        day = date.today().isoformat()

    rows = [
        record
        for record in data["records"]
        if record.get("date") == day
    ]

    if not rows:
        print(
            "برای این تاریخ رکوردی وجود ندارد."
        )
        return

    print(f"\n=== گزارش {day} ===")

    for record in rows:
        person = find_person(
            data,
            record["person_id"],
        )
        name = (
            person["name"]
            if person
            else "نامشخص"
        )
        print(
            f"{name} | {record['status']}"
        )


def person_report(data):
    person = get_person_from_input(data)

    if person is None:
        return

    rows = [
        record
        for record in data["records"]
        if record.get("person_id")
        == person["id"]
    ]

    if not rows:
        print(
            "برای این فرد رکوردی وجود ندارد."
        )
        return

    present = sum(
        1
        for record in rows
        if record.get("status") == "حاضر"
    )
    absent = sum(
        1
        for record in rows
        if record.get("status") == "غایب"
    )
    total = len(rows)
    percentage = (
        present / total
    ) * 100

    print(
        f"\n=== گزارش {person['name']} ==="
    )
    print(
        f"کل روزهای ثبت‌شده: {total}"
    )
    print(f"حاضر: {present}")
    print(f"غایب: {absent}")
    print(
        f"درصد حضور: {percentage:.1f}%"
    )


def absence_report(data):
    counts = {}

    for record in data["records"]:
        if record.get("status") == "غایب":
            person_id = record.get("person_id")
            counts[person_id] = (
                counts.get(person_id, 0)
                + 1
            )

    rows = []

    for person_id, count in counts.items():
        person = find_person(data, person_id)
        name = (
            person["name"]
            if person
            else "نامشخص"
        )
        rows.append((name, count))

    rows.sort(
        key=lambda item: item[1],
        reverse=True,
    )

    if not rows:
        print("هنوز غیبتی ثبت نشده است.")
        return

    print("\n=== بیشترین غیبت ===")

    for name, count in rows:
        print(f"{name}: {count}")


def delete_person(data):
    person = get_person_from_input(data)

    if person is None:
        return

    data["people"].remove(person)
    data["records"] = [
        record
        for record in data["records"]
        if record.get("person_id")
        != person["id"]
    ]

    save_data(data)
    print(
        "فرد و رکوردهای مربوط به او حذف شدند."
    )


def show_menu():
    print("\n=== سیستم حضور و غیاب ===")
    print("1) ثبت فرد")
    print("2) نمایش افراد")
    print("3) ثبت حضور")
    print("4) ثبت غیبت")
    print("5) گزارش روزانه")
    print("6) گزارش فردی")
    print("7) گزارش بیشترین غیبت")
    print("8) حذف فرد")
    print("0) خروج")


def main():
    data = load_data()

    while True:
        show_menu()
        choice = input(
            "انتخاب شما: "
        ).strip()

        if choice == "1":
            add_person(data)
        elif choice == "2":
            list_people(data)
        elif choice == "3":
            mark_attendance(data, "حاضر")
        elif choice == "4":
            mark_attendance(data, "غایب")
        elif choice == "5":
            day_report(data)
        elif choice == "6":
            person_report(data)
        elif choice == "7":
            absence_report(data)
        elif choice == "8":
            delete_person(data)
        elif choice == "0":
            print("برنامه پایان یافت.")
            break
        else:
            print("گزینه نامعتبر است.")


if __name__ == "__main__":
    main()

نمونه اجرای برنامه

=== سیستم حضور و غیاب ===
1) ثبت فرد
2) نمایش افراد
3) ثبت حضور
4) ثبت غیبت
5) گزارش روزانه
6) گزارش فردی
7) گزارش بیشترین غیبت
8) حذف فرد
0) خروج

انتخاب شما: 1
نام: علی رضایی
فرد با شناسه 1 ثبت شد.

نمونه گزارش فردی

=== گزارش علی رضایی ===
کل روزهای ثبت‌شده: 20
حاضر: 18
غایب: 2
درصد حضور: 90.0%

چرا نباید رکورد حضور و غیاب را فقط با نام ذخیره کنیم؟

نام افراد یکتا نیست و ممکن است دو نفر نام مشابه داشته باشند. علاوه بر این، اگر نام فرد بعداً اصلاح شود، رکوردهای قدیمی با نام قبلی باقی می‌مانند. استفاده از شناسه عددی باعث می‌شود ارتباط بین فرد و رکوردهای حضور پایدار بماند.

چرا جلوگیری از رکورد تکراری مهم است؟

اگر برای یک فرد در یک تاریخ چند رکورد ثبت شود، محاسبه درصد حضور دیگر قابل اعتماد نخواهد بود. بنابراین قبل از ذخیره هر رکورد، ترکیب person_id + date را بررسی می‌کنیم. در نسخه دیتابیسی همین مفهوم معمولاً با Unique Constraint پیاده‌سازی می‌شود.

اشتباه‌های رایج در پروژه حضور و غیاب

ثبت تاریخ بدون اعتبارسنجی

اگر کاربر بتواند هر متن دلخواهی به‌عنوان تاریخ ذخیره کند، گزارش‌گیری بر اساس روز و ماه دشوار می‌شود. فرمت ISO یعنی YYYY-MM-DD انتخاب مناسبی است.

ذخیره نام داخل تمام رکوردها

این کار داده را تکراری می‌کند. بهتر است رکورد فقط شناسه فرد را نگه دارد.

ثبت چند وضعیت در یک روز

این اشتباه باعث می‌شود فرد در یک تاریخ هم حاضر و هم غایب محسوب شود. تابع attendance_exists() جلوی این مشکل را می‌گیرد.

حذف فرد بدون حذف رکوردهای مربوط به او

در این حالت رکوردهایی باقی می‌مانند که دیگر صاحب مشخصی ندارند. در پروژه فعلی هنگام حذف فرد، رکوردهای او هم حذف می‌شوند.

محاسبه درصد حضور با تعداد کل افراد

درصد حضور یک فرد باید بر اساس تعداد روزهای ثبت‌شده همان فرد محاسبه شود، نه تعداد افراد سیستم.

چطور پروژه را توسعه دهیم؟

  1. وضعیت تأخیر: علاوه بر حاضر و غایب، وضعیت «تأخیر» اضافه کنید.
  2. ساعت ورود: زمان ورود هر فرد را ثبت کنید.
  3. گزارش ماهانه: رکوردهای یک ماه را بر اساس پیشوند YYYY-MM فیلتر کنید.
  4. خروجی CSV: گزارش حضور را برای Excel یا Google Sheets صادر کنید.
  5. گروه یا کلاس: برای هر فرد کلاس، واحد سازمانی یا دپارتمان ثبت کنید.
  6. حد مجاز غیبت: در صورت عبور از مقدار مشخص هشدار نمایش دهید.
  7. ویرایش رکورد: امکان اصلاح وضعیت اشتباه ثبت‌شده را اضافه کنید.
  8. SQLite: برای تعداد رکورد زیاد، JSON را با دیتابیس جایگزین کنید.
  9. رابط گرافیکی: با Tkinter جدول افراد و دکمه‌های حاضر/غایب بسازید.
  10. OOP: کلاس‌های Person، AttendanceRecord و AttendanceManager طراحی کنید.

تمرین پیشنهادی: گزارش ماهانه

از کاربر یک مقدار مثل 2026-08 بگیرید و رکوردهایی را که تاریخ آن‌ها با همین مقدار شروع می‌شود جدا کنید. سپس برای هر فرد تعداد حضور، غیبت و درصد حضور همان ماه را نمایش دهید. این تمرین شما را با فیلتر چندمرحله‌ای و گزارش‌گیری واقعی‌تر آشنا می‌کند.

این پروژه چه مفاهیمی را تمرین می‌دهد؟

  • List و Dictionary
  • ساختار داده چندبخشی
  • JSON و File I/O
  • Pathlib
  • date و تاریخ ISO
  • توابع و پارامترها
  • حلقه for و while
  • شرط‌ها
  • Exception Handling
  • List Comprehension
  • Generator Expression
  • any و sum
  • sorted و Lambda
  • جست‌وجو و فیلتر داده
  • اعتبارسنجی ورودی
  • جلوگیری از داده تکراری
  • گزارش‌گیری و محاسبه درصد
  • مدیریت ارتباط بین رکوردها

جمع‌بندی

در این آموزش یک برنامه کامل حضور و غیاب با پایتون ساختیم. سیستم می‌تواند افراد را با ID یکتا ثبت کند، وضعیت حاضر یا غایب را برای تاریخ مشخص ذخیره کند، از ثبت رکورد تکراری جلوگیری کند، گزارش روزانه و فردی بسازد، درصد حضور را محاسبه کند و افراد دارای بیشترین غیبت را نمایش دهد.

کد کامل پروژه قبل از انتشار از نظر Syntax بررسی شد. مرحله منطقی بعدی برای توسعه این پروژه، اضافه کردن گزارش ماهانه، ساعت ورود و سپس انتقال داده‌ها از JSON به SQLite است.

📌 همچنین بخوانید: تمرین پایتون با جواب، ساخت بازی با پایتون و Pygame و ساخت ربات با پایتون

می‌خواهی پیشرفتت ذخیره شود؟با ورود یا ثبت‌نام رایگان، درس‌های تمام‌شده را علامت بزن و درصد پیشرفتت را در حساب کاربری ببین.ورود / ثبت‌نام برای ذخیره پیشرفت
درس بعدی · پروژه‌های پایتون (17 از 19)ساخت برنامه مدیریت رمزهای عبور با پایتون؛ پروژه امن‌تر با Fernet و Master Password ←
آموزش‌های تازه، کد آماده و رفع خطاهر هفته در کانال Dev Source؛ رایگان.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *