Python حرفه‌ای: ساخت وب‌سایت شخصی - بخش دوم

Professional Python: Build a Personal Website - Part 2

27 اسفند 1399
درسنامه درس 36 از سری پایتون حرفه‌ای
Python حرفه ای: ساخت وب سایت شخصی - بخش دوم (قسمت 36)

پردازش فرم در flask

در جلسه ی قبل توضیح دادم که اگر به آدرس http://127.0.0.1:5000/contact در مرورگر برویم یک فرم تماس با ما (contact me) را خواهیم داشت تا مردم بتوانند سوالات یا پیشنهادات خود را برای ما ارسال کنند. از طرفی نمونه کد زیر را در documentation رسمی flask پیدا کردیم که نحوه ی دریافت فرم را به ما نشان می داد:

@app.route('/login', methods=['POST', 'GET'])

def login():

    error = None

    if request.method == 'POST':

        if valid_login(request.form['username'],

                       request.form['password']):

            return log_the_user_in(request.form['username'])

        else:

            error = 'Invalid username/password'

    # the code below is executed if the request method

    # was GET or the credentials were invalid

    return render_template('login.html', error=error)

در flask شیء ای خاص به نام request داریم که می توانیم با استفاده از خصوصیت form آن، به تمام فیلد های فرم دسترسی داشته باشیم اما همانطور که در کد بالا می بینید ایندکس خاصی از form مد نظر ما می باشد. در این کد ایندکس های username و password را مشاهده می کنیم اما آیا این ایندکس ها به صورت پیش فرض روی شیء request موجود هستند؟ خیر! اگر با فرم های HTML کار کرده باشید می دانید که attribute ای به نام name برای هر فیلد وجود دارد که مشخص می کند آن فیلد در سمت سرور با چه نامی دریافت شود. با این حساب فایل contact.html را باز کرده و مانند من به فیلد های فرم،‌ خصوصیت name را اضافه کنید. در ضمن مقداری که به name می دهید کاملا دلخواه است اما پیشنهاد می کنم اسم عجیب و غریبی انتخاب نکنید تا در سمت سرور و هنگام دریافتشان مشکلی نداشته باشیم:

<div class="col-md-7">

  <div class="form-group">

    <input type="email" name="email" class="form-control" id="email" placeholder="Email">

  </div>

  <div class="form-group">

    <input type="text" name="subject" class="form-control" id="subject" placeholder="Subject">

  </div>

  <div class="form-group">

    <textarea class="form-control" name="message" rows="5"

      placeholder="Enter your message"></textarea>

  </div>

  <button type="submit" class="btn btn-default btn-lg">Send</button>

</div>

کل فرم ما ۳ فیلد دارد و همانطور که از کد بالا مشخص است، من نام های email و subject و message (به ترتیب «ایمیل»، «موضوع» و «پیام») را به name داده ام. حالا می توانیم به server.py رفته و به جای برگرداندن یک رشته، با استفاده از شیء request به محتویات فرم دسترسی پیدا کنیم:

from flask import Flask, render_template, redirect, request

app = Flask(__name__)







@app.route('/')

def home():

    return render_template('index.html')







@app.route('/index')

def home_redirect():

    return redirect('/', code=302)







@app.route('/works')

def works():

    return render_template('works.html')







@app.route('/work')

def work():

    return render_template('work.html')







@app.route('/about')

def about():

    return render_template('about.html')







@app.route('/contact')

def contact():

    return render_template('contact.html')







@app.route('/components')

def components():

    return render_template('components.html')







@app.route('/submit_form', methods=['POST', 'GET'])

def submit_form():

    if request.method == 'POST':

        data = request.form.to_dict()

        print(data)

        return data

    else:

        return 'request was not POST'

همانطور که می بینید من در ابتدا شیء request را در خط اول import کرده ام. در مرحله ی بعدی یک شرط ساده نوشته ام که اگر نوع درخواست POST بود داده ها را دریافت کنیم. چطور؟ در پایتون متدی کاربردی به نام to_dict وجود دارد که می تواند داده های درون form را برای ما تبدیل به یک dictionary کند. اگر این کار را نکنیم مجبور هستیم مانند مثالی که در documentation رسمی flask آمده است، تک تک فیلد ها را به صورت دستی دریافت کنیم. در مرحله ی بعد این داده ها را پرینت کرده ایم؛ یک بار با print در ترمینال و یک بار با return کردن آن ها در مرورگر. توجه کنید که return کردن برای ارسال داده به سمت مرورگر اجباری است. در نهایت اگر درخواست ارسال شده از نوع POST نباشد یک رشته ی ساده را برگردانده ایم که می گوید درخواست POST نبوده است.

تنها کاری که باقی مانده است، تست کردن فرم است. برای این کار به آدرس http://127.0.0.1:5000/contact می رویم و فرم را با هر داده ی دلخواهی پُر می کنیم. با کلیک روی دکمه ی send فرم شما ثبت می شود و در مرورگر خود داده ای به شکل زیر را دریافت می کنید:

{

email: "info@roxo.ir",

message: "This is a test message",

subject: "Notice"

}

همانطور که می بینید داده های ما برایمان نمایش داده شده اند. طبیعتا اگر شما مقدار متفاوتی را برای فرم نوشته باشید، مقدار متفاوتی را نیز دریافت می کنید. همچنین اگر به ترمینال خود (یا CMD و Powershell برای کاربران ویندوز) نگاهی بیندازید این داده ها را به صورت یک dictionary خواهید دید:

{'email': 'info@roxo.ir', 'subject': 'Notice', 'message': 'This is a test message'}

من این کار را انجام دادم تا به شما ثابت کنم داده ها به سرور ما منتقل شده اند اما در وب سایت های واقعی، پیام کاربر را به خودش نشان نمی دهیم بلکه یک متن ساده به او نشان می دهیم که می گوید پیام شما دریافت شد و بعدا به آن پاسخ خواهیم داد. برای انجام این کار وارد پوشه ی templates شده و یک فایل HTML جدید به نام thankyou.html را ایجاد کنید. حالا کد های contact.html را کپی کرده و درون فایل thankyou.html قرار بدهید. تنها کاری که باقی می ماند این است که تگ <form> و محتویات درون آن را از این فایل حذف کرده و به جایش یک متن ساده قرار بدهید:

<div class="section-container">

  <div class="container">

    <div class="row">

      <div class="col-xs-12">

        <div class="section-container-spacer text-center">

          <h1 class="h2">03 : Contact me</h1>

        </div>




        <div class="row">

          <div class="col-md-10 col-md-offset-1">

            <p>Thank you for your message. I will respond in a few days.</p>

          </div>

        </div>




      </div>

    </div>

  </div>

</div>

همانطور که می بینید دیگر اثری از <form> و محتویاتش نیست بلکه یک تگ <p> را داریم که از کاربر برای پیامش تشکر می کند و به او می گوید که در چند روز آینده به او پاسخ خواهیم داد. البته ما می توانیم کد هایمان را پیشرفته تر نیز بکنیم. به طور مثال اگر فرم ما نام کاربر را نیز می گرفت می توانستیم به شکل زیر عمل کنیم:

<div class="row">

  <div class="col-md-10 col-md-offset-1">

    <p>Thank you {{ name }} for your message. I will respond in a few days.</p>

  </div>

</div>

اما من نمی خواهم یک فیلد دیگر را نیز به فرم اضافه کنم بنابراین از همان کد قبلی استفاده می کنم. همانطور که حدس می زنید باید این فایل HTML را به یک route متصل کنیم بنابراین نیاز به تعریف یک route جدید داریم اما مشکل اینجاست که تعداد route های ما بسیار زیاد شده اند. چطور می توانیم این مشکل را به صورت پویا حل کنیم؟ من برای حل این مشکل تمام کد های server.py را با کد زیر جایگزین می کنم:

from flask import Flask, render_template, redirect, request

app = Flask(__name__)







@app.route('/')

def home():

    return render_template('index.html')







@app.route('/<string:page_name>')

def page_name(page_name):

    return render_template(page_name + '.html')







@app.route('/submit_form', methods=['POST', 'GET'])

def submit_form():

    if request.method == 'POST':

        data = request.form.to_dict()

        print(data)

        return data

    else:

        return 'request was not POST'

در واقع ما با این کار به صورت پویا آدرس ها را دریافت می کنیم. یعنی هر آدرسی که به صفحه ی اصلی اضافه شود را گرفته و سپس به دنبال فایل HTML ای به همین نام می گیردیم تا آن را نمایش بدهیم. در صورتی که کد بالا را ذخیره کنید و به صفحات مختلف خودمان برویم همه چیز به خوبی کار می کند و نباید مشکلی داشته باشید اما انجام این کار یک باگ را به برنامه ی ما معرفی می کند. چه باگی؟ اگر به آدرسی برویم که وجود نداشته باشد چطور؟ مثلا اگر به آدرس http://127.0.0.1:5000/indexaslfhaslfjhaslkfh برویم چه می شود؟ بله برنامه ی ما crash کرده و متوقف می شود و خطایی را نیز در مرورگر می گیریم. برای حل این مشکل می توانیم از بلوک های try & except استفاده کنیم:

from flask import Flask, render_template, redirect, request, abort

app = Flask(__name__)







@app.route('/')

def home():

    return render_template('index.html')







@app.route('/<string:page_name>')

def page_name(page_name):

    try:

        return render_template(page_name + '.html')

    except:

        return abort(404)







@app.route('/submit_form', methods=['POST', 'GET'])

def submit_form():

    if request.method == 'POST':

        data = request.form.to_dict()

        print(data)

        return data

    else:

        return 'request was not POST'

همانطور که می بینید من در خط اول، تابع abort را از flask وارد کرده ام. حالا یک بلوک try & except را نوشته ام تا اگر فایل HTML مربوطه را پیدا نکردیم یک صفحه ی ۴۰۴ به کاربر نمایش داده شود. شما می توانستید به جای صفحه ی ۴۰۴، کاربر را به صفحه ی اصلی (/) منتقل (redirect) می کردید اما من ترجیح می دهم که از یک صفحه ی ۴۰۴ استفاده کنم تا کاربر متوجه شود که به آدرس اشتباهی رفته است. تنها کاری که باقی مانده است این است که کاربر را به صفحه ی thankyou.html منتقل کنیم:

@app.route('/submit_form', methods=['POST', 'GET'])

def submit_form():

    if request.method == 'POST':

        data = request.form.to_dict()

        print(data)

        return redirect('/thankyou', code=302)

    else:

        return 'request was not POST'

حالا برای تست این کد باید به آدرس http://127.0.0.1:5000/contact رفته و یک بار فرم خود را ثبت کنیم. به محض فشردن کلید send به صفحه ی http://127.0.0.1:5000/thankyou منتقل می شوید که پیام ما را نشان می دهد.

ثبت داده ها در دیسک

در حال حاضر زمانی که کاربر پیامی را برای ما ارسال می کند فقط از او تشکر می کنیم اما هیچ روشی برای ذخیره ی پیام او نداریم تا بعدا آن را مطالعه کنیم. در واقع این داده ها در مموری سیستم (سرور) ما ذخیره می شوند بنابراین به محض ریستارت شدن سیستم ما یا توقف سرور، تمام این داده ها نیز حذف می شوند. راه حل چیست؟ اگر در حال برنامه نویسی یک وب سایت واقعی بودیم باید از پایگاه های داده ای مانند MongoDB یا MySQL و امثال آن ها استفاده می کردیم اما این پایگاه های داده نیاز به دوره ی جداگانه ی خودشان را دارند و نمی توانیم در یک دوره ی پایتون آن ها را نیز آموزش بدهیم. کار اصلی این پایگاه های داده، ذخیره ی داده های ما روی دیسک است تا فقط در مموری نباشند. ما می توانیم از یک فایل txt ساده برای انجام این کار استفاده کنیم. طبیعتا اگر به حوزه ی توسعه ی وب علاقه پیدا کرده اید باید به آموزش های پایگاه داده ی MongoDB یا MySQL را از روکسو مطالعه کنید اما فعلا برای وب سایت ساده ی ما، یک فایل txt کفایت می کند.

برای شروع کار من یک فایل به نام database.txt را در پوشه ی اصلی پروژه ایجاد می کنم. در مرحله ی بعدی به فایل server.py برگشته و کد را کمی تغییر می دهیم:

from flask import Flask, render_template, redirect, request, abort

app = Flask(__name__)







@app.route('/')

def home():

    return render_template('index.html')







@app.route('/<string:page_name>')

def page_name(page_name):

    try:

        return render_template(page_name + '.html')

    except:

        return abort(404)







def write_to_file(data):

    with open('database.txt', mode='a') as database:




        email = data['email']

        subject = data['subject']

        message = data['message']




        database.write(f'\n{email},{subject},{message}')







@app.route('/submit_form', methods=['POST', 'GET'])

def submit_form():

    if request.method == 'POST':

        data = request.form.to_dict()

        write_to_file(data)

        return redirect('/thankyou', code=302)

    else:

        return 'request was not POST'

من ابتدا متدی به نام write_to_file را تعریف کرده ام که یک آرگومان به نام data را گرفته و خصوصیات email و subject و message را از آن دریافت می کند. در نهایت این خصوصیات را در یک خط می نویسیم اما آن ها را با علامت ویرگول انگلیسی از هم جدا کرده ایم. در ضمن n\ همان کاراکتر new line است که قبلا درباره اش بحث کرده ایم و تنها کاری که می کند رفتن به خط بعدی است. با این کار هر پیام را در یک خط جداگانه خواهیم داشت و پیام ها با هم ترکیب نمی شوند. در نهایت در route مربوط به submit_form به جای اینکه data را print کنیم، write_to_file را صدا زده ایم تا پیام ها در این فایل ذخیره شود.

برای تست این کد باید به صفحه ی http://127.0.0.1:5000/contact رفته و چند پیام را ثبت کنیم. من دو پیام را ثبت می کنم و سپس نگاهی به فایل database.txt می اندازم:

info@roxo.ir,Notice,This is a test message #1

example@domain.com,topic 2,This is a test message #2

بنابراین سیستم ما به خوبی کار می کند.

کار با فایل های CSV

مشکل کار با فایل های txt به عنوان پایگاه داده این است که نظم و ترتیبی در آن ها وجود ندارد و خودمان این نظم را به وجود آوردیم که آن هم فقط ترتیب نوشته شدن اجزای فرم است. فایل های CSV از دسته فایل های مربوط به Excel هستند بنابراین نظم بهتری دارند. پایتون یک ماژول CSV برای کار با این فایل ها دارد بنابراین ما نیز از این ماژول استفاده می کنیم. البته csv مخفف comma separated value است (یعنی مقادیر جدا شده با ویرگول) بنابراین باز هم مقادیری را خواهیم داشت که با ویرگول جدا شده اند. منظور من از نظم، نظم کاری آن در اسکریپت پایتون است.

برای شروع کار ابتدا به جای فایل database.txt یک فایل جدید به نام database.csv ایجاد کنید. درون این فایل نیز متن زیر را اضافه کنید:

email,subject,message

این نوشته ها سه ستون ما را مشخص می کنند. طبیعتا در مرحله ی اول باید ماژول CSV را وارد فایل server.py کنیم. در وب سایت رسمی پایتون یک نمونه ی کار با این ماژول آمده است:

>>> import csv

>>> with open('eggs.csv', newline='') as csvfile:

...     spamreader = csv.reader(csvfile, delimiter=' ', quotechar='|')

...     for row in spamreader:

...         print(', '.join(row))

Spam, Spam, Spam, Spam, Spam, Baked Beans

Spam, Lovely Spam, Wonderful Spam

بنابراین ماژول csv متد هایی به نام csv_writer و csv_reader را به ما می دهد که می توانند به ترتیب فایل های CSV را خوانده یا در آن ها چیزی بنویسند. آرگومان اولی که این توابع می گیرند فایل csv باز شده است (در کد بالا csvfile) و آرگومان دوم delimiter یا جدا کننده ی مقادیر است. آرگومان سوم نیز مشخص می کند که آیا هر مقدار توسط علامت خاصی احاطه شده باشد یا خیر؟ شما می توانید از هر علامتی در این قسمت استفاده کنید (در کد بالا از | استفاده شده است).

حالا بر اساس چیزی که می دانیم و متدی که write_to_file نام داشت یک متد دیگر را می نویسیم. من ابتدا کد های آماده شده را برایتان قرار می دهم و سپس به سراغ توضیحات آن می روم:

from flask import Flask, render_template, redirect, request, abort

import csv




app = Flask(__name__)







@app.route('/')

def home():

    return render_template('index.html')







@app.route('/<string:page_name>')

def page_name(page_name):

    try:

        return render_template(page_name + '.html')

    except:

        return abort(404)







# def write_to_file(data):

#     with open('database.txt', mode='a') as database:




#         email = data['email']

#         subject = data['subject']

#         message = data['message']




#         database.write(f'\n{email},{subject},{message}')







def write_to_csv(data):

    with open('database.csv', mode='a') as csv_database:




        email = data['email']

        subject = data['subject']

        message = data['message']




        writer = csv.writer(csv_database, delimiter=',', lineterminator='\n', quoting=csv.QUOTE_MINIMAL)

        writer.writerow([email, subject, message])







@ app.route('/submit_form', methods=['POST', 'GET'])

def submit_form():

    if request.method == 'POST':

        data = request.form.to_dict()

        write_to_csv(data)

        return redirect('/thankyou', code=302)

    else:

        return 'request was not POST'

من کد قبلی مربوط به تابع write_to_file را برایتان باقی گذاشته ام تا بتوانید آن را با کد تابع جدید write_to_csv مقایسه کنید. اولین کاری که در کد بالا انجام داده ایم وارد کردن ماژول csv است. در مرحله ی بعدی شروع به نوشتن write_to_csv می کنیم. مثل همیشه فایل database.csv را با ستور with open باز می کنیم. برای دریافت مقادیر از فرم از همان روش همیشگی خودمان استفاده کرده ایم بنابراین چیز جدیدی نیست که نیاز به توضیح داشته باشد. تفاوت اصلی اینجاست که در ماژول csv یک متد به نام writer داریم و این متد وظیفه ی نوشتن داده در فایل های csv را بر عهده دارد. آرگومان اولی که گرفته ایم همان فایل csv ما است، آرگومان دوم delimiter یا جدا کننده ی داده ها در یک ردیف است که ما از ویرگول استفاده کرده ایم، آرگومان سوم lineterminator است که مشخص می کند هر خط با چه کاراکتری به پایان برسد؟ ما n\ را انتخاب کرده ایم تا با نوشتن هر ردیف به خط بعدی برویم. نهایتا  quoting را داریم که مشخص می کند مقادیر نوشته شده در فایل csv دارای quotation (علامت های "") باشند یا خیر. من مقدار csv.QUOTE_MINIMAL را به آن داده ام که یعنی فقط در صورت ضرورت از quote استفاده کند (فقط برای کاراکتر های ویژه). برای نوشتن داده ها روی database.csv نیز کافی است که متد writerow را صدا بزنیم و داده هایمان را به صورت یک لیست به آن پاس بدهیم. توجه داشته باشید که می توانید به جای لیست ها از دیکشنری ها نیز استفاده کنید، البته روش استفاده کمی متفاوت است.

برای تست این کد به آدرس http://127.0.0.1:5000/contact رفته و دو یا سه بار دیگر فرم را ثبت می کنیم. پس از اینکه این کار را انجام دادید، فایل database.csv را باز کنید، باید چنین نتیجه ای داشته باشید:

email,subject,message

email@domain.com,some Title,my message

email@domain.com,some Title2,my message

بنابراین کد ما بدون مشکل کار می کند. حالا از شما یک سوال دارم؛ اگر بخواهیم از پایگاه داده ای واقعی استفاده کنیم چطور؟ من به شما گفتم که استفاده از این پایگاه های داده نیاز به یک دوره ی جداگانه دارد اما اگر بخواهیم فقط با مباحث پایه ای آن آشنا شویم چطور؟ درباره ی این مسئله در جلسه ی بعدی صحبت خواهیم کرد.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری پایتون حرفه‌ای توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.