آموزش 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. <b>Escape me</b></li></ul>دقت کنید که کار سوم با تگ HTML وارد شده، اما Jinja2 آن را به <b> تبدیل کرده و بهصورت متن نمایش داده است. دادهها در این مثال در حافظه نگه داشته میشوند و با بستن برنامه پاک میشوند؛ در قدم بعد میتوانید آنها را در فایل 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 را همزمان با ساخت پروژه یاد بگیرید.
آموزشهای مرتبط
- دوره پایتون سطح متوسط
- دکوراتور در پایتون
- pip و محیط مجازی venv
- کار با فایل JSON در پایتون
- وب اسکرپینگ با پایتون
- آموزش جامع پایتون از صفر تا پیشرفته