PyCharm 2026.2 Help

使用 Flask 创建 Web 应用程序

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

  • 散点图 — 年均气温和湿度的汇总报告,涵盖布拉格、圣 彼得堡、旧金山、巴黎和新加坡。

  • 折线图 — 各城市的月均气温和湿度。

'凌驾于天气之上' 应用概述

您将使用多种 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

    点击 安装 requirements 链接以继续安装这些软件包。

    安装应用所需的软件包

设置数据库

现在为您的应用设置数据源。 使用 PyCharm,这非常简单。

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

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

    将 user_database 文件保存在项目根目录中。

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

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

    您应当看到以下表: city 和 meteo。 双击每个表以预览数据。 city 表包含三列: city_id、 city_name 和 city_climate (城市气候的简要文字描述)。 meteo 表包含四列: city_id、 month、 average_humidity 和 average_temperature。 在 table 的 city_id 列上定义了一个 外键 ,用于在两个表之间建立关系。

    Meteo 数据库
  4. 创建一个 Python 文件, user_database.py ,以使用新创建的数据库。 使用 SQLAlchemy 声明式基类 语法来描述数据库。

    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)

    无需手动为该代码片段添加 import 语句,应用建议的 快速修复 :只需点击灯泡图标(或按 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 将添加到 模板 目录中。 打开新添加的文件,并将以下代码粘贴到其中:

    <!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 页面设置字体和布局。 右键点击 static 目录并选择 新建 | 样式表 ,然后将 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 文件以显示折线图。

    在项目根目录中右键点击 模板 目录并选择 新建 | 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. 在项目根目录中右键点击 模板 目录并选择 新建 | 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_PASSWORD 和 FLASK_WEB_APP_KEY。

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

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

    添加环境变量
  4. 在 city.html 文件中修改 <div id="element2"> 元素,以支持登录功能:

    <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. 重新启动应用,并点击任意城市链接,然后点击 "请先登录" 句子中的 login 链接。 您应当看到登录表单。

    登录表单

    暂时,此表单尚不支持编辑,因为您尚未实现相应的页面。 与此同时,您可以尝试输入任意错误的密码,以检查是否会显示消息: "凭据无效。 请重试。"

编辑数据

最后一步是启用对气象数据的编辑。

  1. 在项目根目录中右键点击 模板 目录并选择 新建 | 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>

    此片段也使用了 Jinjia2 模板来处理输入数据,并将其传递给执行提交到数据库的 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 的、与数据库交互的应用程序的任务。 现在,您已经完全掌控了天气。 请修改任一城市的气象数据,以便这些更改在图表上更加显著。 然后预览这些更改。

2026年 7月 14日