สรุปสั้น ๆ
สำหรับมือใหม่ อยากลองทำ Gallery แสดงภาพของตัวเองที่สามารถอัปโหลดรูปได้ไม่ใช่เรื่องยาก ทำยังไง ไปดู!!!
เขียนโดย
Sirasit Boonklang (Aeff)
Tech and Coding Consultant
บทความนี้ตีพิมพ์ และ เผยแพร่เมื่อ 12 เมษายน 2566
โดยเราสามารถใช้ Flask ที่เป็น Framework ตัวช่วยในการทำเว็บแบบง่าย ๆ สำหรับสาย Python และในการที่เราจะทำเว็บแอปพลิเคชันที่มีการอัปโหลดไฟล์ขึ้นมาได้ใน Flask จะมี Lib ในการช่วยจัดการในการอัปโหลดไฟล์มายังหน้าเว็บนั่นก็คือ Flask-Uploads
การเริ่มต้นใช้งาน Flask-Uploads
เราจะเริ่มทำ Flask Application ที่ให้ผู้ใช้สามารถอัปโหลดไฟล์ไปยังเซิร์ฟเวอร์และแสดงไฟล์ที่อัปโหลดบนหน้าเว็บได้ โดยเราจะมาเริ่มแบบง่าย ๆ
โดยเริ่มจากเปิดโปรเจกต์ขึ้นมา สร้าง Virtual Environments ด้วยคำสั่ง python -m venv venv แล้วทำการ Activate ด้วยคำสั่ง .\venv\Scripts\activate
ทำการติดตั้ง Flask-Uploads และ flask-wtf ด้วยคำสั่ง pip install Flask-Uploads flask-wtf
ทำการสร้างโฟลเดอร์ templates ที่มีไฟล์ index.html และ ไฟล์ app.py ไว้อยู่ในตำแหน่งเดียวกันกับโฟลเดอร์ templates (ห้ามอยู่ใน templates นะ) ตามโครงสร้างของแผนภาพด้านล่างได้เลยนะครับ
Gallery-Flask-Uploads/
├── app.py
└── templates/
└── index.html
เราเริ่มมาดูที่ไฟล์หลักนั่นคือ app.py กันก่อนครับ ส่วนแรกจะเป็นการเรียกใช้งาน Lib ที่เกี่ยวข้องดังนี้
app.py
from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import FileField, SubmitField
from werkzeug.utils import secure_filename
import os
from wtforms.validators import InputRequired
- Flask เป็นการเรียกใช้เฟิร์มเวิร์ก Flask
- render_template เรียกใช้ฟังก์ชันสำหรับการแสดงผลหน้าไฟล์ HTML
- FlaskForm เป็นคลาสจากโมดูล flask_wtf สำหรับสร้างฟอร์ม HTML โดยใช้คลาส Python
- FileField เป็นคลาสจากโมดูล wtforms ที่สร้าง Input ไฟล์ในแบบฟอร์ม
- SubmitField เป็นคลาสจากโมดูล wtforms ที่สร้างปุ่มส่งในแบบฟอร์ม
- secure_filename เป็นฟังก์ชันจากโมดูล Werkzeug ที่ return ชื่อไฟล์เวอร์ชันที่ปลอดภัย
- os เป็นโมดูลที่เอามาใช้ในการอ่านหรือเขียนลงในระบบ
- InputRequired เป็นคลาสจากโมดูล wtforms.validators ที่ตรวจสอบว่า Input แบบฟอร์มไม่ว่างเปล่าหรือไม่
ส่วนที่ 2 ในไฟล์จะเป็นการกำหนดค่าต่าง ๆ และ สร้างคลาส FlaskFrom สำหรับการอัปโหลดไฟล์
app = Flask(__name__)
app.config['SECRET_KEY'] = '8OgSq5gCe7'
app.config['UPLOAD_FOLDER'] = 'static/files'
class UploadFileForm(FlaskForm):
file = FileField("File", validators=[InputRequired()])
submit = SubmitField("Upload File")
- app = Flask(name) สร้างอินสแตนซ์ของคลาส Flask
- app.config[‘SECRET_KEY’] เป็นการสร้าง SECRET_KEY เพื่อใช้กับ session cookies หรือ secure data อื่น
- app.config[‘UPLOAD_FOLDER’] = ‘static/files’ กำหนดว่าจะจัดเก็บไฟล์ไว้ที่ไหน
- class UploadFileForm(FlaskForm): คลาสย่อย FlaskForm ใหม่ชื่อ UploadFileForm
- file = FileField(“File”, validators=[InputRequired()]) สร้าง FileField ที่ให้ User อัปโหลดไฟล์ InputRequired() เป็นตัวตรวจสอบว่าฟิลด์ว่างเปล่าหรือไม่
ส่วนต่อมาจะเป็นการกำหนดการ route ใน Flask app และการทำงานในฟังก์ชัน UploadFile2Web โดยโค้ดการทำงานส่วนสุดท้ายจะมีดังนี้
@app.route('/', methods=['GET', 'POST'])
def UploadFile2Web():
form = UploadFileForm()
if form.validate_on_submit():
file = form.file.data
file.save(os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(file.filename)))
image_files = os.listdir(app.config['UPLOAD_FOLDER'])
return render_template('index.html', form=form, image_files=image_files)
if __name__ == '__main__':
app.run(debug=True)
- @app.route(‘/’, methods=[‘GET’, ‘POST’]) โดยเราได้มีการกำหนดให้เมื่อมี Request มาที่ Path Root โดยสามารถใช้งานได้ทั้ง Methods GET และ POST โปรแกรมก็จะทำงานที่ฟังก์ชัน UploadFile2Web ()
- UploadFile2Web() เราได้มีการสั่งให้ render ไฟล์ index.html ออกมาแสดงเมื่อมีการเรียกมาที่ route นี้ โดยใช้คำสั่ง render_template(‘index.html’, form=form, image_files=image_files) นอกจากที่ Flask Application ของเราจะแสดงหน้า index.html ออกมา เรายังมีการส่งตัวแปรไปยังไฟล์ index.html ด้วยนั่นคือ ตัวแปร form และ image_files
- เมื่อเราได้ทำการสร้าง instance ของ UploadFileForm() เก็บไว้ในตัวแปร form แล้ว เราก็มาทำการเช็คดูว่า form ได้มีการ submit และ validate แล้วหรือไม่
- ถ้าใช่จะทำการ Save ไฟล์ไปยัง Server โดยใช้คำสั่ง file.save(os.path.join(app.config[‘UPLOAD_FOLDER’], secure_filename(file.filename)))
- สุดท้ายเราก็ไปเอาไฟล์ภาพมาแสดง โดยเอาไฟล์จาก Path ที่เก็บไว้ในตัวแปร UPLOAD_FOLDER ด้วยคำสั่ง os.listdir(app.config[‘UPLOAD_FOLDER’]) แล้วเก็บไว้ในตัวแปร image_files
- แล้วตอนที่ index.html ถูก render ออกมาก็จะนำภาพทั้งหมดที่อยู่ใน Path ที่เราเก็บไว้มาแสดงในหน้าเว็บ
และตอนนี้เราก็ได้ทำการเขียนโค้ดส่วนของการทำงานการจัดการไฟล์ และ การอัปโหลดไฟล์ไปยัง Server ได้แล้วในไฟล์ app.py โดยโค้ดแบบเต็ม ๆ เราจะได้ในลักษณะนี้
app.py (Full Code)
from flask import Flask, render_template, url_for
from flask_wtf import FlaskForm
from wtforms import FileField, SubmitField
from werkzeug.utils import secure_filename
import os
from wtforms.validators import InputRequired
app = Flask(__name__)
app.config['SECRET_KEY'] = '8OgSq5gCe7'
app.config['UPLOAD_FOLDER'] = 'static/files'
class UploadFileForm(FlaskForm):
file = FileField("File", validators=[InputRequired()])
submit = SubmitField("Upload File")
@app.route('/', methods=['GET', 'POST'])
def UploadFile2Web ():
form = UploadFileForm()
if form.validate_on_submit():
file = form.file.data
file.save(os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(file.filename)))
image_files = os.listdir(app.config['UPLOAD_FOLDER'])
return render_template('index.html', form=form, image_files=image_files)
if __name__ == '__main__':
app.run(debug=True)
ต่อมาเราก็มาสร้างหน้าเว็บโดยไปที่ไฟล์ index.html ในโฟลเดอร์ templates โดยโค้ดเริ่มต้นของไฟล์ HTML จะเป็นแบบนี้
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Gallery</title>
</head>
<body>
<h1>Upload Image</h1>
<form method='POST' enctype='multipart/form-data'>
{{form.hidden_tag()}}
{{form.file()}}
{{form.submit()}}
</form>
<p>Total images: {{ image_files|length }}</p>
<div class="gallery">
{% for image in image_files %}
<img src="{{ url_for('static', filename='files/' + image) }}" alt="{{ image }}" onclick="openModal('{{ url_for('static', filename='files/' + image) }}')">
{% endfor %}
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Gallery</title>
</head>
<body>
<h1>Upload Image</h1>
<form method='POST' enctype='multipart/form-data'>
{{form.hidden_tag()}}
{{form.file()}}
{{form.submit()}}
</form>
<p>Total images: {{ image_files|length }}</p>
<div class="gallery">
{% for image in image_files %}
<img src="{{ url_for('static', filename='files/' + image) }}" alt="{{ image }}" onclick="openModal('{{ url_for('static', filename='files/' + image) }}')">
{% endfor %}
</div>
</body>
</html>
โดยผมขอข้าม HTML เบื้องต้นไปแต่จะมาโฟกัสในส่วนที่รับค่าและทำงานร่วมกับ Flask นะครับโดยส่วนหลัก ๆ จะอยู่ภายในแท็ก Body
<h1>Upload Image</h1>
<form method='POST' enctype='multipart/form-data'>
{{form.hidden_tag()}}
{{form.file()}}
{{form.submit()}}
</form>
- ส่วนแรกเราจะบอกว่า HTML Form นี้เป็นส่วนอัปโหลดภาพโดยใช้ <h1>Upload Image</h1>
- <form method=’POST’ enctype=’multipart/form-data’> บอกว่าฟอร์มจะถูกส่งโดยใช้ Methods HTTP POST และข้อมูลในฟอร์มจะถูกเข้ารหัสโดยใช้ประเภทการเข้ารหัส “multipart/form-data”
- {{form.hidden_tag()}} เป็นฟังก์ชันพิเศษใน Flask ที่สร้างช่อง Input ที่ซ่อนอยู่เพื่อป้องกันการโจมตี CSRF
- {{form.file()}} สร้างฟิลด์สำหรับรับ Input ไฟล์ที่จะอัปโหลด
- {{form.submit()}} สร้างปุ่มที่คลิกได้ซึ่งส่งแบบฟอร์มไปยังเซิร์ฟเวอร์
ส่วนต่อมาจะเป็นส่วนในการแสดงจำนวนและรูปทั้งหมดที่มีอยู่ในระบบ
<p>Total images: {{ image_files|length }}</p>
<div class="gallery">
{% for image in image_files %}
<img src="{{ url_for('static', filename='files/' + image) }}" alt="{{ image }}" onclick="openModal('{{ url_for('static', filename='files/' + image) }}')">
{% endfor %}
</div>
<p>Total images: {{ image_files|length }}</p>
<div class="gallery">
{% for image in image_files %}
<img src="{{ url_for('static', filename='files/' + image) }}" alt="{{ image }}" onclick="openModal('{{ url_for('static', filename='files/' + image) }}')">
{% endfor %}
</div>
- {{ image_files|length }} คือ Jinja2 expression ที่เอาไว้ดูว่าความยาวของ list ที่ชื่อว่า image_file มีเท่าไหร่แล้วเราก็นำค่านี้มาแสดงเป็นจำนวนรูปที่มีในระบบ
- {% for image in image_files %}เป็น Jinja2 loop ที่วนเอาแต่ละภาพใน list ของ image_file ออกมา
- <img src=”{{ url_for(‘static’, filename=’files/’ + image) }}” alt=”{{ image }}” onclick=”openModal(‘{{ url_for(‘static’, filename=’files/’ + image) }}’)”> เป็นส่วนที่แสดงรูปภาพในแกลเลอรี โดยไปเอารูปภาพจากโฟลเดอร์ static/file รูปแต่ละรูปออกมาโดยใช้ url_for นอกจากนั้นเรายังมีการสร้าง modal สำหรับกดเข้าไปดูภาพเต็มของแต่ละภาพอีกด้วย โดยส่วนของ modal จะมีการนำ JavaScript มาช่วยโดยมีโค้ดการทำงานดังนี้
<div id="myModal" class="modal"> <span class="close" onclick="closeModal()">×</span> <img class="modal-content" id="img01"> </div>
<script> var modal = document.getElementById("myModal"); var modalImg = document.getElementById("img01"); function openModal(imgSrc) { modal.style.display = "block"; modalImg.src = imgSrc; } function closeModal() { modal.style.display = "none"; } </script>
<div id="myModal" class="modal"> <span class="close" onclick="closeModal()">×</span> <img class="modal-content" id="img01"> </div>
<script> var modal = document.getElementById("myModal"); var modalImg = document.getElementById("img01"); function openModal(imgSrc) { modal.style.display = "block"; modalImg.src = imgSrc; } function closeModal() { modal.style.display = "none"; } </script>
ตอนนี้หน้า Gallery แบบง่าย ๆ ไว ๆ ก็เสร็จไปแล้วโดยโค้ดในส่วนไฟล์ index.html แบบเต็ม ๆ จะเป็นดังนี้
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<!-- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300&display=swap" rel="stylesheet">
<title>Home</title>
</head>
<body>
<h1>Image Gallery</h1>
<form method='POST' enctype='multipart/form-data'>
{{form.hidden_tag()}}
{{form.file()}}
{{form.submit()}}
</form>
<p>Total images: {{ image_files|length }}</p>
<div class="gallery">
{% for image in image_files %}
<img src="{{ url_for('static', filename='files/' + image) }}" alt="{{ image }}" onclick="openModal('{{ url_for('static', filename='files/' + image) }}')">
{% endfor %}
</div>
<!-- The Modal -->
<div id="myModal" class="modal">
<span class="close" onclick="closeModal()">×</span>
<img class="modal-content" id="img01">
</div>
<script>
var modal = document.getElementById("myModal");
var modalImg = document.getElementById("img01");
function openModal(imgSrc) {
modal.style.display = "block";
modalImg.src = imgSrc;
}
function closeModal() {
modal.style.display = "none";
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<!-- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300&display=swap" rel="stylesheet">
<title>Home</title>
</head>
<body>
<h1>Image Gallery</h1>
<form method='POST' enctype='multipart/form-data'>
{{form.hidden_tag()}}
{{form.file()}}
{{form.submit()}}
</form>
<p>Total images: {{ image_files|length }}</p>
<div class="gallery">
{% for image in image_files %}
<img src="{{ url_for('static', filename='files/' + image) }}" alt="{{ image }}" onclick="openModal('{{ url_for('static', filename='files/' + image) }}')">
{% endfor %}
</div>
<!-- The Modal -->
<div id="myModal" class="modal">
<span class="close" onclick="closeModal()">×</span>
<img class="modal-content" id="img01">
</div>
<script>
var modal = document.getElementById("myModal");
var modalImg = document.getElementById("img01");
function openModal(imgSrc) {
modal.style.display = "block";
modalImg.src = imgSrc;
}
function closeModal() {
modal.style.display = "none";
}
</script>
</body>
</html>
เมื่อทั้งโค้ดส่วน app.py และ index.html เสร็จแล้วเราสามารถรันโปรแกรมด้วยคำสั่ง flask runแล้วโปรแกรมจะทำงานที่ localhost:5000
โอเคครับ ตอนนี้เราได้หน้าเว็บแบบไว ๆ ที่สามารถลองกดอัปโหลดไฟล์ขึ้นมาได้แล้วนะครับ แต่ปัญหาคือหน้าเว็บยังไม่สวย ภาพก็ใหญ่ 😅 เราเลยต้องใช้ CSS เข้ามาช่วย
สำหรับการเพิ่ม CSS เข้ามาในไฟล์ index.html ใน Flask เราจะใช้คำสั่ง <link rel=”stylesheet” href=”{{ url_for(‘static’, filename=’css/style.css’) }}”> ใส่ไว้ในแท็ก <head></head>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<title>Image Gallery</title>
</head>
แล้วโค้ดของ CSS ของผมจะอยู่ที่ Path static/css/style.css
Gallery-Flask-Project/ ├── app.py ├── static/ │ ├── css/ │ │ └── style.css │ └── files/ └── templates/ └── index.html
โดยโค้ดในไฟล์ style.css ของผมมีดังนี้
/* Set default styles for the body */
body {
font-family: 'Roboto', sans-serif;
text-align: center;
background-color: #f0f0f0;
}
/* Create a gallery grid */
.gallery {
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: 2px;
}
/* Style the gallery images */
.gallery img {
margin: 5px;
width: calc(25%);
height: auto;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border: 1px solid #f0f0f0;
border-radius: 10px;
transition: 0.3s;
}
/* On hover, increase the border size of the images */
.gallery img:hover {
border: 5px solid #777;
}
/* Create a modal to display full-sized images */
.modal {
display: none;
position: fixed;
z-index: 100;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
}
/* Style the full-sized image */
.modal-content {
margin: 0 auto;
display: block;
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
/* Style the close button for the modal */
.close {
position: absolute;
top: 15px;
right: 35px;
color: #f1f1f1;
font-size: 40px;
font-weight: bold;
transition: 0.3s;
}
.close:hover, .close:focus {
color: #bbb;
text-decoration: none;
cursor: pointer;
}
/* Style the upload form */
form {
display: inline-block;
margin-bottom: 20px;
}
หลังจากนั้นกดอีกครั้งทำการรันโปรแกรมอีกครั้งด้วยคำสั่ง flask run และอัปโหลดรูปขึ้นไปก็จะมีหน้าของเว็บแบบนี้
หากใครอยากลองเอาโปรเจกต์นี้ไปปรับแต่งหรือลองใช้งานก็สามารถมาโคลนที่ Github Repo นี้ได้เลยครับ 😊 aeff60/Image-Gallery-Flask (github.com)
สำหรับรายละเอียดของ Library ที่ใช้ในโปรเจกต์นี้สามารถอ่านเพิ่มเติมได้ที่ลิงก์นี้นะครับ
Flask-Uploads: https://flask-uploads.readthedocs.io/en/latest/
Flask WTF: https://flask-wtf.readthedocs.io/en/1.0.x/
ระบบฝึกทักษะ การเขียนโปรแกรม
ที่พร้อมตรวจผลงานคุณ 24 ชั่วโมง
- โจทย์ปัญหากว่า 200 ข้อ ที่รอท้าทายคุณอยู่
- รองรับ 9 ภาษาโปรแกรมหลัก ไม่ว่าจะ Java, Python, C ก็เขียนได้
- ใช้งานได้ฟรี ! ครบ 20 ข้อขึ้นไป รับ Certificate ไปเลย !!