کتابخانه‌های پایتون

آموزش Flask؛ ساخت اولین وب‌سایت با پایتون قدم‌به‌قدم

آموزش Flask و ساخت اولین وب‌سایت با پایتون

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

Flask (فلسک) یک فریم‌ورک سبک و محبوب پایتون برای ساخت وب‌سایت و API است. اگر پایتون را یاد گرفته‌اید و می‌خواهید اولین وب‌سایت خودتان را بسازید، Flask بهترین نقطه شروع است: با چند خط کد یک صفحه وب بالا می‌آید، ساختار پروژه را خودتان انتخاب می‌کنید و چیزی پنهان از شما اتفاق نمی‌افتد. در این آموزش قدم‌به‌قدم Flask را نصب می‌کنیم، مسیرها (Routes) و قالب‌های HTML را می‌سازیم، فرم دریافت می‌کنیم، یک API ساده با JSON می‌نویسیم و در پایان یک برنامه کوچک لیست کارها می‌سازیم. همه کدها با Flask نسخه ۳.۱ روی پایتون ۳.۱۳ تست شده‌اند و خروجی‌ها واقعی هستند.

Flask چیست و چه زمانی سراغش برویم؟

Flask یک «میکروفریم‌ورک» است؛ یعنی هسته آن فقط کارهای اصلی مثل مسیریابی آدرس‌ها، دریافت درخواست و ساخت پاسخ را انجام می‌دهد و بقیه امکانات مثل دیتابیس، ورود کاربران یا پنل مدیریت را با افزونه‌ها اضافه می‌کنید. همین سادگی باعث شده Flask هم برای یادگیری وب با پایتون عالی باشد و هم در پروژه‌های واقعی مثل APIهای کوچک، داشبوردهای داخلی و سرویس‌های یادگیری ماشین استفاده شود.

  • Flask مناسب است برای پروژه‌های کوچک و متوسط، API و سرویس‌های سبک، نمونه‌سازی سریع و یادگیری مفاهیم وب.
  • Django مناسب‌تر است وقتی پروژه بزرگ با پنل مدیریت، سیستم کاربران و دیتابیس آماده می‌خواهید و ترجیح می‌دهید ساختار از قبل تعیین شده باشد.
  • FastAPI گزینه دیگری برای ساخت API سریع با تایپ‌هینت است؛ مفاهیم Flask را که یاد بگیرید، یادگیری آن هم آسان می‌شود.

نصب Flask در محیط مجازی

بهتر است هر پروژه وب محیط مجازی (virtual environment) خودش را داشته باشد تا نسخه کتابخانه‌ها با پروژه‌های دیگر قاطی نشود. اگر با محیط مجازی آشنا نیستید، آموزش pip و محیط مجازی venv را ببینید. دستورهای زیر یک پوشه پروژه می‌سازند، محیط مجازی را فعال می‌کنند و Flask را نصب می‌کنند:

mkdir my_site
cd my_site
python -m venv venv

# Windows
venv\Scripts\activate
# macOS / Linux
source venv/bin/activate

pip install flask
python -m flask --version

اگر هنگام نصب با خطای شبکه یا دستور ناشناخته pip روبه‌رو شدید، راه‌حل‌ها را در آموزش نصب کتابخانه در پایتون با pip و رفع خطای pip is not recognized بخوانید. برای نوشتن کد هم VS Code انتخاب خوبی است.

اولین برنامه Flask: سلام دنیا

یک فایل به نام app.py بسازید. هر برنامه Flask با ساختن یک شیء از کلاس Flask شروع می‌شود. سپس با دکوراتور @app.route مشخص می‌کنیم کدام تابع به کدام آدرس پاسخ بدهد. مقداری که تابع برمی‌گرداند، همان چیزی است که در مرورگر نمایش داده می‌شود. در این آموزش برای نمایش خروجی، به‌جای باز کردن مرورگر از test_client خود Flask استفاده می‌کنیم که درخواست واقعی HTTP را شبیه‌سازی می‌کند:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "<h1>Hello, Flask!</h1>"

client = app.test_client()
response = client.get("/")
print(response.status_code, response.content_type)
print(response.get_data(as_text=True))
print(client.get("/missing").status_code)

خروجی:

200 text/html; charset=utf-8
<h1>Hello, Flask!</h1>
404

آدرس ریشه کد ۲۰۰ و محتوای HTML برگردانده و آدرسی که تعریف نشده، خطای ۴۰۴ داده است. برای دیدن سایت در مرورگر، به‌جای test_client سرور توسعه را اجرا کنید و آدرس http://127.0.0.1:5000 را باز کنید. گزینه --debug باعث می‌شود با هر بار ذخیره فایل، سرور خودکار دوباره بارگذاری شود و خطاها با جزئیات نمایش داده شوند:

flask --app app run --debug

 * Serving Flask app 'app'
 * Debug mode: on
 * Running on http://127.0.0.1:5000

مسیرها و متغیر در آدرس (Dynamic Routes)

بیشتر سایت‌ها صفحه‌هایی دارند که بخشی از آدرس آن‌ها متغیر است؛ مثل صفحه پروفایل هر کاربر یا صفحه هر مقاله. در Flask بخش متغیر را داخل علامت‌های <> می‌نویسید و Flask آن را به‌عنوان آرگومان به تابع می‌دهد. با نوشتن نوع مثل int: هم می‌توانید فقط عدد قبول کنید. پارامترهای بعد از علامت سؤال در آدرس (Query String) هم از request.args خوانده می‌شوند:

from flask import Flask, request

app = Flask(__name__)
posts = {1: "Python basics", 2: "Flask routes"}

@app.route("/user/<name>")
def profile(name):
    return f"Profile of {name}"

@app.route("/post/<int:post_id>")
def show_post(post_id):
    title = posts.get(post_id)
    if title is None:
        return "Post not found", 404
    return f"Post {post_id}: {title}"

@app.route("/search")
def search():
    query = request.args.get("q", "")
    return f"Searching for: {query}"

client = app.test_client()
for url in ["/user/sara", "/post/2", "/post/9", "/post/abc", "/search?q=flask"]:
    r = client.get(url)
    print(url, "->", r.status_code, r.get_data(as_text=True)[:30])

خروجی:

/user/sara -> 200 Profile of sara
/post/2 -> 200 Post 2: Flask routes
/post/9 -> 404 Post not found
/post/abc -> 404 <!doctype html>
<html lang=en>
/search?q=flask -> 200 Searching for: flask

در خروجی ببینید که /post/abc اصلاً به تابع نرسیده و ۴۰۴ گرفته، چون عدد نیست. برای /post/9 هم خودمان با برگرداندن یک تاپل، کد وضعیت ۴۰۴ را تعیین کرده‌ایم. این الگو، یعنی برگرداندن (body, status)، در Flask زیاد استفاده می‌شود.

آدرس /post/abc هم ۴۰۴ گرفته، اما پاسخ آن صفحه HTML پیش‌فرض Flask است که با <!doctype html> شروع می‌شود. در بخش بعد همین صفحه را با یک صفحه خطای اختصاصی عوض می‌کنیم.

صفحه خطای اختصاصی با errorhandler

وقتی کاربر آدرس اشتباهی وارد می‌کند، بهتر است به‌جای صفحه ساده و انگلیسی پیش‌فرض، یک صفحه خطای هماهنگ با طراحی سایت ببیند که او را به صفحه اصلی یا جستجو راهنمایی کند. Flask برای این کار دکوراتور @app.errorhandler را دارد. تابعی که با آن ثبت می‌کنید، برای هر خطا با آن کد وضعیت اجرا می‌شود. همچنین تابع abort به شما اجازه می‌دهد هر جای کد، اجرای درخواست را با یک کد خطا متوقف کنید:

from flask import Flask, abort

app = Flask(__name__)
users = {"sara": "admin", "ali": "member"}

@app.errorhandler(404)
def not_found(error):
    return {"error": "Page not found", "code": 404}, 404

@app.route("/role/<name>")
def role(name):
    if name not in users:
        abort(404)
    return {"user": name, "role": users[name]}

client = app.test_client()
print(client.get("/role/sara").get_json())
print(client.get("/role/reza").get_json())
print(client.get("/no/such/page").get_json())

خروجی:

{'role': 'admin', 'user': 'sara'}
{'code': 404, 'error': 'Page not found'}
{'code': 404, 'error': 'Page not found'}

حالا هم آدرس ناموجود و هم کاربری که در دیکشنری نیست، پاسخ یکسان و قابل‌پیش‌بینی می‌گیرند. در سایت معمولی به‌جای دیکشنری، یک قالب HTML مثل 404.html را با render_template برگردانید. همین کار را برای خطای ۵۰۰ هم انجام دهید تا اگر در کد مشکلی پیش آمد، جزئیات فنی به کاربر نشان داده نشود. نوشتن صفحه خطای خوب هم تجربه کاربر را بهتر می‌کند و هم از نظر سئو اهمیت دارد، چون موتورهای جستجو باید برای صفحه‌های ناموجود واقعاً کد ۴۰۴ دریافت کنند، نه ۲۰۰.

قالب‌های HTML با Jinja2

نوشتن HTML داخل رشته‌های پایتون خیلی زود شلوغ می‌شود. Flask برای این کار از موتور قالب Jinja2 استفاده می‌کند: فایل‌های HTML را در پوشه‌ای به نام templates قرار می‌دهید و با render_template مقادیر پایتون را به آن‌ها می‌فرستید. ساختار پروژه این‌طور می‌شود:

my_site/
├── app.py
├── static/
│   └── style.css
└── templates/
    ├── base.html
    └── index.html

فایل base.html قالب اصلی سایت است که هدر و منو را یک بار تعریف می‌کند و بقیه صفحه‌ها با extends از آن ارث می‌برند. داخل قالب، {{ }} برای نمایش مقدار و {% %} برای دستورهایی مثل حلقه و شرط است:

<!-- templates/base.html -->
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>{{ title }}</title>
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
  <nav><a href="/">Home</a> | <a href="/about">About</a></nav>
  {% block content %}{% endblock %}
</body>
</html>

<!-- templates/index.html -->
{% extends "base.html" %}
{% block content %}
  <h1>{{ title }}</h1>
  <ul>
  {% for course in courses %}
    <li>{{ course }}</li>
  {% endfor %}
  </ul>
{% endblock %}
from flask import Flask, render_template

app = Flask(__name__)

@app.route("/")
def home():
    courses = ["Python basics", "Intermediate Python", "Flask"]
    return render_template("index.html", title="Dev Source", courses=courses)

html = app.test_client().get("/").get_data(as_text=True)
for line in html.splitlines():
    if line.strip():
        print(line.rstrip())

خروجی:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Dev Source</title>
  <link rel="stylesheet" href="/static/style.css">
</head>
<body>
  <nav><a href="/">Home</a> | <a href="/about">About</a></nav>
  <h1>Dev Source</h1>
  <ul>
    <li>Python basics</li>
    <li>Intermediate Python</li>
    <li>Flask</li>
  </ul>
</body>
</html>

Jinja2 به‌طور خودکار متن‌ها را Escape می‌کند؛ یعنی اگر کاربر در فرم کد HTML یا جاوااسکریپت بنویسد، به‌صورت متن ساده نمایش داده می‌شود و اجرا نمی‌شود. این یکی از مهم‌ترین دلایل استفاده از قالب به‌جای چسباندن رشته‌هاست. تابع url_for هم آدرس فایل‌های پوشه static مثل CSS، تصاویر و جاوااسکریپت را درست می‌سازد.

دریافت اطلاعات از فرم با متد POST

به‌طور پیش‌فرض هر مسیر فقط درخواست GET را قبول می‌کند. برای فرم‌ها باید متد POST را هم با پارامتر methods اجازه بدهید و داده‌ها را از request.form بخوانید. همیشه ورودی کاربر را بررسی کنید و در صورت خطا کد وضعیت مناسب برگردانید:

from flask import Flask, request

app = Flask(__name__)

@app.route("/contact", methods=["GET", "POST"])
def contact():
    if request.method == "POST":
        name = request.form.get("name", "").strip()
        if not name:
            return "Name is required", 400
        return f"Thanks, {name}! Your message was received."
    return '<form method="post"><input name="name"><button>Send</button></form>'

client = app.test_client()
print(client.get("/contact").get_data(as_text=True))
r = client.post("/contact", data={"name": "Ali"})
print(r.status_code, r.get_data(as_text=True))
r = client.post("/contact", data={"name": "  "})
print(r.status_code, r.get_data(as_text=True))

خروجی:

<form method="post"><input name="name"><button>Send</button></form>
200 Thanks, Ali! Your message was received.
400 Name is required

در پروژه واقعی برای فرم‌ها از افزونه Flask-WTF استفاده کنید که اعتبارسنجی و محافظت در برابر حمله CSRF را اضافه می‌کند.

ساخت یک API ساده با JSON

Flask برای ساخت API هم بسیار محبوب است. اگر تابع یک دیکشنری یا لیست برگرداند، Flask آن را خودکار به JSON تبدیل می‌کند. داده JSON ارسالی از سمت کاربر هم با request.get_json() خوانده می‌شود. در مثال زیر یک API کوچک برای مدیریت کتاب‌ها داریم که فهرست را برمی‌گرداند و کتاب جدید اضافه می‌کند:

from flask import Flask, request

app = Flask(__name__)
books = [{"id": 1, "title": "Learn Python"}]

@app.get("/api/books")
def list_books():
    return {"count": len(books), "books": books}

@app.post("/api/books")
def add_book():
    data = request.get_json(silent=True) or {}
    title = data.get("title")
    if not title:
        return {"error": "title is required"}, 400
    book = {"id": len(books) + 1, "title": title}
    books.append(book)
    return book, 201

client = app.test_client()
print(client.post("/api/books", json={"title": "Flask Web Basics"}).get_json())
print(client.post("/api/books", json={}).status_code)
print(client.get("/api/books").get_json())

خروجی:

{'id': 2, 'title': 'Flask Web Basics'}
400
{'books': [{'id': 1, 'title': 'Learn Python'}, {'id': 2, 'title': 'Flask Web Basics'}], 'count': 2}

دکوراتورهای @app.get و @app.post شکل کوتاه @app.route(..., methods=[...]) هستند. کد ۲۰۱ یعنی منبع جدید ساخته شد و ۴۰۰ یعنی درخواست ناقص است. برای کار با داده‌های JSON در فایل، آموزش کار با فایل JSON در پایتون را ببینید.

پروژه کوچک: لیست کارها با Flask

حالا همه مفاهیم را کنار هم می‌گذاریم. برنامه زیر یک لیست کارها (To-Do) است که کار جدید را با فرم می‌گیرد، بعد از ذخیره با redirect کاربر را به صفحه اصلی برمی‌گرداند و امکان حذف کار را هم دارد. الگوی «POST سپس Redirect» جلوی ارسال دوباره فرم با رفرش صفحه را می‌گیرد. نسخه کنسولی همین ایده را در پروژه لیست کارها با پایتون ساخته‌ایم:

from flask import Flask, request, redirect, url_for, render_template_string

app = Flask(__name__)
tasks = []

PAGE = """<h1>To-Do ({{ tasks|length }})</h1>
<ul>{% for t in tasks %}<li>{{ loop.index }}. {{ t }}</li>{% endfor %}</ul>"""

@app.route("/")
def index():
    return render_template_string(PAGE, tasks=tasks)

@app.post("/add")
def add():
    task = request.form.get("task", "").strip()
    if task:
        tasks.append(task)
    return redirect(url_for("index"))

@app.post("/delete/<int:number>")
def delete(number):
    if 1 <= number <= len(tasks):
        tasks.pop(number - 1)
    return redirect(url_for("index"))

client = app.test_client()
r = client.post("/add", data={"task": "Learn Flask routes"})
print(r.status_code, r.headers["Location"])
client.post("/add", data={"task": "Build a template"})
client.post("/add", data={"task": "<b>Escape me</b>"})
client.post("/delete/1")
print(client.get("/").get_data(as_text=True))

خروجی:

302 /
<h1>To-Do (2)</h1>
<ul><li>1. Build a template</li><li>2. &lt;b&gt;Escape me&lt;/b&gt;</li></ul>

دقت کنید که کار سوم با تگ HTML وارد شده، اما Jinja2 آن را به &lt;b&gt; تبدیل کرده و به‌صورت متن نمایش داده است. داده‌ها در این مثال در حافظه نگه داشته می‌شوند و با بستن برنامه پاک می‌شوند؛ در قدم بعد می‌توانید آن‌ها را در فایل JSON یا دیتابیس SQLite ذخیره کنید.

وقتی پروژه بزرگ می‌شود: Blueprint و تنظیمات

تا اینجا همه کدها در یک فایل app.py بودند که برای شروع کاملاً مناسب است. اما وقتی تعداد صفحه‌ها زیاد می‌شود، بهتر است مسیرهای مرتبط را در فایل‌های جدا گروه‌بندی کنید. Flask برای این کار Blueprint دارد: مثلاً مسیرهای وبلاگ در یک فایل و مسیرهای API در فایل دیگر قرار می‌گیرند و در فایل اصلی ثبت می‌شوند. تنظیمات مثل کلید مخفی و آدرس دیتابیس هم به‌جای نوشتن مستقیم در کد، از متغیرهای محیطی یا فایل تنظیمات خوانده می‌شوند. ساختار رایج یک پروژه متوسط این‌طور است:

my_site/
├── app.py            # create_app() and register blueprints
├── config.py         # SECRET_KEY, DATABASE_URL from environment
├── blog/
│   ├── __init__.py
│   └── routes.py     # bp = Blueprint("blog", __name__)
├── api/
│   └── routes.py
├── templates/
└── static/

خطاهای رایج در Flask و راه رفع

  • ModuleNotFoundError: No module named flask: محیط مجازی فعال نیست یا Flask در پایتون دیگری نصب شده است. راه‌حل را در رفع خطای ModuleNotFoundError ببینید.
  • TemplateNotFound: پوشه قالب‌ها باید دقیقاً templates نام داشته باشد و کنار app.py باشد.
  • Address already in use: پورت ۵۰۰۰ در حال استفاده است؛ با --port 8000 پورت دیگری انتخاب کنید.
  • 405 Method Not Allowed: فرم با POST ارسال شده اما در methods مسیر اجازه داده نشده است.
  • تغییرات دیده نمی‌شود: سرور را با --debug اجرا کنید یا آن را دوباره راه‌اندازی کنید.

انتشار سایت Flask روی سرور

سرور توسعه Flask فقط برای کار روی کامپیوتر خودتان است و نباید در سایت واقعی از آن یا از حالت debug استفاده کنید. در محیط واقعی برنامه را با یک سرور WSGI مثل Gunicorn اجرا می‌کنند و Nginx را جلوی آن قرار می‌دهند. بسیاری از هاست‌های cPanel هم بخش Setup Python App دارند که برنامه Flask را بدون مدیریت سرور اجرا می‌کند. قبل از انتشار، کلید مخفی (SECRET_KEY) را در متغیر محیطی نگه دارید، فهرست کتابخانه‌ها را با pip freeze > requirements.txt ذخیره کنید و حالت debug را خاموش کنید.

تمرین

برنامه لیست کارها را کامل کنید: برای هر کار یک دکمه «انجام شد» بگذارید که وضعیت آن را تغییر دهد، کارها را در یک فایل JSON ذخیره کنید تا با بستن برنامه پاک نشوند و یک مسیر /api/tasks بسازید که همان فهرست را به‌صورت JSON برگرداند. اگر می‌خواهید داده واقعی از سایت‌های دیگر در برنامه‌تان نمایش دهید، وب اسکرپینگ با پایتون قدم بعدی خوبی است.

سوالات متداول

Flask چیست و چه کاربردی دارد؟

Flask یک فریم‌ورک سبک پایتون برای ساخت وب‌سایت و API است. مسیریابی، دریافت درخواست و ساخت پاسخ را انجام می‌دهد و امکانات دیگر را با افزونه‌ها اضافه می‌کنید.

Flask بهتر است یا Django؟

برای یادگیری و پروژه‌های کوچک و API، Flask ساده‌تر و منعطف‌تر است. برای پروژه‌های بزرگ با پنل مدیریت و سیستم کاربران آماده، Django امکانات بیشتری از ابتدا دارد.

چطور Flask را نصب کنم؟

یک محیط مجازی با python -m venv venv بسازید، آن را فعال کنید و دستور pip install flask را اجرا کنید. با python -m flask –version نصب را بررسی کنید.

چطور برنامه Flask را اجرا کنم؟

در پوشه پروژه دستور flask –app app run –debug را اجرا کنید و آدرس http://127.0.0.1:5000 را در مرورگر باز کنید.

پوشه templates در Flask چیست؟

پوشه‌ای کنار app.py است که فایل‌های HTML قالب Jinja2 در آن قرار می‌گیرند و با render_template نمایش داده می‌شوند. فایل‌های CSS و تصاویر هم در پوشه static قرار می‌گیرند.

آیا برای یادگیری Flask باید HTML بلد باشم؟

برای شروع، آشنایی مقدماتی با HTML کافی است. تمرکز اصلی Flask روی پایتون است و می‌توانید HTML و CSS را هم‌زمان با ساخت پروژه یاد بگیرید.

آموزش‌های مرتبط

آموزش‌های تازه، کد آماده و رفع خطاهر هفته در کانال Dev Source؛ رایگان.

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

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