同样一套房源信息,扔到业主群、朋友圈是纯文字,过几天就刷没了;整理成一个网页或者小系统,不仅自己好维护,别人看着也清楚。今天就用 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.x、Python 3.10.x、Python 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\activatemacOS 或 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 flask2.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 数据库表结构设计
房源信息需要字段来承载结构化的数据。参照前面那条示例房源,可以设计如下表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INTEGER PRIMARY KEY AUTOINCREMENT | 主键,自动增长 |
| title | TEXT | 房源标题 |
| address | TEXT | 详细地址 |
| house_type | TEXT | 房型,比如 3室1厅1厨1卫2阳台 |
| bed_info | TEXT | 床型信息,比如 1.8m+1.5m+1.2m |
| price_day | REAL | 日租价格 |
| price_week | REAL | 周租价格 |
| price_month | REAL | 月租价格 |
| has_balcony | INTEGER | 是否有阳台,1 表示有,0 表示没有 |
| remark | TEXT | 备注或其他说明 |
| status | INTEGER | 状态,1 表示可租,0 表示已下架 |
| create_time | TEXT | 创建时间 |
关于价格字段,这里用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.title比house[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') }}">← 返回列表</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') }}">← 返回列表</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 的pandas和openpyxl实现导出功能。思路是查询数据库后生成 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.security的generate_password_hash和check_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 部分的排查表逐个检查。项目相关代码文件不多,排查起来不会太复杂。