
简介这是一套面向计算机专业毕业设计的完整项目资料基于PythonFlask构建租房数据分析系统适合需要完成毕设或想练手全栈开发的学生。系统分为后台管理端与前台展示端管理员可进行用户管理、房屋信息维护、租房数据统计与系统维护普通用户可浏览房源、查看租金价格分布与热门区域分析、管理个人收藏与浏览记录前后端分离架构兼顾体验与效率。压缩包共573个文件约23.51MB以vue组件、js脚本、py源码、svg与jpg/png图片资源为主另含sql数据库脚本、bat一键安装与运行脚本、md说明文档及docx开发说明覆盖源码、数据库、运行教程与逻辑讲解。目前已有71人学习下载。读者可获得完整可运行的毕设方案、清晰的目录结构与部署脚本以及数据分析模块的实现思路便于快速理解Flask接口设计、数据库建模与前端交互逻辑为答辩与二次开发提供参考。1. 从一份租房数据到能跑起来的分析系统这套 Flask 方案到底解决什么问题租房数据本身不稀缺稀缺的是把它变成可查询、可对比、可解释的形态。很多同学拿到一份 CSV 就开始df.describe()跑完发现除了均价和最高价什么结论都说不出来。这套基于 Python Flask 的租房数据分析系统核心要解决的就是把「一堆散落的房源记录」变成「一个能按区域、价格、户型筛选并出图的服务」。它适合两类人一类是正在做计算机毕业设计、需要一个完整可演示项目的同学另一类是想用 Flask 练手一个真实数据闭环的开发者。整套东西的技术栈不复杂——Python 做数据处理Flask 做 Web 层前端用模板渲染加图表库数据落在 SQLite 或 MySQL 里。真正值得花时间的不是框架本身而是数据清洗规则、筛选接口的参数设计以及图表怎么和筛选条件联动。下面按「先立住原理、再动手复现、最后讲坑」的顺序拆开讲。2. 数据层怎么搭从原始房源表到可查询的结构化数据2.1 先想清楚字段设计再谈爬不爬数据租房数据的字段看着简单真动手就会发现坑全在细节里。原始数据通常来自公开房源平台或自己整理的表格字段大致是标题、区域、小区、户型、面积、朝向、楼层、租金、发布时间、标签。这里第一个决策点是租金到底存字符串还是数值。原始数据里经常出现「3500元/月」「面议」「3500-4000」这几种混写如果直接存字符串后面排序和区间筛选全废。我的做法是拆成两个字段price数值面议存 NULL和price_raw原始文本留作追溯。第二个决策点是区域字段的粒度。很多数据里「朝阳区」「朝阳」「朝阳区望京」混在一起如果不做归一化按区域分组统计时会出现同一个区被拆成好几组。常见做法是维护一张区域映射表把各种写法收敛到标准名。字段名类型说明是否可空idINTEGER主键自增否titleVARCHAR(200)房源标题否districtVARCHAR(50)归一化后的区域名否communityVARCHAR(100)小区名是layoutVARCHAR(20)户型如 2室1厅是areaFLOAT建筑面积平米是orientationVARCHAR(20)朝向是floorVARCHAR(30)楼层描述是priceFLOAT月租金数值是price_rawVARCHAR(50)原始租金文本是publish_dateDATE发布时间是tagsVARCHAR(200)标签逗号分隔是这张表的设计原则是能结构化的尽量结构化不能结构化的保留原文。price用来算price_raw用来给人看两者不冲突。2.2 用 pandas 做清洗把脏数据挡在入库之前清洗脚本是整个系统的地基写得好后面省一半事。下面这段是我一般会用的清洗流程核心是三件事租金解析、区域归一、异常值处理。import pandas as pd import re # 读取原始数据注意编码中文数据常见 gbk / utf-8-sig df pd.read_csv(raw_rent.csv, encodingutf-8-sig) # 1. 租金解析从 3500元/月 面议 3500-4000 中提取数值 def parse_price(text): if pd.isna(text): return None text str(text) if 面议 in text: return None # 取区间下限匹配第一个数字串 match re.search(r(\d), text.replace(,, )) return float(match.group(1)) if match else None df[price] df[price_raw].apply(parse_price) # 2. 区域归一把各种写法收敛到标准名 district_map { 朝阳: 朝阳区, 朝阳区: 朝阳区, 海淀: 海淀区, 海淀区: 海淀区, 丰台: 丰台区, 丰台区: 丰台区, } df[district] df[district].str.strip().map( lambda x: district_map.get(x, x) ) # 3. 异常值处理面积和租金超出合理范围的直接标记 df[area] pd.to_numeric(df[area], errorscoerce) df.loc[(df[area] 5) | (df[area] 1000), area] None df.loc[(df[price] 100) | (df[price] 200000), price] None # 4. 去重标题小区面积 相同视为重复 df df.drop_duplicates(subset[title, community, area], keepfirst) df.to_csv(clean_rent.csv, indexFalse, encodingutf-8-sig) print(f清洗完成剩余 {len(df)} 条记录)逻辑说明parse_price用正则抓第一个数字串区间价取下限这是租房场景里比较稳妥的策略因为区间上限往往包含虚高报价。区域映射表用字典维护新增区域直接加键值对不用改逻辑。异常值用loc批量置空而不是删除保留记录但让它在统计时被自动忽略。去重维度选「标题小区面积」是因为同一房源可能被多次发布标题和面积组合基本能唯一标识。参数说明encoding一定要试中文 CSV 用utf-8-sig能避免 BOM 导致的列名乱码errorscoerce让无法转换的值变成 NaN 而不是报错中断面积上下限 5 和 1000 是经验值太小可能是车位太大可能是整栋按你的数据分布调。2.3 入库SQLite 够用但要知道它的边界毕业设计场景下 SQLite 完全够用零配置、单文件、方便打包。用 SQLAlchemy 建表并写入from flask_sqlalchemy import SQLAlchemy from flask import Flask import pandas as pd app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///rent.db db SQLAlchemy(app) class RentHouse(db.Model): id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(200), nullableFalse) district db.Column(db.String(50), indexTrue) # 区域加索引 community db.Column(db.String(100)) layout db.Column(db.String(20)) area db.Column(db.Float) price db.Column(db.Float, indexTrue) # 价格加索引 publish_date db.Column(db.Date) with app.app_context(): db.create_all() df pd.read_csv(clean_rent.csv) for _, row in df.iterrows(): db.session.add(RentHouse( titlerow[title], districtrow[district], communityrow[community], layoutrow[layout], arearow[area], pricerow[price], )) db.session.commit()district和price加索引是因为筛选接口最常按这两个字段过滤数据量上万后没索引查询会明显变慢。SQLite 的边界在于并发写入多个请求同时写会锁库但分析系统以读为主这个问题不突出。如果要做多用户同时提交数据换 MySQL 更稳。3. Flask 后端筛选接口和统计接口怎么设计才不返工3.1 接口按「筛选」和「统计」分开别混在一起新手常犯的错是把筛选和统计塞进一个接口前端传一堆参数后端又查列表又算均值结果两边都不好复用。我的做法是拆成两类/api/houses负责按条件返回房源列表分页/api/stats负责返回聚合结果均价、数量、分布。这样前端筛选条件变化时两个接口用同一套参数图表和列表能同步刷新。from flask import request, jsonify app.route(/api/houses) def get_houses(): # 获取筛选参数都有默认值 district request.args.get(district) price_min request.args.get(price_min, typefloat) price_max request.args.get(price_max, typefloat) layout request.args.get(layout) page request.args.get(page, 1, typeint) size request.args.get(size, 20, typeint) query RentHouse.query if district: query query.filter(RentHouse.district district) if price_min is not None: query query.filter(RentHouse.price price_min) if price_max is not None: query query.filter(RentHouse.price price_max) if layout: query query.filter(RentHouse.layout layout) pagination query.paginate(pagepage, per_pagesize, error_outFalse) return jsonify({ total: pagination.total, items: [{ title: h.title, district: h.district, price: h.price, area: h.area, layout: h.layout, } for h in pagination.items] })逻辑说明每个筛选条件都是「有值才加」这样前端可以只传变化的那一个参数不用每次传全量。paginate自带总数和分页信息前端做分页器直接用total。参数类型用typefloat让 Flask 自动转换传了非数字会返回 None 而不是报错。参数说明page从 1 开始size默认 20这两个值前端可以调price_min/price_max用is not None判断而不是真值判断因为价格为 0 是合法输入虽然租房场景少见但逻辑上要正确。3.2 统计接口用 SQL 聚合别在 Python 里循环算统计接口如果写成「查出所有记录再在 Python 里算均值」数据量一大就慢得离谱。正确做法是让数据库做聚合from sqlalchemy import func app.route(/api/stats) def get_stats(): district request.args.get(district) query db.session.query( RentHouse.district, func.count(RentHouse.id).label(count), func.avg(RentHouse.price).label(avg_price), func.avg(RentHouse.area).label(avg_area), ) if district: query query.filter(RentHouse.district district) rows query.group_by(RentHouse.district).all() return jsonify([{ district: r.district, count: r.count, avg_price: round(r.avg_price, 2) if r.avg_price else None, avg_area: round(r.avg_area, 2) if r.avg_area else None, } for r in rows])func.count、func.avg是 SQLAlchemy 对 SQL 聚合函数的封装生成的 SQL 在数据库端执行只返回聚合结果。group_by按区域分组一次查询拿到所有区域的统计。注意avg_price可能为 None该区域所有房源价格都为空所以加判断再 round否则会抛异常。3.3 前端图表联动筛选条件一变两个接口一起刷前端用 ECharts 或 Chart.js 都行关键是筛选条件统一管理。下面是一个最小联动逻辑async function refresh() { const params new URLSearchParams({ district: document.getElementById(district).value, price_min: document.getElementById(priceMin).value, price_max: document.getElementById(priceMax).value, }); // 两个接口并行请求互不阻塞 const [housesRes, statsRes] await Promise.all([ fetch(/api/houses?${params}), fetch(/api/stats?${params}), ]); const houses await housesRes.json(); const stats await statsRes.json(); renderTable(houses.items); // 渲染列表 renderChart(stats); // 渲染图表 }Promise.all让两个请求并行比串行快一倍。URLSearchParams自动处理参数编码避免中文区域名出问题。筛选条件变化时调refresh()列表和图表同步更新用户感知是一致的。4. 避坑与排查这套系统最容易翻车的五个地方4.1 中文乱码从 CSV 到页面全链路都要管编码现象CSV 读进来列名是乱码或者页面显示的区域名是问号。原因CSV 文件编码、pandas 读取编码、数据库存储编码、HTTP 响应编码任何一环不一致都会乱。解决CSV 统一用utf-8-sig读Flask 返回 JSON 时设置app.config[JSON_AS_ASCII] False新版 Flask 用app.json.ensure_ascii FalseHTML 模板里加meta charsetutf-8。三处都设对乱码基本绝迹。4.2 筛选后图表不更新参数没传全或缓存没清现象改了区域筛选列表变了但图表还是旧数据。原因图表接口没接收筛选参数或者浏览器缓存了 GET 请求。解决确认/api/stats也接收并应用了同样的筛选参数在 fetch 里加cache: no-store或者给请求 URL 加时间戳。这个坑很隐蔽因为列表对了会让人以为筛选逻辑没问题。4.3 均价被极端值带偏该用中位数的时候用了均值现象某个区域均价显示 8000但点进去看大部分房源是 3000-4000。原因有几条异常高价房源没清干净均值被拉高。解决统计接口同时返回均值和中位数中位数对极端值不敏感更能反映真实水平。SQL 里用func.percentile_cont或先查出价格列表在 Python 里算数据量不大时后者更简单。4.4 分页参数越界page 传太大返回空但不报错现象用户点到最后一页再点下一页列表空了但没提示。原因paginate在 page 超出范围时返回空列表error_outFalse不抛异常。解决前端根据total和size算出总页数禁用超出范围的翻页按钮后端可以在返回里加has_next字段pagination.has_next让前端判断。4.5 部署后接口 500路径和依赖在本地对、服务器不对现象本地跑得好好的部署到服务器上接口报 500。原因数据库文件路径用了相对路径服务器工作目录不同导致找不到或者依赖没装全。解决数据库 URI 用绝对路径或基于app.root_path拼接部署前用pip freeze requirements.txt导出依赖服务器上pip install -r requirements.txt。Flask 部署常见做法是 gunicorn 加 nginx但毕业设计演示用flask run --host0.0.0.0也够关键是路径要对。5. 让分析结果更可信两个我常用的验证技巧5.1 用交叉验证检查清洗规则有没有误伤清洗规则写完不能直接信要验证。我的习惯是抽 50 条原始记录手工标注「应该保留的价格」然后跑清洗脚本对比脚本解析出的价格和手工标注是否一致。不一致的分两类脚本解析错正则要改和原始数据本身有问题该条该丢。这个动作花不了半小时但能避免后面所有统计建立在错误数据上。# 抽样对比脚本 sample df.sample(50, random_state42) sample[price_parsed] sample[price_raw].apply(parse_price) # 导出到 Excel 手工核对 sample[[price_raw, price_parsed]].to_excel(check.xlsx, indexFalse)random_state42保证每次抽样一样方便复现。核对完把有问题的price_raw模式记下来回头补进parse_price的分支里。5.2 统计结果用「分组下钻」自查合理性图表出来别急着截图交差先做一次下钻验证整体均价 4500那按区域分组后各区域均价加权平均应该接近 4500权重是各区域房源数。如果差很多说明有区域数据异常或者分组逻辑有问题。这个检查能抓出「某个区域只有 1 条记录但价格极高」这类问题。验证项方法预期结果价格解析准确率抽样 50 条手工核对准确率 95%区域归一覆盖率统计未映射的区域名数量未映射 5%分组加权均值各区域均价按数量加权与整体均价偏差 5%分页边界请求 page999返回空列表且 total 正确5.3 一个具体技巧把常用筛选存成「视图参数」如果系统要给老师或同学演示每次手动选区域、拖价格区间很费时间。我的做法是在前端加几个预设按钮比如「3000 以下」「3000-5000」「5000 以上」点击直接填充筛选条件并触发刷新。实现上就是几个按钮绑定onclick设置输入框的值再调refresh()。这个技巧不复杂但演示时能省很多解释时间也让系统看起来更完整。最后说个我自己的习惯每次改完清洗规则或接口逻辑一定重新跑一遍从 CSV 到页面的完整流程而不是只测改动的那个函数。因为数据系统里上下游耦合比想象中紧改了解析规则可能影响统计结果改了统计可能影响图表渲染。全链路跑一遍心里才踏实。希望帮到你。本文还有配套的精品资源点击获取