PyCharm 2025.2 Help

使用 Flask 创建 Web 应用程序

本教程解释了如何使用 Flask Web 框架开发基于 Web 的应用程序。 我们的 MeteoMaster 应用程序处理存储在数据库中的气象数据,并以以下图表的形式呈现:

  • 散点图——布拉格、圣彼得堡的年度平均温度和湿度的累积报告。 圣彼得堡、旧金山、巴黎和新加坡。

  • 折线图——每个城市的月平均温度和湿度。

“Staying Above the Weather” 应用程序概述

您将使用各种 Web 技术来实现以下应用程序功能:

函数

技术

气象数据操作

SQLite 数据库用于存储数据, SQLAlchemy 包用于在 Python 代码中对数据库执行操作。

图形表示

matplotlib 包用于绘制图表。

查看和编辑内容

HTML 用于创建页面视图, Jinja 用于创建智能模板。

管理内容

Flask 用于协调应用程序内容。

MeteoMaster 是一个包含以下组件的 Web 应用程序:

Flask Web 应用程序的关键模块
主页

应用程序入口点,用于渲染散点图并提供指向特定城市气候详细摘要的链接。

城市

一系列包含每个城市气候详细信息的页面。

登录

身份验证页面。 需要输入有效的凭据才能编辑气象数据。

编辑

一系列用于编辑特定城市数据的页面。

每个 HTML 页面都有一个对应的 Flask 视图,由 Python 代码实现。

在 PyCharm 中创建 Flask 应用程序

按照 创建 Flask 项目 中描述的步骤创建一个基本的 Flask 项目,以开始应用程序的原型设计。

  1. 新项目 对话框中选择 Flask

    创建一个新的 Flask 项目
  2. 位置 字段中,提供项目位置的路径,并输入 meteoMaster作为项目名称。 保留其余设置为默认值,然后点击 创建

  3. PyCharm 为您创建了新的虚拟环境,并选择使用 Jinja2 作为模板语言。 结果,您将获得预定义的项目结构和基本的 "Hello World!" 代码。

    默认的 Flask 应用程序

    点击顶部 运行 小部件中的 运行运行 'meteoMaster' 以启动自动创建的运行配置。

  4. 运行 工具窗口中,点击超链接,预览基本 "Hello, world" 应用程序的页面。

    预览 Hello World 应用程序
  5. 现在安装 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,这非常简单。

  1. 从以下位置下载包含五个城市气象数据的预定义数据库:

    https://github.com/allaredko/flask-tutorial-demo/blob/master/user_database

    user_database 文件保存到项目根目录。

  2. 双击添加的文件。 在打开的 数据源和驱动程序 对话框中,点击 测试连接 以确保数据源已正确配置。 如果您看到 不完整的配置 警告,请点击 下载驱动程序文件

    添加数据源
  3. 点击 确定 以完成数据源的创建, 数据库工具窗口 将打开。

    您应该会看到以下表格: citymeteo。 双击每个表格以预览数据。 表格 city 有三列: city_idcity_namecity_climate (城市气候的简要文字描述)。 表格 meteo 有四列: city_idmonthaverage_humidityaverage_temperature。 为 city_id 列的 table 定义了一个 外键 ,以建立两个表之间的关系。

    气象数据库
  4. 创建一个 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)。

    应用快速修复以解决缺失的导入语句
  5. 现在,您已经定义了表格及其关系,请添加以下函数以从数据库中检索数据:

    # 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]
  6. 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库来设置图表并分配值。

  1. 创建另一个 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()
  2. 预览图表的最快方法是右键点击编辑器中的任意位置,然后从上下文菜单中选择 运行 'charts'。 PyCharm 在 图表 工具窗口中渲染散点图。

    预览散点图
  3. 现在将图表保存为图像,以便您可以将其添加到应用程序的主页中。 将 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

创建主页

设置应用程序的主页并为散点图创建视图。

  1. 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
  2. 应用建议的快速修复以添加缺失的导入语句。

  3. 请注意,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_name Python 变量传递到 HTML 模板中。

  4. 还需要创建样式表,以设置应用程序中所有 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%; }
  5. 您可以使用自动创建的 meteoMaster 配置随时运行修改后的应用程序以评估结果。

    点击 运行运行重新运行重新运行 ,并在 运行 工具窗口中跟随 http://127.0.0.1:5000/ 链接。

    运行/调试配置

    您应该看到以下页面:

    MeteoMaster 应用程序的主页

绘制折线图

为了向应用程序用户提供特定城市气候的详细信息,请渲染包含相关信息的折线图。

  1. 通过添加 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 函数类似,它将图表保存为图像。

  2. 再创建一个 .html 文件以显示折线图。

    右键点击项目根目录中的 templates 目录并选择 新建 | HTML 文件 ,然后输入 city.html 作为文件名,并将以下代码粘贴到新创建的文件中:

  3. <!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>
  4. 剩下的步骤是使用 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 快速修复来添加缺失的导入语句。

  5. 现在修改 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 页面。

巴黎的详细天气信息

创建登录表单

到目前为止,您已经创建了一个功能齐全的应用程序,该应用程序从数据库中检索气象数据并以图表形式呈现。 然而,在实际应用中,您通常需要编辑数据。 合理地说,编辑应仅限于授权用户,因此,让我们创建一个登录表单。

  1. 右键点击项目根目录中的 templates 目录并选择 新建 | HTML 文件 ,然后输入 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>

    此代码实现了一个典型的登录表单,包含 用户名密码 字段。

  2. 将以下代码添加到 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_PASSWORDFLASK_WEB_APP_KEY

  3. 您可以在 meteoMaster 运行/调试配置中记录新创建的环境变量的值,因为这是一种比将其硬编码到 app.py 文件中更安全的存储敏感信息的方式。

    点击 ,在 运行 小部件中选择 编辑配置。 在 运行/调试配置 对话框中,确保选择了 meteoMaster 配置,并点击 编辑环境变量 图标,在 环境变量 字段中添加这两个变量。

    添加环境变量
  4. 修改 <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>
  5. 重新启动应用程序并点击任意城市链接,然后点击“请先登录”句子中的 登录 链接。 您应该会看到登录表单。

    登录表单

    目前,此表单尚未启用编辑功能,因为您尚未实现相应的页面。 同时,您可以尝试输入任何错误的密码,以检查是否会显示消息:“无效的凭据。 请再试一次。"

编辑数据

最后剩下的步骤是启用气象数据的编辑功能。

  1. 右键点击项目根目录中的 templates 目录并选择 新建 | HTML 文件 ,然后输入 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 代码中。

  2. 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()
  3. 重新启动运行配置或保存 app.py 文件(Ctrl+S )以触发自动重新启动。 现在,您可以在应用程序的主页上选择例如 Paris ,点击 编辑 ,输入管理员的凭据,您将进入可以编辑气象数据的页面。

    编辑气象参数

通过此步骤,您已完成创建与数据库交互的基于 Flask 的应用程序的任务。 现在,您完全掌控了天气。 去修改任意城市的气象数据,以便这些更改在图表上显而易见。 然后预览更改。

最后修改日期: 2025年 9月 26日