最近好几个准备做毕设的同学来找我聊选题,翻来覆去总绕不开"XX管理系统""XX商城"这类做了几百遍的东西,答辩时很难讲出新意。我一般会建议换个思路:做一个租房市场数据采集与可视化平台——用Python写爬虫把房源数据抓下来,清洗后存进数据库,后端用Flask提供接口,前端用Layui搭整体界面,配合ECharts把房租走势、区域均价、户型分布这些指标用图表展示出来。这样一个项目同时覆盖数据采集、数据清洗、后端接口、前端可视化四块能力,拿到毕设答辩现场有内容可讲,写进简历也是实打实的项目经验。
整个平台的原始方向并不复杂,核心就是"取数—入库—出报表"。但真正动手做起来,从爬虫反爬规避到字段口径统一,再到前后端联调时的各种小坑,每一步都有不少讲究。这篇文章我按完整项目落地的顺序,从选题定位、技术选型、爬虫实现、数据库设计、可视化统计,一直讲到Layui和ECharts的联调,最后再补充大模型、agent等可以给毕设加分的扩展玩法,以及我个人踩过的几个实打实的坑。内容偏实操,代码片段可以直接拿去改。
1. 毕设选题定位:数据采集+可视化为什么值得做
1.1 三类常见毕设选题的利弊对比
我把这些年接触过的计算机类毕设选题大致分成三类。第一类是纯管理系统,比如图书管理、学生信息管理,开发难度低,但技术点单薄,数据库设计稍微常规一点,答辩老师问两句"缓存怎么设计""并发怎么处理"就容易卡壳。第二类是算法研究型,比如图像识别、推荐算法,听起来高大上,但需要很强的数学功底和实验条件,短期出成果风险高。第三类就是数据采集加可视化这类"工程型+数据型"项目,它既有完整的开发链路,又能展示数据处理的思维,难度适中,工作量可调控,最适合本科阶段用来证明综合能力。
这个租房平台的定位,就是典型的第三类。它不是简单地把页面做出来就行,而是要先证明数据来源真实可靠——爬虫能否稳定抓取、字段是否完整、清洗是否严谨,这些都会直接影响后端接口和可视化图表的最终效果。答辩时老师一看到有真实数据在流动,就会觉得这个项目不是凭空捏造的Demo,而是有一套完整的数据链路。
1.2 项目功能全景与数据流转链路
我在给同学规划这个项目时,会把系统拆成四个模块:数据采集模块、数据存储模块、后端服务模块、前端可视化模块。数据流向是从目标站点抓取原始HTML,解析出房源标题、租金、面积、户型、所在区域、发布时间等字段,清洗之后写入MySQL,再通过Flask提供的REST接口按条件查询聚合结果,最后在Layui搭建的管理界面里用表格和图表呈现。
这里有个容易被忽略的点:数据采集模块输出的字段质量,决定了后面所有统计结果的准确性。比如"租金"这个字段,有的房源是按月出租,有的是按季度付,还有的是押一付三;"面积"字段可能是"45㎡",也可能在标题里写的是"45平"。如果清洗阶段不把这些格式统一掉,后面算区域平均租金时就会混入脏数据,图表上的数字一塌糊涂。所以在项目第一天就要定好字段规范化规则,而不是等到可视化阶段才返工。
2. 技术选型:Flask做后端、Layui做前端的取舍逻辑
2.1 后端框架为什么选Flask而不是Django
后端框架这一层,毕业生常纠结的是Flask和Django。我给的结论很直接:这个项目用Flask比Django更合适。Django自带Admin后台、ORM、表单组件,功能齐全但框架感强,路由和视图之间容易绕;Flask足够轻量,路由定义简单清晰,配合蓝图(Blueprint)可以把爬虫接口、统计接口、筛选接口拆得很干净。更重要的是,Flask的学习曲线平缓,对于主要精力要花在爬虫和可视化上的同学来说,能更快进入状态。
| 对比项 | Flask | Django |
|---|---|---|
| 项目启动成本 | 低,一个文件就能跑起Hello World | 高,需生成完整项目骨架 |
| 路由与视图 | 装饰器风格,直观 | 类视图或函数视图,规则固定 |
| ORM | 可自由选择SQLAlchemy等 | 内置ORM,绑定较紧 |
| 适合场景 | 轻量接口服务、小型数据分析平台 | 内容管理后台、大型Web应用 |
用Flask还有一个隐藏优势:它可以和爬虫代码放同一个工程目录里,爬虫跑完的数据直接通过同一个Python进程写入数据库,写接口时不用跨语言调服务,整个项目结构统一,不管是一个人开发还是分模块写都很顺手。
2.2 前端框架选型的实际考量
前端这块,很多同学第一反应是Vue或React。但对于毕设项目,我反而推荐Layui。Layui是一个经典的后端开发者友好型前端UI框架,它不需要Node.js构建环境,不用npm install,直接在HTML里引入CSS和JS文件就能工作。它内置了表格(table)、表单(form)、弹层(layer)、导航等组件,恰好覆盖了管理类界面最常见的需求。对毕设来说,省掉前端工程化构建步骤,意味着少踩很多环境坑,把精力放在业务逻辑上。
Layui的表格组件尤其适合展示房源列表。它自带分页、排序、前端搜索和工具栏按钮,通过一个简单的table.render()配置就能渲染后端返回的JSON数据。相比从零手写一个列表页,再用原生JS做分页,Layui可以节省大量时间。而且这套框架在国内项目中使用广泛,网上示例多,遇到问题搜索一下很快就有解决方案。
2.3 数据库与可视化组件的选择
数据库我倾向于MySQL,因为租房数据以结构化字段为主——价格、面积、户型、区域都是明确字段,MySQL存储和聚合查询都很高效。如果采集量特别大,还可以考虑分表存储,但毕设阶段数据量在几千到几万条级别,单表加索引完全够用。如果你本机没装MySQL,用SQLite也能跑通,但答辩时老师可能会问"为什么不用主流的MySQL",与其被追问,不如直接上MySQL更稳。
可视化组件用ECharts,理由很简单:它只有,图表类型丰富,折线图、柱状图、散点图、地图全都有,交互效果好,而且Layui里面可以无缝嵌ECharts的图表容器。后面的统计模块,我会用它画租金分布直方图、区域均价柱状图、时间趋势折线图,这些图表放在一起,整个平台的"可视化"成色就出来了。
3. 爬虫模块实现:从页面解析到数据落库的完整过程
3.1 目标站点结构分析与解析策略
写爬虫前先要搞清楚目标站点的HTML结构。我一般用Requests库把页面源码拉下来,再用BeautifulSoup做解析。这里的核心技巧是:不要上来就写全量解析代码,而是先跑一小段脚本打印出目标区块的HTML结构,看清楚房源信息是嵌在HTML标签里,还是被JSON初始化变量包裹,又或者是通过接口异步加载的。不同类型的页面,解析策略完全不同。
很多房源平台把数据放在一个叫window.__INITIAL_STATE__之类的全局变量里,或者直接给一个内部接口返回JSON。这种情况用BeautifulSoup纯解析标签其实很别扭,更优雅的做法是直接访问那个内部JSON接口,拿到干净的数据。怎么发现内部接口?打开页面后通过开发者工具里的网络面板看XHR请求,找到返回内容里带房源列表的接口地址,复制接口参数,在Python里模拟请求就行。
示例代码框架大致是这样:
import requests from bs4 import BeautifulSoup headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } resp = requests.get("https://目标站点列表页URL", headers=headers, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") for item in soup.select("div.house-list > div.item"): title = item.select_one(".title").text.strip() price_info = item.select_one(".price").text.strip() location = item.select_one(".location").text.strip() print(title, price_info, location)3.2 请求策略:请求头、限速与随机延迟
爬虫被限制IP是最常见的问题。如果一次性并发太高、请求频率过快,目标站点很容易识别出不是真人操作,返回滑块验证或者直接封IP。我常用的策略有三个:第一,请求头一定要补全,至少包含User-Agent和Referer,必要时加上Accept-Language,模仿真实浏览器的请求特征;第二,每抓一页加一个随机延迟,大致在1到3秒之间波动,不要固定间隔,固定间隔反而容易被统计规律识别;第三,为爬虫准备一个代理池,如果平台封了一段时间的IP,可以切换代理重新抓取,但毕设阶段如果只是演示功能,控制好频率通常不至于触发风控。
另外要强调的是,爬取前应该确认目标站点是否允许此类访问。毕设项目本来就是学习用途,我们只做小规模数据演示,不要对目标站点造成压力,更不要抓取个人信息字段。公开的房源基础信息用于学习和统计分析,属于常态操作。
3.3 数据清洗与字段标准化的几个要点
清洗是整个爬虫最耗时间的一步。字段标准化规则,我在搭爬虫之前就定好,大概是这样的清单。
| 源字段 | 标准化规则 | 示例 |
|---|---|---|
| 租金 | 去掉"元/月",统一转成整数 | "4500元/月" → 4500 |
| 面积 | 保留数字,单位统一用平方米 | "45.2㎡" → 45.2 |
| 户型 | 规范成"N室M厅"格式 | "两室一厅" → "2室1厅" |
| 区域 | 只取第一级行政区词 | "北京市朝阳区" → "朝阳区" |
| 发布时间 | 统一为YYYY-MM-DD格式 | "昨天" → 按当前日期推算 |
这里有一个容易出错的地方:不少房源的租金是"面议"或者"5500-6500元/月"这种区间形式。如果直接int()转换就会报错,所以清洗时要先做非数值判断,把"面议"清洗成空值,把区间租金取中间值或下限值,并标注字段来源,保证统计时不至于因为空值中断。
3.4 增量更新与数据去重
如果平台只跑一次爬虫,生成的是静态快照,维度单一。我建议做一个简单的增量更新机制:每次爬取前查数据库里已有的房源ID集合,然后在新抓取结果里过滤掉已存在的ID,只插入新数据。这个去重逻辑用一条SQL就能实现——在房源表上给房源唯一标识建唯一索引,插入时加INSERT IGNORE,新记录自动进去,重复记录自动忽略,简单高效。
ALTER TABLE house ADD UNIQUE KEY uk_source_id (source_id); INSERT IGNORE INTO house (source_id, title, price, area, layout, region, publish_date) VALUES (%s, %s, %s, %s, %s, %s, %s);这样每次跑爬虫就相当于增量同步,几天后去重统计,#总房源数#只会增长,不会产生重复记录。答辩时可以给老师演示这个增量流程,说明平台不是一次性数据,而是可持续更新的,这比死板的静态展示更有说服力。
4. 数据库建模:让房源数据"查得动、算得快"
4.1 核心表结构设计与字段说明
数据库表设计不能等到爬虫写完再拍脑袋。我的习惯是先画ER图,把房源、区域、小区、租金走势这几类实体分开。最核心的是房源表,字段设计大致是:id主键,source_id表示来源平台唯一标识,title是标题,price是租金,area是面积,layout是户型,region是区域,address是地址,publish_date是发布日期,create_time是抓取时间。
此外我用一张area_stats表来存区域维度的统计结果,字段包括region、avg_price、total_count、avg_area、stat_date。当数据量变大时,页面上的区域对比图直接查这张汇总表,比每次都对全表做GROUP BY要快得多。很多同学会把汇总计算全部放在查询时动态完成,数据少的时候看不出问题,一旦到几万条数据,页面接口响应会明显变慢。提前算好汇总表,实时接口只做轻查询,是典型的空间换时间思路。
4.2 索引设计的实操建议
索引是答辩时的一个加分点。不要在每列上都建索引,索引太多会拖慢写入速度。这个场景下我只建三个:region列建普通索引,因为区域筛选是最高频操作;publish_date列建普通索引,因为趋势图要按日期排序;price列建普通索引,因为价格区间的过滤经常用。复合索引可以按(region, publish_date)建一个,用于区域加时间的交叉分析。
CREATE INDEX idx_region ON house (region); CREATE INDEX idx_publish_date ON house (publish_date); CREATE INDEX idx_region_date ON house (region, publish_date);用EXPLAIN验证一下查询计划,确认SQL能够命中索引而不是全表扫描,这个细节说给答辩老师听,比堆功能更显专业。
4.3 Flask中连接数据库的两种方式
操作数据库我建议直接用Flask-SQLAlchemy,它把连接池、事务、对象映射都封装好了,代码写起来干净。如果不想引入ORM,用PyMySQL原生游标也可以,但获取到的数据要手动转成字典,再JSON化,项目一大就显得繁琐。下面是一个用Flask-SQLAlchemy做统计查询的示例:
from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() class House(db.Model): __tablename__ = "house" id = db.Column(db.Integer, primary_key=True) source_id = db.Column(db.String(64), unique=True) title = db.Column(db.String(200)) price = db.Column(db.Integer) area = db.Column(db.Float) layout = db.Column(db.String(32)) region = db.Column(db.String(32)) publish_date = db.Column(db.Date)用ORM还有一个好处:Flask的模型定义和迁移工具能自动生成建表语句,做完model定义后执行db.create_all()就能把表建出来,省去手工维护SQL脚本的麻烦。
5. 可视化统计模块:图表背后的口径与逻辑
5.1 先把统计指标口径定清楚
做可视化之前,要先把指标口径敲定,否则图表数字自己都解释不通。比如"区域均价",我规定为区域内所有有效房源的算术平均租金,剔除price为空和明显异常值(比如价格小于500元或大于100000元的记录)。"价格分布"按1000元一个区间分段,统计每段内房源数。"增长率"则是本期均价相对上期均价的百分比。每个指标都要在页面边上放一个说明性文字,这个细节很多同学忽略,但它能直观体现数据分析的严谨性。
5.2 用ECharts画价格分布直方图
价格分布图我推荐用ECharts的histogram或者bar,X轴是租金分段,Y轴是房源数量。后端接口把每个区间的计数返回给前端,前端用一个bar series就能渲染。需要注意的一个细节是,ECharts的grid间距默认比较紧凑,在做毕设演示时,把图表的字号调大,tooltip触发方式设为axis,鼠标滑过能看到该区间房源占比,交互体验会好很多。
var chart = echarts.init(document.getElementById("priceChart")); chart.setOption({ tooltip: { trigger: "axis" }, xAxis: { type: "category", data: priceRangeLabels }, yAxis: { type: "value" }, series: [{ type: "bar", data: priceRangeCounts, itemStyle: { color: "#5B8FF9" } }] });5.3 区域均价对比与时间趋势图
区域均价对比适合用横向柱状图,把区域名放Y轴,均价放X轴,一眼能看出去哪个区域租金高。时间趋势图适合用折线图,统计每天或每周发布房源的平均租金,观察价格随时间的波动。这里的关键是后端要返回按时间排序的数组,前端再按序渲染,别把坐标轴数据传成乱序集合。
做趋势图时,最好再叠加一个"房源数量"副Y轴,这样既能看均价变化,也能看供给量变化,两个维度结合才有分析价值。ECharts的yAxisIndex属性可以指定系列使用第几个Y轴,合理使用这个参数,图表的信息密度会高不少。这个细节做好,整个平台的"数据分析"深度就上去了。
5.4 维度筛选组合:区域、户型、价格区间联动
可视化比静态报表强的地方就在于可以交互筛选。我在页面上放三个筛选条件:区域下拉选择、户型下拉选择、价格区间滑动条。用户每调整一个条件,页面就重新请求接口,图表和数据表格一起刷新。后端的接口通过args参数接收筛选条件,动态拼接SQL,然后返回表格数据与统计聚合数据。这样一个页面既展示了数据列表,又展示了可视化图表,功能之间互相联动,比单独的列表页更有说服力。
6. Flask后端与Layui前端的联调细节
6.1 统一接口返回格式与分页参数
前后端联调首先要约定接口返回值格式。我的约定是:所有接口统一返回一个JSON对象,包含code、msg、data三个字段。code为0表示成功,非0表示业务异常;data里放实际数据,列表场景再包含count字段用于分页。这个约定一旦定下来,前端Layui表格的parseData回调几乎不用额外处理,可以直接映射字段。
{ "code": 0, "msg": "success", "data": { "count": 256, "rows": [ {"id": 1, "title": "出租主卧带阳台", "price": 4500, "area": 20.5} ] } }6.2 Layui表格组件的数据加载配置
Layui的table组件加载后端数据非常直接,在render时指定url和cols数组,它就会自动发起请求并渲染表格。需要注意,如果后端返回格式不是Layui默认的{code:0, msg:"", count, data},需要写一个parseData函数把字段映射一下。我在实际项目中就遇到过同学把后端返回的count放在data里导致分页显示异常的问题,原因就是没做parseData映射。
table.render({ elem: "#houseTable", url: "/api/house/list", page: true, limit: 10, parseData: function(res) { return { "code": res.code, "msg": res.msg, "count": res.data.count, "data": res.data.rows }; }, cols: [[ {field: "title", title: "房源标题"}, {field: "price", title: "月租金"}, {field: "area", title: "面积"}, {field: "region", title: "区域"} ]] });6.3 筛选条件与图表的联动实现
联动的实现思路很简单:把筛选控件绑定change事件,每次变化后重新请求两个接口,一个返回表格数据,一个返回图表聚合数据。这里要避免的一个性能坑是,筛选条件一变就同时请求多个接口,多个接口串行执行会导致页面有明显的卡顿感。我建议把stats相关的聚合放在同一个接口里一次返回,前端分成多个图表去取对应字段,减少往返次数。
另外,Layui的table实例有一个reload方法,可以传新参数重新加载表格数据:
function reloadData() { var region = $("#regionSelect").val(); table.reload("houseTable", { where: { region: region } }); loadCharts(region); }这样表格和图表就通过同一个筛选状态保持同步,整体的交互逻辑非常顺。
7. 给毕设加分:大模型、agent与大数据方向的扩展玩法
7.1 大模型接口的合理接入:做一个租房助手问答
标题里出现了"大模型"三个字,这其实是很自然的加分点。最稳妥的接入方式是调用市场上常见的大模型服务商的客服类接口,在后端封装一个/chat接口,把用户的问题和平台的统计数据拼成提示词,让模型基于真实数据回答。比如用户问"朝阳区一居室均价多少",后端先查数据库得到结果,再把结果作为上下文发给大模型,让它组织成更自然的语言返回。这个方案既展示了调用大模型的能力,又不会出现模型回答跟数据库数据对不上的尴尬,因为关键数字是查出来的,不是模型生成的。
7.2 简化版agent:自动生成数据分析报告
所谓agent,不一定真要搭一个复杂的决策智能体,而是可以用一套规则引擎加自然语言生成,做一个"自动分析报告"功能。比如系统定期读取area_stats表,发现某个区域均价环比上涨超5%,就自动生成一段提醒文案:"朝阳区近7日平均租金环比上涨6.2%,本周挂牌房源数量下降8%,建议关注该区域供需变化。"再加上大模型接口润色,就变成一个能自动产出结论的轻量agent。这个玩法成本很低,但答辩时可以讲清楚"数据驱动决策"的逻辑,比单纯的图表展示更有深度。
7.3 大数据方向的正确打开方式
"大数据"这个词在标题里更多是噱头,但我们可以正向利用它。毕设阶段不一定要真的搭建Hadoop集群,把批量数据处理思路带到项目里就够了:数据文件分区存储、ETL清洗流程、定时任务调度,这些概念的实践成本不高,但让项目结构看起来有大数据处理的影子。如果学有余力,可以考虑把爬虫采集到的原始日志写入Kafka,再由消费者写入MySQL,但这类重型组件对毕设来说通常不是必需项,更适合作为口头扩展方向。
8. 实战踩坑记录:从爬虫被限制到图表不渲染的完整排查
8.1 爬虫触发风控后的排查思路
我第一次帮某同学调这个项目时,他的爬虫跑了二十分钟后被目标站点限制了访问,请求返回的内容从房源列表变成了一段验证跳转HTML。当时我让他先别急着换代理,而是去看请求头是不是伪装得不够完整——他代码里只写了User-Agent,没有写Referer,有些站点会校验这个字段。补齐Referer且换成浏览器动线后,问题就消失了。如果补齐请求头仍然被限制,再考虑降低抓取频率,或者分时段抓取。排查的顺序应该是:请求头完整性 → 请求频率 → 单IP累计次数 → 代理更换,不要跳过任何一层。
8.2 数据量增长导致页面报表变慢
项目刚开始数据量只有几百条,页面响应很快。等到采集了几千条后,首页的区域统计接口开始明显变慢,我一看SQL,发现查询语句里的WHERE region = 'xx'没有命中索引,因为最初建表时没给region字段建索引。补上索引之后,响应时间从1.8秒降到0.3秒以内。这个问题的教训是:哪怕最初数据量小,也要按最终规模去设计索引,别等到卡了再补。同样的思路也适用于price排序和publish_date范围查询。
8.3 Layui Tab切换后ECharts图表不渲染或变形
Layui的Tab切换到含有图表的页面时,ECharts常常出现宽度为0或加载不出来的情况。这是因为Tab面板初始状态是隐藏的,图表在隐藏容器中初始化时无法正确计算宽度。解决方案有两条:一是利用Layui的Tab切换事件,在切换到对应Tab后再调用chart.resize()强制重绘;二是使用ECharts的resize方法配合定时器兜底。从有效性的角度,绑定Tab切换事件来触发resize更可靠,因为定时器依赖时间和实际切换动作的匹配,容易出现逻辑错位。我后来把触发代码统一封装成一个refreshChart函数,在每个Tab的切换回调里调用,图表渲染的稳定性明显提高。
layui.element.on("tab(filter)", function(data) { setTimeout(function() { priceChart.resize(); regionChart.resize(); }, 50); });8.4 表单提交时的时间字段格式问题
还有一个隐藏得比较深的坑:清洗后的发布日期在前端表格里总是显示NaN或者乱码。排查后发现是因为Python的datetime对象在JSON序列化时变成了"Fri, 01 Jan 2024 00:00:00 GMT"这种字符串,Layui的表格模板无法直接解析。解决方法是后端在序列化时间字段前统一转成"%Y-%m-%d"格式,或者在前端用templet自定义列模板做格式转换。这个问题属于典型的"后端看着没问题,前端显示错乱",多遇到几次就长记性了。
写在最后:关于这个项目的个人体会
这类"数据采集+可视化"项目我能推荐给多数准备做毕设的同学,核心原因在于它能力边界清晰,工作成果可量化。写爬虫能看到真实数据落库,写统计能看到图表变化,每一步都有反馈,适合一个人独立完成。如果你也想趁这个题目扩展,我建议优先级排成这样:先把爬虫清洗流程做实,再把统计口径讲清楚,接着把Layui界面打磨到能流畅演示,最后还有精力再做agent和大模型扩展。很多同学一上来就研究大模型,结果基础设计稀碎,答辩时反而被问住了。按这个路线走,项目完成度会高很多,祝顺利。