使用 Flask 创建 Web 应用程序
本教程解释了如何使用 Flask Web 框架开发基于 Web 的应用程序。 我们的 MeteoMaster 应用程序处理存储在数据库中的气象数据,并以以下图表的形式呈现:
散点图——布拉格、圣彼得堡的年度平均温度和湿度的累积报告。 圣彼得堡、旧金山、巴黎和新加坡。
折线图——每个城市的月平均温度和湿度。
“Staying Above the Weather” 应用程序概述
您将使用各种 Web 技术来实现以下应用程序功能:
函数 | 技术 |
|---|---|
气象数据操作 | SQLite 数据库用于存储数据, SQLAlchemy 包用于在 Python 代码中对数据库执行操作。 |
图形表示 | matplotlib 包用于绘制图表。 |
查看和编辑内容 | |
管理内容 | Flask 用于协调应用程序内容。 |
MeteoMaster 是一个包含以下组件的 Web 应用程序:

- 主页
应用程序入口点,用于渲染散点图并提供指向特定城市气候详细摘要的链接。
- 城市
一系列包含每个城市气候详细信息的页面。
- 登录
身份验证页面。 需要输入有效的凭据才能编辑气象数据。
- 编辑
一系列用于编辑特定城市数据的页面。
每个 HTML 页面都有一个对应的 Flask 视图,由 Python 代码实现。
在 PyCharm 中创建 Flask 应用程序
按照 创建 Flask 项目 中描述的步骤创建一个基本的 Flask 项目,以开始应用程序的原型设计。
在 新项目 对话框中选择 Flask。

在 位置 字段中,提供项目位置的路径,并输入 meteoMaster作为项目名称。 保留其余设置为默认值,然后点击 创建。
PyCharm 为您创建了新的虚拟环境,并选择使用 Jinja2 作为模板语言。 结果,您将获得预定义的项目结构和基本的 "Hello World!" 代码。

点击顶部 运行 小部件中的
运行 'meteoMaster' 以启动自动创建的运行配置。
在 运行 工具窗口中,点击超链接,预览基本 "Hello, world" 应用程序的页面。

现在安装 MeteoMaster 应用程序所需的所有包。 最简单的方法是使用项目依赖项(请参阅 使用 requirements.txt)。 右键点击项目根目录并选择 ,然后将文件名指定为 requirements.txt ,并添加以下依赖项列表。
blinker==1.7.0 click==8.1.7 contourpy==1.2.0 cycler==0.12.1 flask==3.0.0 fonttools==4.47.0 itsdangerous==2.1.2 jinja2==3.1.2 kiwisolver==1.4.5 markupsafe==2.1.3 matplotlib==3.8.2 numpy==1.26.2 packaging==23.2 pillow==10.2.0 pyparsing==3.1.1 python-dateutil==2.8.2 six==1.16.0 sqlalchemy==2.0.24 typing-extensions==4.9.0 werkzeug==3.0.1点击 安装依赖项 链接以继续安装这些包。

设置数据库
现在,为您的应用程序设置一个数据源。 使用 PyCharm,这非常简单。
从以下位置下载包含五个城市气象数据的预定义数据库:
https://github.com/allaredko/flask-tutorial-demo/blob/master/user_database
将 user_database 文件保存到项目根目录。
双击添加的文件。 在打开的 数据源和驱动程序 对话框中,点击 测试连接 以确保数据源已正确配置。 如果您看到 不完整的配置 警告,请点击 下载驱动程序文件。

点击 确定 以完成数据源的创建, 数据库工具窗口 将打开。
您应该会看到以下表格:
city和meteo。 双击每个表格以预览数据。 表格city有三列:city_id、city_name和city_climate(城市气候的简要文字描述)。 表格meteo有四列:city_id、month、average_humidity和average_temperature。 为city_id列的table定义了一个 外键 ,以建立两个表之间的关系。
创建一个 Python 文件, user_database.py ,用于处理新创建的数据库。 使用 SQLAlchemy declarative base 语法来描述数据库。
metadata = MetaData() engine = create_engine('sqlite:///user_database', connect_args={'check_same_thread': False}, echo=False) # echo=False Base = declarative_base() db_session = sessionmaker(bind=engine)() # Table city class City(Base): __tablename__ = 'city' city_id = Column(Integer, primary_key=True) city_name = Column(String) city_climate = Column(String) city_meteo_data = relationship("Meteo", backref="city") # Table meteo class Meteo(Base): __tablename__ = 'meteo' id = Column(Integer, primary_key=True) city_id = Column(ForeignKey('city.city_id')) month = Column(String) average_humidity = Column(Integer) average_temperature = Column(Float)无需手动为代码片段添加导入语句,应用建议的 快速修复 :只需点击灯泡图标(或按 Alt+Enter)。

现在,您已经定义了表格及其关系,请添加以下函数以从数据库中检索数据:
# Retrieving data from the database def get_cities(): return db_session.query(City) # Generating the set of average temperature values for a particular city def get_city_temperature(city): return [month.average_temperature for month in city.city_meteo_data] # Generating the set of average humidity values for a particular city def get_city_humidity(city): return [month.average_humidity for month in city.city_meteo_data] data = get_cities() MONTHS = [record.month for record in data[0].city_meteo_data] CITIES = [city.city_name for city in data]user_database.py 文件的完整代码如下:
- user_database.py
- from sqlalchemy import MetaData, create_engine, Column, Integer, String, ForeignKey, Float from sqlalchemy.orm import declarative_base, sessionmaker, relationship metadata = MetaData() engine = create_engine('sqlite:///user_database', connect_args={'check_same_thread': False}, echo=False) # echo=False Base = declarative_base() db_session = sessionmaker(bind=engine)() # Table city class City(Base): __tablename__ = 'city' city_id = Column(Integer, primary_key=True) city_name = Column(String) city_climate = Column(String) city_meteo_data = relationship("Meteo", backref="city") # Table meteo class Meteo(Base): __tablename__ = 'meteo' id = Column(Integer, primary_key=True) city_id = Column(ForeignKey('city.city_id')) month = Column(String) average_humidity = Column(Integer) average_temperature = Column(Float) # Retrieving data from the database def get_cities(): return db_session.query(City) # Generating the set of average temperature values for a particular city def get_city_temperature(city): return [month.average_temperature for month in city.city_meteo_data] # Generating the set of average humidity values for a particular city def get_city_humidity(city): return [month.average_humidity for month in city.city_meteo_data] data = get_cities() MONTHS = [record.month for record in data[0].city_meteo_data] CITIES = [city.city_name for city in data]
绘制散点图
您已准备好检索数据并绘制第一个图表——每个城市年度平均温度和湿度的散点图。 使用 matplotlib库来设置图表并分配值。
创建另一个 Python 文件, charts.py ,并填入以下代码:
import matplotlib.pyplot as plt from user_database import data, get_city_temperature, get_city_humidity, CITIES yearly_temp = [] yearly_hum = [] for city in data: yearly_temp.append(sum(get_city_temperature(city))/12) yearly_hum.append(sum(get_city_humidity(city))/12) plt.clf() plt.scatter(yearly_hum, yearly_temp, alpha=0.5) plt.title('Yearly Average Temperature/Humidity') plt.xlim(70, 95) plt.ylabel('Yearly Average Temperature') plt.xlabel('Yearly Average Relative Humidity') for i, txt in enumerate(CITIES): plt.annotate(txt, (yearly_hum[i], yearly_temp[i])) plt.show()预览图表的最快方法是右键点击编辑器中的任意位置,然后从上下文菜单中选择 运行 'charts'。 PyCharm 在 图表 工具窗口中渲染散点图。

现在将图表保存为图像,以便您可以将其添加到应用程序的主页中。 将
plt.show()替换为使用savefig(img)方法的片段,并将代码包装到get_main_image()函数中。 您应该会得到以下内容:from io import BytesIO import matplotlib.pyplot as plt from user_database import data, MONTHS, get_city_temperature, get_city_humidity, CITIES def get_main_image(): """Rendering the scatter chart""" yearly_temp = [] yearly_hum = [] for city in data: yearly_temp.append(sum(get_city_temperature(city))/12) yearly_hum.append(sum(get_city_humidity(city))/12) plt.clf() plt.scatter(yearly_hum, yearly_temp, alpha=0.5) plt.title('Yearly Average Temperature/Humidity') plt.xlim(70, 95) plt.ylabel('Yearly Average Temperature') plt.xlabel('Yearly Average Relative Humidity') for i, txt in enumerate(CITIES): plt.annotate(txt, (yearly_hum[i], yearly_temp[i])) img = BytesIO() plt.savefig(img) img.seek(0) return img
创建主页
设置应用程序的主页并为散点图创建视图。
将 app.py 文件中
hello_world函数的定义替换为app.route()装饰器,并使用以下代码:def get_headers(response): response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '0' @app.route('/') def main(): """Entry point; the view for the main page""" cities = [(record.city_id, record.city_name) for record in data] return render_template('main.html', cities=cities) @app.route('/main.png') def main_plot(): """The view for rendering the scatter chart""" img = get_main_image() response = send_file(img, mimetype='image/png') get_headers(response) return response应用建议的快速修复以添加缺失的导入语句。
请注意,PyCharm 会突出显示 main.html ,因为您尚未创建此文件。

使用 PyCharm 的意图操作,您可以快速创建缺失的模板文件。 按 Alt+Enter 并从上下文菜单中选择 创建模板 main.html。 确认模板文件的名称和位置,然后点击 确定。 结果, main.html 将被添加到 templates 目录中。 打开新添加的文件,并将以下代码粘贴到其中:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link href="../static/style.css" rel="stylesheet" type="text/css"> <title>Typical Climate</title> </head> <body> <div id="element1"> <img src="{{ url_for('main_plot') }}" alt="Image"> </div> <div id="element2"> <p>What city has the best climate?</p> <p>When planning your trip or vacation you often check weather data for humidity and temperature.</p> <p>Below is the collection of the yearly average values for the following cities: </p> <ul> {% for city_id, city_name in cities %} <li><a href="">{{ city_name }}</a></li> {% endfor %} </ul> </div> </body> </html>请注意,此片段中的
{{ city_name }}是一个 Jinja2模板变量,用于将city_namePython 变量传递到 HTML 模板中。还需要创建样式表,以设置应用程序中所有 HTML 页面字体和布局的设置。 右键点击 静态 目录并选择 ,然后指定 css 文件的名称, style.css ,并粘贴以下样式定义:
body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } #element1 { display: inline-block; } #element2 { display: inline-block; vertical-align: top; margin-top: 90px; alignment: left; width: 25%; }您可以使用自动创建的
meteoMaster配置随时运行修改后的应用程序以评估结果。点击
运行 或
重新运行 ,并在 运行 工具窗口中跟随 http://127.0.0.1:5000/ 链接。

您应该看到以下页面:

绘制折线图
为了向应用程序用户提供特定城市气候的详细信息,请渲染包含相关信息的折线图。
通过添加
get_city_image函数修改 charts.py 文件:def get_city_image(city_id): """Rendering line charts with city specific data""" city = data.get(city_id) city_temp = get_city_temperature(city) city_hum = get_city_humidity(city) plt.clf() plt.plot(MONTHS, city_temp, color='blue', linewidth=2.5, linestyle='-') plt.ylabel('Mean Daily Temperature', color='blue') plt.yticks(color='blue') plt.twinx() plt.plot(MONTHS, city_hum, color='red', linewidth=2.5, linestyle='-') plt.ylabel('Average Relative Humidity', color='red') plt.yticks(color='red') plt.title(city.city_name) img = BytesIO() plt.savefig(img) img.seek(0) return img此函数绘制了两个线性图:每月的 日均温度和 平均相对湿度 ,分别对应每个城市。 与
get_main_image函数类似,它将图表保存为图像。再创建一个 .html 文件以显示折线图。
右键点击项目根目录中的 templates 目录并选择 ,然后输入 city.html 作为文件名,并将以下代码粘贴到新创建的文件中:
- <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link href="../static/style.css" rel="stylesheet" type="text/css"> <title>{{ city_name }}</title> </head> <body> <div id="element1"> <img src="{{ url_for('city_plot', city_id=city_id) }}" alt="Image"> </div> <div id="element2"> <p>This graph shows mean daily temperature and average relative humidity in {{ city_name }}.</p> <p> {{ city_climate }}</p> <hr/> <p><a href="/">Back to the main page</a></p> </div> </body> </html>
剩下的步骤是使用
Flask.route函数创建另外两个视图。 将以下代码片段添加到 app.py 文件中:@app.route('/city/<int:city_id>') def city(city_id): """Views for the city details""" city_record = data.get(city_id) return render_template('city.html', city_name=city_record.city_name, city_id=city_id, city_climate=city_record.city_climate) @app.route('/city<int:city_id>.png') def city_plot(city_id): """Views for rendering city specific charts""" img = get_city_image(city_id) response = send_file(img, mimetype='image/png') get_headers(response) return response请不要忘记使用 Alt+Enter 快速修复来添加缺失的导入语句。
现在修改 main.html 文件,将城市列表填充为指向相应 city/*页面的链接。 将
<li><a href="">{{ city_name }}</a></li>替换为<li><a href="{{ url_for('city', city_id=city_id) }}">{{ city_name }}</a></li>。
重新运行运行/调试配置以重新启动应用程序,然后点击指向巴黎的链接。 您应该会被导航到 city/2 页面。

创建登录表单
到目前为止,您已经创建了一个功能齐全的应用程序,该应用程序从数据库中检索气象数据并以图表形式呈现。 然而,在实际应用中,您通常需要编辑数据。 合理地说,编辑应仅限于授权用户,因此,让我们创建一个登录表单。
右键点击项目根目录中的 templates 目录并选择 ,然后输入 login.html 作为文件名,并将以下代码粘贴到新创建的文件中:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link rel="stylesheet" type="text/css" href="../static/style.css"> <title>Login form </title> </head> <body> <p>Login to edit the meteo database: </p> <div class="container"> <form action="" class="form-inline" method="post"> <input type="text" class="form-control" placeholder="Username" name="username" value="{{ request.form.username }}"> <input type="password" class="form-control" placeholder="Password" name="password" value="{{ request.form.password }}"> <input class="btn btn-default" type="submit" value="Login"> </form> <p>{{ error }}</p> </div> </body> </html>此代码实现了一个典型的登录表单,包含 用户名 和 密码 字段。
将以下代码添加到 app.py 文件中,以创建登录表单的 Flask 视图并控制登录会话。
@app.route('/login/<int:city_id>', methods=["GET", "POST"]) def login(city_id): """The view for the login page""" city_record = data.get(city_id) try: error = '' if request.method == "POST": attempted_username = request.form['username'] attempted_password = request.form['password'] if attempted_username == 'admin' and attempted_password == os.environ['USER_PASSWORD']: session['logged_in'] = True session['username'] = request.form['username'] return redirect(url_for('edit_database', city_id=city_id)) else: print('invalid credentials') error = 'Invalid credentials. Please, try again.' return render_template('login.html', error=error, city_name=city_record.city_name, city_id=city_id) except Exception as e: return render_template('login.html', error=str(e), city_name=city_record.city_name, city_id=city_id) def login_required(f): @wraps(f) def wrap(*args, **kwargs): """login session""" if 'logged_in' in session: return f(*args, **kwargs) else: pass return redirect(url_for('login')) return wrap app.secret_key = os.environ['FLASK_WEB_APP_KEY']使用 Alt+Enter 快捷方式添加缺失的导入语句。 请注意,此代码片段引入了两个环境变量:
USER_PASSWORD和FLASK_WEB_APP_KEY。您可以在 meteoMaster 运行/调试配置中记录新创建的环境变量的值,因为这是一种比将其硬编码到 app.py 文件中更安全的存储敏感信息的方式。
点击
,在 运行 小部件中选择 编辑配置。 在 运行/调试配置 对话框中,确保选择了 meteoMaster 配置,并点击
图标,在 环境变量 字段中添加这两个变量。

修改
<div id="element2">文件中的 city.html 元素以适应登录功能:<p>This graph shows mean daily temperature and average relative humidity in {{ city_name }}.</p> <p> {{ city_climate }}</p> {% if session['logged_in'] %} <p>Want to add more data?</p> <p>Go and <a href="{{ url_for('edit_database', city_id=city_id) }}">edit</a> the meteo database.</p> {% else %} <p>Want to edit meteo data?</p> <p>Please <a href="{{ url_for('login', city_id=city_id) }}">login</a> first.</p> {% endif %} <hr/> <p><a href="/">Back to the main page</a></p>重新启动应用程序并点击任意城市链接,然后点击“请先登录”句子中的 登录 链接。 您应该会看到登录表单。

目前,此表单尚未启用编辑功能,因为您尚未实现相应的页面。 同时,您可以尝试输入任何错误的密码,以检查是否会显示消息:“无效的凭据。 请再试一次。"
编辑数据
最后剩下的步骤是启用气象数据的编辑功能。
右键点击项目根目录中的 templates 目录并选择 ,然后输入 edit.html 作为文件名,并将以下代码粘贴到新创建的文件中:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link rel="stylesheet" type="text/css" href="../static/style.css"> <title>Edit meteo data for {{ city_name }}</title> </head> <body> <p>Edit the data for {{ city_name }} as appropriate:</p> <div class="container"> <form name="meteoInput" action="" class="form-inline" method="post"> <table> <tr> <td>Month</td> <td colspan="2" align="center">Average Temperature</td> <td colspan="2" align="center">Average Humidity</td> </tr> {% for month in months %} <tr> <td>{{ month }}</td> <td> <input placeholder="20" class="form-control" name="temperature{{ loop.index0 }}" value="{{ meteo[0][loop.index0]}}" type="range" min="-50.0" max="50.0" step="0.01" oninput="temp_output{{ loop.index0 }}.value=this.value" > </td> <td> <output name="temp_output{{ loop.index0 }}">{{ '%0.2f' % meteo[0][loop.index0]|float }}</output> <label> C</label> </td> <td> <input placeholder="20" class="form-control" name="humidity{{ loop.index0 }}" value="{{ meteo[1][loop.index0]}}" type="range" min="0" max="100" oninput="hum_output{{ loop.index0 }}.value=this.value"> </td> <td> <output name="hum_output{{ loop.index0 }}">{{ meteo[1][loop.index0]}}</output> <label> %</label> </td> </tr> {% endfor %} </table> <input class="btn btn-default" type="submit" value="Save"> </form> <p>{{ error }}</p> </div> </body> </html>此片段还使用 Jinja2 模板来处理输入数据,并将其传递到执行数据库提交的 Python 代码中。
向 app.py 文件中再添加一个代码片段,该片段为编辑页面创建 Flask 视图,处理输入数据并更新数据库:
@app.route('/edit/<int:city_id>', methods=["GET", "POST"]) @login_required def edit_database(city_id): """Views for editing city specific data""" month_temperature = [] month_humidity = [] city_record = data.get(city_id) meteo = [get_city_temperature(city_record), get_city_humidity(city_record)] try: if request.method == "POST": # Get data from the form for i in range(12): # In a production application we ought to validate the input data month_temperature.append(float(request.form[f'temperature{i}'])) month_humidity.append(int(request.form[f'humidity{i}'])) # Database update for i, month in enumerate(city_record.city_meteo_data): month.average_temperature = month_temperature[i] month.average_humidity = month_humidity[i] db_session.commit() return redirect(url_for('main', city_id=city_id)) else: return render_template('edit.html', city_name=city_record.city_name, city_id=city_id, months=MONTHS, meteo=meteo) except Exception as error: return render_template('edit.html', city_name=city_record.city_name, city_id=city_id, months=MONTHS, meteo=meteo, error=error)app.py 文件的完整代码如下:
- app.py
- import os from functools import wraps from flask import Flask, send_file, render_template, request, session, redirect, url_for from charts import get_main_image, get_city_image from user_database import data, db_session, get_city_temperature, get_city_humidity, MONTHS app = Flask(__name__) def get_headers(response): response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '0' @app.route('/') def main(): """Entry point; the view for the main page""" cities = [(record.city_id, record.city_name) for record in data] return render_template('main.html', cities=cities) @app.route('/main.png') def main_plot(): """The view for rendering the scatter chart""" img = get_main_image() response = send_file(img, mimetype='image/png') get_headers(response) return response @app.route('/city/<int:city_id>') def city(city_id): """Views for the city details""" city_record = data.get(city_id) return render_template('city.html', city_name=city_record.city_name, city_id=city_id, city_climate=city_record.city_climate) @app.route('/city<int:city_id>.png') def city_plot(city_id): """Views for rendering city specific charts""" img = get_city_image(city_id) response = send_file(img, mimetype='image/png') get_headers(response) return response @app.route('/login/<int:city_id>', methods=["GET", "POST"]) def login(city_id): """The view for the login page""" city_record = data.get(city_id) try: error = '' if request.method == "POST": attempted_username = request.form['username'] attempted_password = request.form['password'] if attempted_username == 'admin' and attempted_password == os.environ['USER_PASSWORD']: session['logged_in'] = True session['username'] = request.form['username'] return redirect(url_for('edit_database', city_id=city_id)) else: print('invalid credentials') error = 'Invalid credentials. Please, try again.' return render_template('login.html', error=error, city_name=city_record.city_name, city_id=city_id) except Exception as e: return render_template('login.html', error=str(e), city_name=city_record.city_name, city_id=city_id) def login_required(f): @wraps(f) def wrap(*args, **kwargs): """login session""" if 'logged_in' in session: return f(*args, **kwargs) else: pass return redirect(url_for('login')) return wrap app.secret_key = os.environ['FLASK_WEB_APP_KEY'] @app.route('/edit/<int:city_id>', methods=["GET", "POST"]) @login_required def edit_database(city_id): """Views for editing city specific data""" month_temperature = [] month_humidity = [] city_record = data.get(city_id) meteo = [get_city_temperature(city_record), get_city_humidity(city_record)] try: if request.method == "POST": # Get data from the form for i in range(12): # In a production application we ought to validate the input data month_temperature.append(float(request.form[f'temperature{i}'])) month_humidity.append(int(request.form[f'humidity{i}'])) # Database update for i, month in enumerate(city_record.city_meteo_data): month.average_temperature = month_temperature[i] month.average_humidity = month_humidity[i] db_session.commit() return redirect(url_for('main', city_id=city_id)) else: return render_template('edit.html', city_name=city_record.city_name, city_id=city_id, months=MONTHS, meteo=meteo) except Exception as error: return render_template('edit.html', city_name=city_record.city_name, city_id=city_id, months=MONTHS, meteo=meteo, error=error) if __name__ == '__main__': app.run()
重新启动运行配置或保存 app.py 文件(Ctrl+S )以触发自动重新启动。 现在,您可以在应用程序的主页上选择例如
Paris,点击 编辑 ,输入管理员的凭据,您将进入可以编辑气象数据的页面。
通过此步骤,您已完成创建与数据库交互的基于 Flask 的应用程序的任务。 现在,您完全掌控了天气。 去修改任意城市的气象数据,以便这些更改在图表上显而易见。 然后预览更改。