news 2026/9/3 21:53:34

用Flask和SQLite从零搭建房源信息展示与发布系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Flask和SQLite从零搭建房源信息展示与发布系统

同样一套房源信息,扔到业主群、朋友圈是纯文字,过几天就刷没了;整理成一个网页或者小系统,不仅自己好维护,别人看着也清楚。今天就用 Flask 加 SQLite,带大家从零搭一个房源信息展示与发布小系统,把房型、床型、租期、联系方式这些信息做成可浏览、可维护的页面。这套方案不依赖复杂前端框架,纯 Python 后端起一个本地服务,后续发布到服务器也容易,适合做房产信息管理、二房东管理台账、个人房源展示这类场景。

文章会覆盖从环境准备、数据库设计、Flask 路由编写、页面模板渲染,到常见报错排查和上线注意点。代码都能直接复制运行,你只需要一个能跑 Python 的电脑。

1. 背景:从一条租房信息到一个小系统

平时我们在群里见到最多的房源信息大概是这样的:

碧桂园山湖城观澜1街1座2903,3室1厅1厨1卫2阳台,3床:1.8m+1.5m+1.2m,可日租/周租/月租,欢迎各位邻居咨询。

这种文本信息的问题在于:

  • 信息结构不统一,房型、床型、租期混在一起;
  • 无法筛选,租客想找“带阳台月租房”得一条条翻;
  • 修改价格或者添加房间信息时,只能复制原文再改,容易错;
  • 发出去以后很快被其他消息顶掉,想回看历史记录很麻烦。

如果把这套房源信息做成一个页面,效果就完全不一样了。租客打开页面,能看到结构化的房型信息、床型尺寸、租期选项、联系方式;房东自己维护数据时,只需要在后台输入表单,就能新增、修改、下架房源。

本文会实现一个最小可用的“房源信息展示系统”,技术栈只有三个:

  • Flask:Python 轻量级 Web 框架,用来处理请求和页面跳转;
  • SQLite:Python 自带的关系型数据库,不需要单独安装服务;
  • Jinja2 模板:Flask 默认集成的模板引擎,用来渲染 HTML 页面。

整个项目可以跑在本地 Windows、macOS 或 Linux 上,也可以部署到云服务器。

2. 环境准备与项目初始化

2.1 检查 Python 环境

Flask 是基于 Python 的 Web 框架,先确认机器上已经安装了 Python 3。这里用命令行检查:

python --version

如果你使用的是 macOS 或 Linux,可能需要用python3

python3 --version

只要看到类似Python 3.8.xPython 3.10.xPython 3.11.x这样的输出,就说明环境没问题。本文示例以 Python 3 环境为例,重点演示配置思路,具体小版本不影响运行。

2.2 创建项目目录

建议单独建一个项目文件夹,避免和系统环境混在一起。例如在桌面或工作目录下新建:

mkdir house_info_system cd house_info_system

后续所有代码文件都放在这个目录下。

2.3 创建虚拟环境

虚拟环境可以让 Flask 及其依赖只作用于当前项目,不会污染系统级 Python。这一步在生产环境、多人协作场景下尤其重要。

Windows 下创建并激活虚拟环境:

python -m venv venv venv\Scripts\activate

macOS 或 Linux 下:

python3 -m venv venv source venv/bin/activate

激活后,命令行提示符前面会出现(venv),表示当前正在使用虚拟环境。

2.4 安装 Flask

在虚拟环境中执行:

pip install flask

安装完成后,可以通过下面命令确认版本:

pip show flask

不同时期安装到的 Flask 版本会有差异,但本文用到的功能在 Flask 2.x 和 3.x 中都保持一致。如果你安装的是较老版本,建议升级到当前稳定版本再运行示例:

pip install --upgrade flask

2.5 项目结构规划

为了让代码清晰,这里采用一个非常小的分层方式:

house_info_system/ ├── app.py ├── init_db.py ├── templates/ │ ├── index.html │ ├── detail.html │ └── add_house.html ├── static/ │ └── style.css └── instance/ └── house.db

说明:

  • app.py:Flask 主程序,包含路由和页面逻辑;
  • init_db.py:初始化数据库脚本,建表并写入示例数据;
  • templates/:Jinja2 模板文件;
  • static/:静态文件,如 CSS 样式;
  • instance/:SQLite 数据库文件所在目录,Flask 默认会创建这个目录。

3. 核心知识拆解

在写完整代码之前,先花五分钟理解三个知识点,后面看代码会轻松很多。

3.1 Flask 最小应用长什么样

Flask 的核心概念是“路由”,也就是把 URL 地址映射到对应的 Python 函数上。

先看一个最简单的例子:

from flask import Flask app = Flask(__name__) @app.route("/") def index(): return "Hello, House System!" if __name__ == "__main__": app.run(debug=True)

这里有几个关键点:

  • Flask(__name__)创建应用实例,__name__告诉 Flask 当前模块在哪,从而定位模板和静态文件;
  • @app.route("/")是路由装饰器,表示访问根路径/时执行下面的index()函数;
  • app.run(debug=True)启动开发服务器,debug=True表示修改代码后自动重启,并显示详细错误页面。

开发环境用debug=True很方便,但是生产环境必须关闭,否则会暴露敏感错误信息。

3.2 SQLite 数据库表结构设计

房源信息需要字段来承载结构化的数据。参照前面那条示例房源,可以设计如下表:

字段名类型说明
idINTEGER PRIMARY KEY AUTOINCREMENT主键,自动增长
titleTEXT房源标题
addressTEXT详细地址
house_typeTEXT房型,比如 3室1厅1厨1卫2阳台
bed_infoTEXT床型信息,比如 1.8m+1.5m+1.2m
price_dayREAL日租价格
price_weekREAL周租价格
price_monthREAL月租价格
has_balconyINTEGER是否有阳台,1 表示有,0 表示没有
remarkTEXT备注或其他说明
statusINTEGER状态,1 表示可租,0 表示已下架
create_timeTEXT创建时间

关于价格字段,这里用REAL类型存储浮点数,适合租金这种带小数的金额。如果对金额精度要求很高,也可以改成INTEGER存储“分”,在展示时再换算成“元”。实际项目里,建议用整数分存储,避免浮点误差。

3.3 Jinja2 模板渲染

Flask 默认从templates/目录加载 Jinja2 模板。模板文件里可以写 HTML,也可以通过{{ 变量 }}插入 Python 传过来的数据,通过{% for item in list %}做循环。

例如:

<ul> {% for house in houses %} <li>{{ house.title }} - {{ house.address }}</li> {% endfor %} </ul>

这个能力让页面能动态展示数据库中的每一条房源,而不是写死静态文本。

4. 完整实战:房源信息展示系统

下面我们逐步实现整个项目。先从数据库开始,再写 Flask 主程序,最后写页面模板。

4.1 初始化数据库脚本

先创建init_db.py,用来建表并插入一条示例数据。

文件路径:house_info_system/init_db.py

import sqlite3 import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.path.join(BASE_DIR, "instance", "house.db") def init_db(): if not os.path.exists(os.path.join(BASE_DIR, "instance")): os.makedirs(os.path.join(BASE_DIR, "instance")) conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS house ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, address TEXT NOT NULL, house_type TEXT NOT NULL, bed_info TEXT NOT NULL, price_day REAL, price_week REAL, price_month REAL, has_balcony INTEGER DEFAULT 0, remark TEXT, status INTEGER DEFAULT 1, create_time TEXT DEFAULT CURRENT_TIMESTAMP ) """) cursor.execute("SELECT COUNT(*) FROM house") count = cursor.fetchone()[0] if count == 0: cursor.execute(""" INSERT INTO house ( title, address, house_type, bed_info, price_day, price_week, price_month, has_balcony, remark, status ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( "碧桂园山湖城观澜1街1座2903", "碧桂园山湖城观澜1街1座2903", "3室1厅1厨1卫2阳台", "1.8m + 1.5m + 1.2m", 268.0, 1580.0, 5280.0, 1, "可日租/周租/月租,欢迎各位邻居咨询;小区环境安静,交通便利。", 1, )) conn.commit() conn.close() print("数据库初始化完成,数据文件位于:", DB_PATH) if __name__ == "__main__": init_db()

几点说明:

  • CREATE TABLE IF NOT EXISTS保证重复执行脚本不会重复建表报错;
  • SELECT COUNT(*) FROM house用来判断表里是否已有数据,避免重复插入示例数据;
  • 示例数据里地址信息直接复用了标题中的地址,实际项目中也可以把title写成“山湖城三房两阳台整套出租”这种更具吸引力的文案;
  • 价格字段这里只是示例,具体定价请在真实业务中自行决定。

执行初始化脚本:

python init_db.py

运行成功后,会在instance目录下生成house.db数据库文件。

4.2 编写 Flask 主程序

接下来创建app.py。这个文件负责创建 Flask 应用、连接数据库、提供页面路由。

文件路径:house_info_system/app.py

import os import sqlite3 from datetime import datetime from flask import Flask, render_template, request, redirect, url_for, abort BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.path.join(BASE_DIR, "instance", "house.db") app = Flask(__name__) def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn @app.route("/") def index(): keyword = request.args.get("keyword", "") status = request.args.get("status", "1") conn = get_db() sql = "SELECT * FROM house WHERE 1=1" params = [] if keyword: sql += " AND (title LIKE ? OR address LIKE ? OR remark LIKE ?)" like_word = f"%{keyword}%" params.extend([like_word, like_word, like_word]) if status in ("0", "1"): sql += " AND status = ?" params.append(status) sql += " ORDER BY id DESC" houses = conn.execute(sql, params).fetchall() conn.close() return render_template("index.html", houses=houses, keyword=keyword, status=status) @app.route("/house/<int:house_id>") def detail(house_id): conn = get_db() house = conn.execute("SELECT * FROM house WHERE id = ?", (house_id,)).fetchone() conn.close() if house is None: abort(404) return render_template("detail.html", house=house) @app.route("/add", methods=["GET", "POST"]) def add_house(): if request.method == "POST": title = request.form.get("title", "").strip() address = request.form.get("address", "").strip() house_type = request.form.get("house_type", "").strip() bed_info = request.form.get("bed_info", "").strip() price_day = request.form.get("price_day", "0") price_week = request.form.get("price_week", "0") price_month = request.form.get("price_month", "0") has_balcony = 1 if request.form.get("has_balcony") else 0 remark = request.form.get("remark", "").strip() status = 1 if request.form.get("status") else 1 if not title or not address: return "标题和地址不能为空", 400 conn = get_db() conn.execute(""" INSERT INTO house ( title, address, house_type, bed_info, price_day, price_week, price_month, has_balcony, remark, status ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, (title, address, house_type, bed_info, float(price_day or 0), float(price_week or 0), float(price_month or 0), has_balcony, remark, status)) conn.commit() conn.close() return redirect(url_for("index")) return render_template("add_house.html") @app.errorhandler(404) def page_not_found(e): return "页面不存在或房源已下架", 404 if __name__ == "__main__": app.run(debug=True)

这段代码里最核心的是几个路由:

  • /:房源列表页,支持关键词搜索和状态筛选;
  • /house/<int:house_id>:房源详情页;
  • /add:新增房源表单页面,GET 请求显示表单,POST 请求保存数据。

值得注意的地方:

  • sqlite3.Row可以让查询结果像字典一样通过字段名访问,在模板里写house.titlehouse[0]可读性好很多;
  • SQL 查询使用?占位符传参,不用字符串拼接,能有效避免 SQL 注入风险;
  • 新增房源时,如果标题或地址为空,直接返回 400 错误。这里只是示例,真实项目中更推荐使用 Flask-WTF 表单校验组件,把错误信息显示在页面上。

4.3 编写列表页模板

templates目录下创建index.html

文件路径:house_info_system/templates/index.html

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>房源信息展示系统</title> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> </head> <body> <div class="container"> <header class="header"> <h1>房源信息展示系统</h1> <a class="btn btn-primary" href="{{ url_for('add_house') }}">新增房源</a> </header> <form class="search-bar" method="get" action="{{ url_for('index') }}"> <input type="text" name="keyword" value="{{ keyword }}" placeholder="输入小区名、地址或备注关键词"> <select name="status"> <option value="1" {% if status == '1' %}selected{% endif %}>在租房源</option> <option value="0" {% if status == '0' %}selected{% endif %}>已下架</option> </select> <button type="submit">搜索</button> </form> <div class="house-list"> {% for house in houses %} <div class="house-card"> <h2> <a href="{{ url_for('detail', house_id=house.id) }}">{{ house.title }}</a> </h2> <p class="address">地址:{{ house.address }}</p> <p class="house-type">房型:{{ house.house_type }}</p> <p class="bed-info">床型:{{ house.bed_info }}</p> <p class="price"> <span>日租:{{ house.price_day }}元</span> <span>周租:{{ house.price_week }}元</span> <span>月租:{{ house.price_month }}元</span> </p> <p class="remark">{{ house.remark }}</p> {% if house.has_balcony == 1 %} <span class="tag">阳台</span> {% endif %} </div> {% else %} <p class="empty">暂无可展示房源,请先新增房源。</p> {% endfor %} </div> </div> </body> </html>

模板里用到了 Jinja2 的关键语法:

  • {{ house.title }}输出字段值;
  • {% for house in houses %}循环渲染列表;
  • {% else %}配合for使用,当列表为空时显示提示;
  • url_for('static', filename='style.css')生成静态文件地址;
  • url_for('detail', house_id=house.id)生成详情页链接。

4.4 编写详情页模板

创建detail.html,用来展示单套房源的完整信息。

文件路径:house_info_system/templates/detail.html

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{ house.title }}</title> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> </head> <body> <div class="container"> <a class="back-link" href="{{ url_for('index') }}">&larr; 返回列表</a> <h1>{{ house.title }}</h1> <div class="detail-info"> <p><strong>详细地址:</strong>{{ house.address }}</p> <p><strong>房型:</strong>{{ house.house_type }}</p> <p><strong>床型:</strong>{{ house.bed_info }}</p> <p><strong>日租价格:</strong>{{ house.price_day }} 元/天</p> <p><strong>周租价格:</strong>{{ house.price_week }} 元/周</p> <p><strong>月租价格:</strong>{{ house.price_month }} 元/月</p> <p><strong>阳台:</strong>{% if house.has_balcony == 1 %}有{% else %}无{% endif %}</p> <p><strong>房源状态:</strong>{% if house.status == 1 %}可租{% else %}已下架{% endif %}</p> <p><strong>备注:</strong>{{ house.remark }}</p> </div> </div> </body> </html>

详情页主要用来完整展示字段,方便租客快速了解房源。如果后续要做“联系房东”功能,可以在这里增加一个按钮,展示微信号或电话,也可以直接接第三方在线咨询链接。

4.5 编写新增房源表单页

创建add_house.html,这个页面用来提交新房源信息。

文件路径:house_info_system/templates/add_house.html

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>新增房源</title> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> </head> <body> <div class="container"> <a class="back-link" href="{{ url_for('index') }}">&larr; 返回列表</a> <h1>新增房源</h1> <form class="house-form" method="post" action="{{ url_for('add_house') }}"> <label>标题</label> <input type="text" name="title" placeholder="例如:碧桂园山湖城观澜1街1座2903" required> <label>详细地址</label> <input type="text" name="address" placeholder="例如:碧桂园山湖城观澜1街1座2903" required> <label>房型</label> <input type="text" name="house_type" placeholder="例如:3室1厅1厨1卫2阳台"> <label>床型</label> <input type="text" name="bed_info" placeholder="例如:1.8m + 1.5m + 1.2m"> <label>日租价格(元)</label> <input type="number" step="0.01" name="price_day" value="0"> <label>周租价格(元)</label> <input type="number" step="0.01" name="price_week" value="0"> <label>月租价格(元)</label> <input type="number" step="0.01" name="price_month" value="0"> <label> <input type="checkbox" name="has_balcony" value="1"> 有阳台 </label> <label>备注</label> <textarea name="remark" rows="4" placeholder="可以填写租期要求、配套家电、联系方式等"></textarea> <button type="submit" class="btn btn-primary">保存房源</button> </form> </div> </body> </html>

需要注意的是,HTML 表单里的name属性必须和 Flask 代码中request.form.get("title")的参数名一致,否则提交后数据会丢失。这个问题在真实项目里经常出现,一旦发现保存后字段为空,优先检查name是否匹配。

4.6 编写静态样式

为了让页面不至于太简陋,补一份简单的 CSS。

文件路径:house_info_system/static/style.css

* { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif; background-color: #f5f7fa; color: #333; line-height: 1.6; } .container { max-width: 900px; margin: 40px auto; padding: 20px; } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .header h1 { font-size: 24px; } .btn { display: inline-block; padding: 8px 16px; border: none; border-radius: 6px; text-decoration: none; cursor: pointer; font-size: 14px; } .btn-primary { background-color: #1a73e8; color: #fff; } .search-bar { display: flex; gap: 10px; margin-bottom: 20px; } .search-bar input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 6px; } .search-bar select, .search-bar button { padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; background: #fff; cursor: pointer; } .house-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; } .house-card { background: #fff; border-radius: 10px; padding: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); } .house-card h2 { font-size: 18px; margin-bottom: 8px; } .house-card h2 a { color: #1a73e8; text-decoration: none; } .house-card .address { color: #888; font-size: 13px; } .house-card .price span { display: inline-block; margin-right: 8px; color: #e67e22; font-weight: 600; } .tag { display: inline-block; margin-top: 10px; padding: 2px 8px; background: #e8f0fe; color: #1a73e8; border-radius: 4px; font-size: 12px; } .empty { grid-column: 1 / -1; text-align: center; color: #999; padding: 40px 0; } .back-link { display: inline-block; margin-bottom: 16px; color: #1a73e8; text-decoration: none; } .detail-info p { padding: 8px 0; border-bottom: 1px solid #f0f0f0; } .house-form label { display: block; margin-top: 12px; font-weight: 600; } .house-form input, .house-form textarea, .house-form select { width: 100%; padding: 10px; margin-top: 6px; border: 1px solid #ddd; border-radius: 6px; } .house-form button { margin-top: 20px; }

这份样式是演示级别的,够用但不花哨。如果你想放到正式对外环境,可以基于 Bootstrap、Tailwind CSS 或者 Element Plus 做一版更成熟的 UI,核心业务逻辑不需要改变。

4.7 运行与验证

确认所有文件都创建好后,在项目根目录启动 Flask 开发服务器:

python app.py

启动后,终端会显示类似下面的日志:

* Running on all addresses (0.0.0.0) * Running on http://127.0.0.1:5000 * Running on http://192.168.1.101:5000

打开浏览器访问http://127.0.0.1:5000,就能看到示例房源在列表中展示。

点击房源标题进入详情页,可以看到该房源的完整信息。再回到首页,点击“新增房源”按钮,填写另一套房源信息并提交,新数据会写入 SQLite 数据库,随后自动跳回列表页并显示新增记录。

到这里,一个最基础版本的房源信息展示系统就跑通了。

5. 功能扩展:筛选、搜索与上下架管理

基础版满足信息展示需求,但离“好用”还有一段距离。下面给出几个低成本、高收益的扩展点。

5.1 上下架管理

现在status字段已经能区分可租与下架状态,但还没实现“一键下架”的入口。可以增加一个/house/<int:house_id>/toggle路由:

@app.route("/house/<int:house_id>/toggle", methods=["POST"]) def toggle_house(house_id): conn = get_db() house = conn.execute("SELECT * FROM house WHERE id = ?", (house_id,)).fetchone() if house is None: conn.close() abort(404) new_status = 0 if house["status"] == 1 else 1 conn.execute("UPDATE house SET status = ? WHERE id = ?", (new_status, house_id)) conn.commit() conn.close() return redirect(url_for("detail", house_id=house_id))

然后在详情页模板里增加一个按钮,通过表单 POST 提交:

<form method="post" action="{{ url_for('toggle_house', house_id=house.id) }}"> <button type="submit">{% if house.status == 1 %}下架{% else %}重新上架{% endif %}</button> </form>

这里有两个细节值得注意:

  • 为什么不用GET请求做上下架?因为GET链接会被浏览器预加载、被搜索爬虫抓取,误触概率很高。修改状态这种操作应该使用POST,这是 Web 开发里的一条通用规范。
  • 修改数据前先SELECT一次,是为了判断记录是否存在。如果记录不存在,直接执行UPDATE也不会报错,但页面上的跳转逻辑会变得不明确。

5.2 按价格区间筛选

在列表页加入最低价和最高价输入框,可以按日租价或月租价过滤房源。这里演示按月租价格筛选:

@app.route("/") def index(): keyword = request.args.get("keyword", "") status = request.args.get("status", "1") min_price = request.args.get("min_price", "") max_price = request.args.get("max_price", "") conn = get_db() sql = "SELECT * FROM house WHERE 1=1" params = [] if keyword: sql += " AND (title LIKE ? OR address LIKE ? OR remark LIKE ?)" like_word = f"%{keyword}%" params.extend([like_word, like_word, like_word]) if status in ("0", "1"): sql += " AND status = ?" params.append(status) if min_price: sql += " AND price_month >= ?" params.append(float(min_price)) if max_price: sql += " AND price_month <= ?" params.append(float(max_price)) sql += " ORDER BY id DESC" houses = conn.execute(sql, params).fetchall() conn.close() return render_template( "index.html", houses=houses, keyword=keyword, status=status, min_price=min_price, max_price=max_price )

搜索页模板只要在原有表单中增加两个 number 输入框即可。这样,租客可以快速锁定“月租 3000-6000 元”区间的房源,比翻聊天记录高效得多。

5.3 增加图片上传

房源信息没有图片,说服力会弱很多。SQLite 本身可以存 BLOB 二进制数据,但更通用的做法是:图片上传到服务器静态目录,数据库只保存图片文件路径。

在 Flask 中处理图片上传需要配置上传目录和允许的扩展名。这里给出一个最简实现思路:

import os from werkzeug.utils import secure_filename UPLOAD_FOLDER = os.path.join(BASE_DIR, "static", "uploads") ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp"} app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER def allowed_file(filename): return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS

表单提交时判断文件扩展名是否合法,然后调用secure_filename清洗文件名,保存到static/uploads目录,最后把文件名写入数据库中的image_path字段。展示时通过url_for('static', filename='uploads/' + house.image_path)生成图片地址。

图片上传项目中,一定要限制扩展名和文件大小,否则很容易被塞入恶意文件。生产环境还需要考虑把图片存到云存储,例如阿里云 OSS、腾讯云 COS 等,避免应用服务器磁盘被撑满。

5.4 导出房源信息为 Excel

业主或者运营同学经常需要把房源台账整理成 Excel 发给别人,可以用 Python 的pandasopenpyxl实现导出功能。思路是查询数据库后生成 DataFrame,再to_excel导出:

import pandas as pd def export_houses(): conn = get_db() rows = conn.execute("SELECT * FROM house WHERE status = 1").fetchall() conn.close() data = [dict(row) for row in rows] df = pd.DataFrame(data) df.to_excel("houses.xlsx", index=False)

这只是一个示例思路,实际导出时需要处理临时文件、响应下载、编码等问题。由于涉及导出文件格式和版本差异,这里不展开写死,等真正需要这个功能时,建议先在一份测试数据上验证,再接入正式环境。

6. 常见问题与排查思路

运行 Flask 项目时,新手最容易遇到下面几类问题。这里整理成一个排查表:

问题现象常见原因解决思路
启动时报ModuleNotFoundError: No module named 'flask'当前环境没有安装 Flask,或没有激活虚拟环境执行pip install flask,并确认命令行前缀有(venv)
启动后访问http://127.0.0.1:5000提示 404路由写错,或 Flask 应用启动的是其他目录的代码检查@app.route路径与浏览器访问路径是否一致
页面中文出现乱码HTML 文件没有设置 UTF-8,或模板编码不对<head>中加<meta charset="UTF-8">,并确认代码文件保存为 UTF-8 编码
点击“新增房源”提交后页面报 400表单必填字段为空,或字段name与后端不匹配检查request.form.get()参数名与 HTML 表单name属性
数据库操作报sqlite3.OperationalError: no such table没有先运行init_db.py,或数据库路径不一致执行python init_db.py,确认DB_PATH指向同一数据库文件
address already in use或端口被占用5000 端口已被其他程序占用换端口启动:python app.py前设置app.run(port=5001),或用lsof -i:5000查看占用进程
修改了app.py但页面没有变化没有开启 debug 模式,或浏览器缓存重启 Flask 服务,并刷新浏览器;开发时建议开启debug=True
页面样式完全丢失静态资源路径错误,或者static目录结构不对检查模板中url_for('static', filename='style.css'),确认存在static/style.css文件

这里重点说两个新手容易踩的坑。

第一个是数据库路径问题。sqlite3.connect(DB_PATH)使用相对路径时,会根据“当前工作目录”查找数据库文件。如果你在项目根目录启动python init_db.py后,又在其他目录启动python app.py,就可能导致两个进程操作不同的数据库文件。解决方法就是像我上面代码里一样,基于os.path.dirname(os.path.abspath(__file__))计算出绝对路径,保证无论从哪里启动,都能找到同一个数据库。

第二个是表单 POST 返回 400 或 500 的排查顺序。建议按“字段名是否匹配 → 数据库字段是否存在 → SQL 语法是否正确 → 是否缺少必填校验”的顺序排查。先看页面提交的数据,再看后端日志,日志里通常会给出具体报错行数。

7. 工程建议与部署注意

代码能跑通只是第一步,真正要应用到实际业务中,下面这些工程建议值得提前考虑。

7.1 配置分离

现在app.py里直接写死了数据库路径,只要项目变大,这种写法就会很痛苦。建议把配置抽到独立的配置文件中,用环境变量覆盖默认值:

import os class Config: BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.environ.get("HOUSE_DB_PATH", os.path.join(BASE_DIR, "instance", "house.db")) SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-key-change-me") DEBUG = os.environ.get("FLASK_DEBUG", "0") == "1" UPLOAD_FOLDER = os.path.join(BASE_DIR, "static", "uploads") MAX_CONTENT_LENGTH = 5 * 1024 * 1024

这样在本地开发、测试服务器、生产服务器三套环境切换时,只需要修改环境变量,不用改代码。

7.2 数据库备份

SQLite 是文件型数据库,备份最简单的方式就是复制数据库文件。但是注意,直接复制正在写入的 SQLite 文件可能产生不一致的备份。推荐用 SQLite 的在线备份命令:

sqlite3 instance/house.db ".backup 'backup_house.db'"

在 Python 脚本里也可以调用sqlite3.Connection.backup方法:

import sqlite3 src = sqlite3.connect("instance/house.db") dst = sqlite3.connect("backup_house.db") src.backup(dst) dst.close() src.close()

备份频率可以根据业务量决定。像房源信息这种低频修改的数据,每天备份一次已经足够;如果做了“租客在线预订”这类高频写入功能,就要考虑更频繁的备份,甚至迁移到 MySQL 这类数据库。

7.3 SQL 注入防护

在模板渲染和 SQL 拼接上,要特别留意 SQL 注入。本文示例已经使用?占位符,这是最基础也最重要的防护手段。如果以后二次开发时有人把用户输入直接拼进 SQL,比如:

sql = f"SELECT * FROM house WHERE title LIKE '%{keyword}%'"

这就是典型的注入写法。用户输入%' OR 1=1 --就能改变查询逻辑,甚至删除数据。一旦在代码审查中发现类似写法,必须立刻改成参数化查询。

7.4 上线部署注意

本地开发用的是 Flask 自带开发服务器。把它直接暴露到公网,既不安全,性能也扛不住高并发。建议部署方式:

  • 使用 Gunicorn 作为 WSGI 服务器(Linux/macOS),或者 Waitress(Windows 友好);
  • 前面再挂 Nginx 做反向代理和静态文件处理;
  • 数据库文件放在独立数据目录,做好备份;
  • Flask 的debug=True必须关闭,SECRET_KEY必须设置为随机值;
  • 开启 HTTPS,避免表单信息和联系方式在传输中被明文截获。

部署命令示例(Gunicorn):

gunicorn -w 4 -b 127.0.0.1:8000 app:app

然后让 Nginx 把80/443端口的请求转发到127.0.0.1:8000

部署是另一个比较大的话题,这里不展开全部细节。如果你第一次部署 Flask 项目,建议先在一台测试服务器上完整走一遍流程,再切生产环境。涉及公网访问、权限配置时,一定要先确认自己的操作合法合规,并且有备份和回滚方案。

7.5 权限管理

如果这个系统不止一个人使用,或者放在公网上,就一定要考虑权限。目前/add路由任何人都能访问,意味着任何访问者都可以修改数据库,这在真实场景不可接受。

最轻量的方案是给表单页加一个简单的登录校验,用 Flask 的session记录登录状态。复杂一点的方案是接入 Flask-Login 或 Flask-Security。无论哪种方案,都要注意:

  • 密码不能明文存储,要使用werkzeug.securitygenerate_password_hashcheck_password_hash
  • session 需要配置SECRET_KEY
  • 所有修改操作,包括新增、编辑、上下架,都应该校验权限,而不是只隐藏按钮。

8. 总结与后续学习路线

这一篇从一条零散的房源文本信息出发,完成了三个层次的事:先把房源信息拆成结构化字段,再用 Flask 写了一个完整的展示、筛选、新增流程,最后补充了搜索、导出、部署、安全等工程化建议。

具体来说,你应该已经掌握了:

  • CREATE TABLE设计房源信息表;
  • 用 Flask 路由实现列表页、详情页、新增表单页;
  • 用 Jinja2 模板循环渲染房源卡片;
  • 用 SQLite 占位符参数查询,避免 SQL 注入;
  • url_for处理静态资源与页面跳转;
  • 用 POST 请求实现上下架类状态修改操作;
  • 部署前需要关注的配置分离、数据库备份、权限校验、关闭 debug 等事项。

接下来的学习方向,可以根据实际需要选择:如果想把页面做得更像正式产品,可以学学 Bootstrap 或 Tailwind CSS;如果房源数量变多、查询变慢,可以研究 SQLite 索引优化,或者切换到 MySQL;如果要做在线预约、订单管理,那就需要学习 Flask 的表单校验、用户认证和 REST API 设计。

不管往哪个方向深入,都建议先把今天这个最小系统跑通,然后自己加一个小功能,比如增加“带阳台”筛选按钮,或者给详情页加一个图片区域。改一个功能,比重新读一遍文章学到的东西更多。

如果你在跟着文章实操时遇到了报错,欢迎把错误日志里关键的报错行截图或复制出来,对照第 6 部分的排查表逐个检查。项目相关代码文件不多,排查起来不会太复杂。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 21:47:52

欧卡2宝马M4 G82 MOD安装全攻略:从版本适配到排错实战

玩《欧洲卡车模拟2》的老玩家应该都有这种体会&#xff1a;开着卡车在高速上巡航久了&#xff0c;总想换换口味&#xff0c;在同一个欧洲世界里体验一把性能轿跑的感觉。正因如此&#xff0c;车辆类MOD一直是欧卡社区最热门的方向之一。本文围绕2022款宝马M4 G82 Competition&a…

作者头像 李华
网站建设 2026/9/3 21:41:48

合成工作流排查指南:从素材体检到导出校验的稳定输出方法

做创意合成这行&#xff0c;最常听到的牢骚是“明明时间线上看着挺好&#xff0c;一导出全毁了”。MONTAGEM 这个词放到中文语境里&#xff0c;既可以指短视频剪辑里的卡点混剪&#xff0c;也可以指音视频工程里的多层合成&#xff0c;而“DEAD WRONG”描述的正是那种最要命的状…

作者头像 李华
网站建设 2026/9/3 21:40:18

104.C 语言字节对齐:从底层原理到工程实践的完整指南

在嵌入式开发、系统编程和跨平台通信中&#xff0c;sizeof(struct)的结果往往不等于成员变量大小之和&#xff0c;这背后的核心原因就是字节对齐。作为 C 语言面试的高频考点&#xff0c;字节对齐不仅涉及 CPU 硬件的底层设计&#xff0c;还直接影响程序的性能、内存占用和跨平…

作者头像 李华
网站建设 2026/9/3 21:39:47

PyTorch+LSTM实现高速车辆轨迹预测实战方案

简介&#xff1a;本资源是一套基于PyTorch实现的高速公路车辆轨迹预测完整项目&#xff0c;面向计算机、人工智能及相关专业本科生&#xff0c;特别适用于毕业设计、课程设计及期末大作业场景。项目采用LSTM深度学习模型处理NGSIM真实交通数据集&#xff0c;完成多步车辆轨迹建…

作者头像 李华
网站建设 2026/9/3 21:30:30

基于WPF和ReactiveUI的节点编辑器实践:NodeNetwork库应用解析

简介&#xff1a;NodeNetwork 是一个面向 .NET 平台、基于 C# 与 WPF 的节点编辑器组件库&#xff0c;核心采用 ReactiveUI 实现响应式 MVVM 交互&#xff0c;适合为图形化工具、着色器编辑器和计算器应用快速搭建可视化节点编辑界面。资源包共 266 个文件&#xff0c;压缩后仅…

作者头像 李华
网站建设 2026/9/3 21:29:57

从左右开弓到11杀吃鸡:PUBG第一视角复盘的核心是决策顺序

如果你在直播间里刷到“左右开弓连打两队”“11杀吃鸡”这种标题&#xff0c;大概率会把它归类为“又一把爽局”&#xff0c;然后看完击杀镜头就划走。但如果你真的想从一场比赛里学到东西&#xff0c;这种局恰恰是最值得停下来的样本&#xff0c;因为它不是靠单一个镜头赢下来…

作者头像 李华