Python全棧工程師之從網頁搭建入門到Flask全棧專案實戰(6) - Flask表單的實現

葛老頭發表於2022-12-15

1.表單介紹

1.1.表單知識回顧

 

常見的表單元素:

  • 表單標籤<form>
    • action:表單提交的URL地址
    • method:表單請求的方式(GET/POSt)
    • enctype:請求內容的形式,如:application/x-www-form-urlencoded、multipart/form-data
  • 單行文字框/多行文字框
    • textarea:多行文字
    • 單行文字(type的不同值),常見的有:text(單行文字)、password(密碼)、email(郵箱)、url(URL)、number(數字)、color(顏色)、日期時間等(date、month、week等等)
  • 選擇(單選、多選、下拉選擇)
    • 單選: <input type="radio"> 
    • 多選: <input type="checkbox"> 
    • 下拉框選擇: <select><option></option></select> 
  • 隱藏表單域: <input type="hidden"> 
  • 表單按鈕: <input type="button">  <button></button> 
  • 檔案上傳框: <input type="file"> 
 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>表單知識點回顧</title>
 6 </head>
 7 <body>
 8 <form action="/index" method="post" enctype="multipart/form-data">
 9     <ul>
10         <li>
11             使用者地址:
12             <textarea name="" id="" cols="30" rows="10" placeholder="請輸入地址"></textarea>
13         </li>
14         <li>
15             使用者名稱:
16             <input type="text" placeholder="請輸入使用者名稱">
17         </li>
18         <li>
19             密碼:
20             <input type="password" placeholder="請輸入密碼">
21         </li>
22         <li>
23             使用者的年齡:
24             <input type="number">
25         </li>
26         <li>
27             性別:
28             <label><input type="radio" value="男" name="sex"></label>
29             <label><input type="radio" value="女" name="sex"></label>
30         </li>
31         <li>
32             愛好
33             <input id="id-paly-ball" type="checkbox" value="打球">
34             <label for="id-paly-ball">打球</label>
35             <input id="id-paly" type="checkbox" value="玩耍">
36             <label for="id-paly">玩耍</label>
37         </li>
38     </ul>
39 </form>
40 </body>
41 </html>

 

在檢視中獲取表單值:

  • get請求: request.args.get('name',None) 
  • post請求: request.form.get('name',None) 

 

思考:HTML表單在Flask中如何快速使用?

 

1.2.wtf表單介紹

透過在Flask中寫python程式碼,可以直接生成HTML表單,透過wtf實現。

flask-wtf提供的3個組要功能:

  • 整合wtforms
  • CSRF保護,flask-wtf能保護所有表單免受跨站請求偽造(CSRF)的攻擊
  • 與Flask-Uploads一起支援檔案上傳

安裝

  • pip安裝: pip install Flask-WTF 
  • 原始碼安裝: python setup.py install 

下載好之後如何使用呢?需不需要一些配置呢?它的配置很簡單,它配置的目的就是用來做CSRF保護。只需要在Flask app上加上 WTF_CSRF_SECRET_KEY = 'a random string' 就行了,這個key可以隨便給一個字串,沒有要求,只是一個隨機的串就行了。

1 from flask import Flask, render_template,flash
2 from flask_sqlalchemy import SQLAlchemy
3 
4 app = Flask(__name__)
5 # 配置資料庫的連線引數
6 app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:@127.0.0.1/test_flask'
7 app.config['SECRET_KEY'] = 'abc'    #訊息閃現保護的key,注意當訊息閃現的SECRET_KEY配置了,WTF_CSRF_SECRET_KEY也可以不用配置,但是你配置了也沒有影響,這個知識點了解一下
8 app.config['WTF_CSRF_SECRET_KEY'] = 'abc1234abc'    #WTF_CSRF_SECRET_KEY配置

 

第一個表單模型

1 from flask_wtf import FlaskForm     #匯入flask_wtf的FlaskForm類
2 from wtforms import StringField     #StringField表示的是一個文字的輸入框
3 
4 
5 class LoginForm(FlaskForm):     #繼承FlaskForm類
6     """ 登入表單的實現 """
7     username = StringField(label='使用者名稱')

 

1.3.表單常用欄位型別及渲染

 

表單常用欄位型別

  • 文字/字串
    •  StringField :字串輸入
    •  PasswordField :密碼輸入
    •  TextAreaField :長文字輸入
    •  HiddenField :隱藏表單域
  • 數值(整數,小數)
    •  FloatField :浮點數輸入
    •  IntegerField :整數輸入
    •  DecimalField :小數輸入(更準確)
  • 選擇
    •  RadioFied :radio單選
    •  SelectField :下拉單選
    •  SelectMultipleField :下拉多選
    •  BooleanField :勾選(核取方塊)
  • 日期/時間
    •  DateField :日期選擇
    •  DateTimeField :日期時間選擇
  • 檔案/檔案上傳
    •  FileField :檔案單選
    •  MultipleFileField :檔案多選
  • 其他
    •  SubmitField :提交按鈕
    •  FieldList :自定義的表單選擇列表(如:選擇使用者物件)
    •  FormField :自定義多個欄位構成的選項

表單欄位的常用核心引數

  •  lable :lable標籤(如:輸入框錢的文字描述)
  •  default :表單的預設值
  •  validators :表單驗證規則
  •  widget :定製介面顯示方式(如:文字框、選擇框)
  •  description :幫助文字

表單渲染

使用模板語法渲染表單內容:

  • 表單輸入區域: {{form.username}} 
  • 表單label: {{form.username.label}} 

 

例項程式碼:

forms.py:python編寫登入表單頁面匯入FlaskForm,wtfforms實現

1 from flask_wtf import FlaskForm
2 from wtforms import StringField, PasswordField, SubmitField
3 
4 
5 class LoginForm(FlaskForm):
6     """ 登入表單的實現 """
7     username = StringField(label='使用者名稱', default='admin')
8     password = PasswordField(label='密碼')
9     submit = SubmitField('登入')

app.py:匯入forms檔案,將python編寫好的HTML頁面展示類傳遞給html檔案

 1 from flask import Flask, render_template
 2 
 3 from forms import LoginForm
 4 
 5 app = Flask(__name__)
 6 app.config['WTF_CSRF_SECRET_KEY'] = 'abc1234abc'
 7 app.config['SECRET_KEY'] = 'abc'
 8 
 9 
10 @app.route('/form', methods=['GET', 'POST'])
11 def page_form():
12     """ form 表單練習 """
13     form = LoginForm()
14     return render_template('page_form.html', form=form)

page_form.html

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Flask Form表單練習</title>
 6 </head>
 7 <body>
 8     <h3>歡迎登入</h3>
 9     <form action="" method="post">
10         <p>
11             {{ form.username.label }}
12             {{ form.username }}
13         </p>
14         <p>
15             {{ form.password.label }}
16             {{ form.password }}
17         </p>
18         <p>
19             {{ form.submit }}
20         </p>
21 
22     </form>
23 </body>
24 </html>

 

1.4.透過表單儲存資料

保單儲存資料步驟:

  • 第一步:檢測表單是否已經透過驗證
    • form.validate_on_submit()
  • 第二步:獲取表單中傳遞過來的值
    •  form.field_name.data 
  • 第三步:業務邏輯程式碼編寫(結合ORM)

 

表單儲存資料的時候會觸發CSRF表單保護:

  • 預設模板是開啟CSRF保護
  • 關閉單個表單CSRF保護
    •  form = LoginForm(csrf_enabled=False) 
  • 全域性關閉(不推薦)
    •  在類上面加上:WTF_CSRF_ENABLED=False 

如果不關閉CSRF保護,如何處理:

同步請求CSRF保護,在模板中新增csrf_token,透過CSRF機制的驗證:

  • 方式一:{{ form.csrf_token }}
  • 方式二:<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

 

例項程式碼:

新增使用者登錄檔單,對註冊的內容進行入庫,註冊成功後返回index主頁面。

app.py

 1 from flask import Flask, render_template, flash, redirect, url_for
 2 from flask_sqlalchemy import SQLAlchemy
 3 
 4 from forms import RegisterForm
 5 
 6 app = Flask(__name__)
 7 # 配置資料庫的連線引數
 8 app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:@********/test_flask'
 9 app.config['WTF_CSRF_SECRET_KEY'] = 'abc1234abc'
10 app.config['SECRET_KEY'] = 'abc'
11 db = SQLAlchemy(app)
12 
13 
14 class User(db.Model):
15     __tablename__ = 'weibo_user'
16     id = db.Column(db.Integer, primary_key=True)
17     username = db.Column(db.String(64), nullable=False)
18     password = db.Column(db.String(256), nullable=False)
19     birth_date = db.Column(db.Date, nullable=True)
20     age = db.Column(db.Integer, default=0)
21 
22 @app.route('/')
23 def index():
24     """  首頁 """
25     return render_template('index.html')
26 
27 @app.route('/user/register', methods=['GET', 'POST'])
28 def page_register():
29     """ 新使用者註冊 """
30     # csrf_enabled為False表示不做csrf校驗
31     # form = RegisterForm(csrf_enabled=False)
32     form = RegisterForm()
33     # 使用者在提交表單的時候,會觸發validate_on_submit
34     if form.validate_on_submit():
35         # 表單驗證透過,接下來處理業務邏輯
36         # 1. 獲取表單資料
37         username = form.username.data
38         password = form.password.data
39         birth_date = form.birth_date.data
40         age = form.age.data
41         # 2. 構建使用者物件
42         user = User(
43             username=username,
44             password=password,
45             birth_date=birth_date,
46             age=age
47         )
48         # 3. 提交到資料庫
49         db.session.add(user)
50         db.session.commit()
51         print('新增成功')
52         # 4. 跳轉到登入頁面
53         return redirect(url_for('index'))
54     else:
55         # 列印錯誤資訊
56         print(form.errors)
57     return render_template('page_register.html', form=form)

forms.py

 1 from flask_wtf import FlaskForm
 2 from wtforms import StringField, PasswordField, SubmitField, DateField, IntegerField
 3 
 4 
 5 class RegisterForm(FlaskForm):
 6     """ 使用者登錄檔單 """
 7 
 8     # def __init__(self, csrf_enabled, *args, **kwargs):
 9     #     super().__init__(csrf_enabled=csrf_enabled, *args, **kwargs)
10 
11     username = StringField(label='使用者名稱', default='')
12     password = PasswordField(label='密碼')
13     birth_date = DateField(label='生日')
14     age = IntegerField(label='年齡')
15     submit = SubmitField('註冊')

page_register.html

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>使用者註冊</title>
 6 </head>
 7 <body>
 8     <h3>使用者註冊</h3>
 9 {#  todo 註釋內容:使用宏來把表單進一步完善 #}
10     <form action="{{ url_for('page_register') }}" method="post">
11         {{ form.csrf_token }}
12         <p>
13             {{ form.username.label }}
14             {{ form.username }}
15         </p>
16         <p>
17             {{ form.password.label }}
18             {{ form.password }}
19         </p>
20         <p>
21             {{ form.birth_date.label }}
22             {{ form.birth_date }}
23         </p>
24         <p>
25             {{ form.age.label }}
26             {{ form.age }}
27         </p>
28         <p>
29             {{ form.submit }}
30         </p>
31 
32     </form>
33 </body>
34 </html>

index.html

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>使用者首頁</title>
 6 </head>
 7 <body>
 8 註冊成功
 9 </body>
10 </html>

 

 

2.表單驗證與圖片上傳

2.1.表單驗證

思考:以手機註冊為例,不驗證表單會怎麼樣?

  • 使用者輸入的可能不是手機號
  • 使用者輸入的可能不是他的手機號
  • 不斷的提交表單

思考:表單驗證為了什麼?

  • 更好的使用者體驗
  • 更少的安全隱患
  • 永遠不要相信使用者的輸入

內建的表單驗證器

  •  DataRequired/InputRequired :必填驗證
  •  Email/URL/UUID :電子郵箱/URL/UUID格式驗證
  •  Length(min=-1,max=-1,message=None) :長度範圍驗證
  •  EqualTo(fieldname,message=None) :重複驗證,用於密碼的二次輸入驗證和上一次是否一致

 

自定義表單驗證

  • 場景一:只有本表單使用,在表單類裡面建立一個方法,方法名為:validatge_需要驗證的變數,在方法中編寫驗證邏輯
  • 場景二:多個表單中使用,如:驗證手機號碼,登入/註冊/修改使用者資訊頁面均會用到。在表單類外面宣告一個驗證方法,傳入form物件。這個方法命名就沒有要求,這個見示例程式碼。

?圖為場景一的示例,場景二跟這個差不多;兩者的區別主要在於一個放到類裡面,一個放到類外面;備註:方法命名的規則暫時這麼理解,我學的好像是這樣的,我是這麼理解的,有知道所以然的可以教下我,謝謝!

 

例項:

場景一(當前表單使用):

 1 import re
 2 
 3 from flask_wtf import FlaskForm
 4 from wtforms import StringField, PasswordField, SubmitField, DateField, IntegerField
 5 from wtforms.validators import DataRequired,ValidationError
 6 
 7 class RegisterForm(FlaskForm):
 8     """ 使用者登錄檔單 """
 9 
10     username = StringField(label='使用者名稱', default='')
11     password = PasswordField(label='密碼',validators=[DataRequired("請輸入密碼")])  #增加必填驗證
12     birth_date = DateField(label='生日')
13     age = IntegerField(label='年齡')
14     submit = SubmitField('註冊')
15 
16     def validate_username(self,field):  #注意這個方法必須是validate_*****
17         """ 驗證使用者名稱 """
18         # 自定義:強制驗證使用者名稱為手機號
19         username = field.data
20         pattern = r'^1[0-9]{10}$'   #自定義正則匹配規則
21         if not re.search(pattern,username):
22             raise ValidationError('請輸入手機號碼')
23         return field

 

我們看到報錯提示資訊是在控制檯展示的,那麼我們怎麼讓提示資訊展示給使用者?

forms.py檔案裡面自定義了表單驗證規則,驗證失敗丟擲form的錯誤

app.py檔案無法提交時,將列印form檔案丟擲的異常.。異常內容為 {'username': ['請輸入手機號碼']} 是個items結構。

前端頁面,獲取forms的username的報錯資訊,進行展示

場景二(多個表單使用):

?圖froms.py:用python編寫登入的前端頁面、表單自定義的驗證、表單欄位使用自定義的驗證

疑問:在類外面自定義的驗證方法,為什麼要傳form,不傳行不行;不穿form就會報錯,說應該傳兩個引數,只傳了一個,那這是為什麼呢?

看了系統自帶驗證規則的原始碼,發現都是兩個形參form和field,就暫時這麼理解吧:因為原始碼有,跟他保持一致,form具體幹什麼使的後面研究了再說吧!

?圖app.py啟動檔案:將登入頁面forms.py交給html檔案page_form進行渲染展示,同時進行form表單的驗證

?圖:page_form.html+展實效果

2.2.圖片上傳

圖片上傳主要有兩種方式實現

  • 方式一:不使用wtf實現
  • 方式二:使用wtf的FileField並新增型別驗證

 

圖片上傳:不使用wtf

  • 1)設定<form>的enctype, enctype="multipart/form-data" 
  • 2)在檢視函式中獲取檔案物件 request.files 
  • 3)儲存檔案 f.save(file_path) 

 

檔名稱格式化: werkzeug.utils.secure_filename 

 

示例:

上傳不規則的檔名,secure_filename進行格式化

app.py

 1 import os
 2 
 3 from flask import Flask, render_template, redirect, url_for, request
 4 from werkzeug.utils import secure_filename
 5 
 6 from forms import UserAvatarForm
 7 
 8 app = Flask(__name__)
 9 app.config['WTF_CSRF_SECRET_KEY'] = 'abc1234abc'
10 app.config['SECRET_KEY'] = 'abc'
11 # 自定義的配置擴充套件,表示檔案上傳的路徑
12 app.config['UPLOAD_PATH'] = os.path.join(os.path.dirname(__file__), 'medias')
13 
14 @app.route('/img/upload', methods=['GET', 'POST'])
15 def img_upload():
16     """ 不使用wtf實現的檔案上傳 """
17     if request.method == 'POST':
18         # 獲取檔案列表
19         files = request.files
20         file1 = files.get('file1', None)
21         if file1:
22             # 儲存檔案
23             f_name = secure_filename(file1.filename)
24             print('filename:', f_name)
25             file_name = os.path.join(app.config['UPLOAD_PATH'], f_name)
26             file1.save(file_name)
27             print('儲存成功')
28         return redirect(url_for('img_upload'))
29     return render_template('img_upload.html')

img_upload.html

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>檔案上傳</title>
 6 </head>
 7 <body>
 8 <form action="/img/upload" method="post" enctype="multipart/form-data">
 9     <p>
10         <input type="file" name="file1">
11         <input type="file" name="file2">
12     </p>
13     <p>
14         <input type="submit" value="開始上傳">
15     </p>
16 </form>
17 </body>
18 </html>

 

圖片上傳:驗證

  • 檔案必須上傳: FileRequired 
  • 檔案型別驗證: FileAllowed 

app.py

 1 import os
 2 
 3 from flask import Flask, render_template, redirect
 4 from werkzeug.utils import secure_filename
 5 
 6 from forms import UserAvatarForm
 7 
 8 app = Flask(__name__)
 9 app.config['WTF_CSRF_SECRET_KEY'] = 'abc1234abc'
10 app.config['SECRET_KEY'] = 'abc'
11 # 自定義的配置擴充套件,表示檔案上傳的路徑
12 app.config['UPLOAD_PATH'] = os.path.join(os.path.dirname(__file__), 'medias')
13 
14 
15 @app.route('/avatar/upload', methods=['GET', 'POST'])
16 def avatar_upload():
17     """ 頭像上傳 """
18     form = UserAvatarForm()
19     if form.validate_on_submit():
20         # 獲取圖片物件
21         img = form.avatar.data
22         f_name = secure_filename(img.filename)
23         file_name = os.path.join(app.config['UPLOAD_PATH'], f_name)
24         img.save(file_name)
25         print('儲存成功')
26         return redirect('/')
27     else:
28         print(form.errors)
29     return render_template('avatar_upload.html', form=form)

forms.py

 1 from flask_wtf import FlaskForm
 2 from flask_wtf.file import FileRequired, FileAllowed
 3 from wtforms import FileField
 4 
 5 
 6 class UserAvatarForm(FlaskForm):
 7     """ 使用者頭像上傳 """
 8     avatar = FileField(label='上傳頭像', validators=[
 9         FileRequired('請選擇頭像檔案'),
10         FileAllowed(['png'], '僅支援PNG圖片上傳')
11     ])

avatar_upload.html

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>WTF 檔案上傳</title>
 6 </head>
 7 <body>
 8 <form action="/avatar/upload" method="post" enctype="multipart/form-data">
 9     {{ form.csrf_token }}
10     <p>
11         {{ form.avatar.label }}
12         {{ form.avatar }}
13     </p>
14     <p>
15         <input type="submit" value="開始上傳">
16     </p>
17 </form>
18 </body>

 

使用擴充:Flask-Uploads

  • 常用檔案型別驗證
  • 指定檔案上傳的目錄

 

相關文章