
每年毕业季都有学生问我同一个问题怎么找一个数据量充足、技术栈能铺开、又不容易烂尾的毕业设计题目。我的答案里基于Python的租房数据分析系统是出现频率最高的一个没有之一。这个题目用Django做Web框架用Requests爬虫采集同城平台的房子租赁信息拿到数据后做清洗与分析最后用可视化图表把结果呈现出来还能再挂一个大模型做智能问答——每个环节都是答辩老师熟悉的技术点工作量天然充足而且做出来是真的有人会用的东西不是自嗨玩具。这篇文章就把这套系统的完整拆解、代码思路、以及我实际踩过的坑一次讲清楚无论是准备开题还是正在赶工的同学都能直接当施工参考。1. 选题拆解租房数据分析系统为什么是毕设安全牌1.1 这个题目天然覆盖了毕设验收的全部指标先聊最现实的问题毕设到底在评什么不是评你的算法多牛而是看你有没有完整走完一个发现问题—设计方案—实现落地—验证效果的流程同时技术点能覆盖你所学专业的核心课程。租房数据分析系统恰好每一关都踩在点上数据来源真实。同城平台上每天都有大量公开的房源出租信息标题、价格、户型、面积、朝向、楼层、发布时间都是现成的不需要自己编数据也不需要漫长的数据采集审批流程这点在毕设里太重要了。技术栈覆盖面广。前端展示要用到Web框架Django数据获取要用到网络爬虫Requests库数据处理要用到Pandas和正则表达式结果呈现要对接ECharts可视化再往上有大模型接口调用。一门毕设同时串起爬虫—存储—清洗—分析—可视化—AI答辩时无论老师问到哪个方向你都有内容可讲。工作量可控制。核心闭环一个人在三到四周内完成是完全可行的。后面拿到充足时间后再去打磨细节、加亮点而不是一上来就做一个根本做不完的大系统。说白了这是一个起步容易、上限不低的题目不会出现两周做完了不知道干嘛的情况也不会因为技术选型太偏导致卡在某个环节出不来。1.2 系统的功能边界什么必须做什么加分做很多同学的问题不是不会做而是什么都想做。用户系统、预约看房、在线签约、App端……全都塞进去结果到了答辩前一周核心的数据分析页面还没跑通。所以一开始就要划清楚边界。必须做完的核心闭环只有一条爬虫定时采集房源公开信息 → 数据落库去重 → Pandas清洗与统计分析 → Django提供查询接口 → 前端ECharts渲染看板 → 展示区域均价、户型占比、价格分布、发布时间趋势做完以上这些毕设的保底工作量就已经达标了。在此基础上再考虑加分项大模型自然语言问答用户输入中心城区两居室均价多少由系统转换为查询条件并返回结果房源描述关键词词云简单的租金预测线性回归即可不要一上来就深度学习地图热力图展示租金分布我自己带过的项目里凡是没守住边界的最后都在熬夜补核心功能凡是先做完闭环再叠加亮点的不仅不慌往往还有时间优化界面。1.3 关于数据来源的合规要点爬虫类毕设最容易被答辩老师追问的就是合规性。这里给出我建议的处理方式把用途限定在个人学习与研究论文里也要写得清楚只采集页面公开展示的房源信息字段不采集联系方式、不抓取非公开内容控制请求频率做限速不要对目标站造成压力优先对接目标平台公开的数据接口即页面异步加载的JSON接口比直接解析HTML更稳定、负载更小数据展示时可以做匿名化处理例如对商圈信息做聚合而不是逐条列表展示把这些写进论文的数据获取说明章节答辩老师基本不会再追问。2. 技术选型Django、Requests、可视化的组合逻辑与替代方案2.1 Django在毕设场景里为什么是最优解Web框架三选一的问题几乎每届都有。Flask轻量灵活FastAPI靠异步性能出圈但论一个人短时间搭出完整可用系统Django的优势非常明显自带ORM。不用自己写SQL数据模型定义好之后迁移、建表一条龙。对于爬虫落库这种频繁建表改表的场景省掉大量重复劳动。自带Admin后台。房源数据表直接能在管理后台里看、筛、改答辩演示时打开Admin页面展示系统管理能力这比从零写一个后台管理页面快十倍。自带模板引擎。虽然现在流行前后端分离但毕设阶段用Django模板直接渲染页面再把ECharts嵌进去部署简单不容易出跨域问题。目录结构清晰。项目、应用、模型、视图天然分层论文里的系统架构图、模块设计图都很好画。版本选择上建议直接装最新LTS版本。不要用特别老的环境也不要追最新的非稳定版稳定优先。2.2 Requests为什么够用Scrapy什么时候才需要标题里明确写了Requests爬虫所以主推方案就是Requests。但很多同学会问老师是不是用Scrapy显得更专业我的回答是看数据量级和页面复杂程度。这里给个量化对比对比项Requests BeautifulSoupScrapy学习曲线低半天上手中高要理解爬虫框架、选择器、中间件机制性能单线程约每秒几到几十个请求够用异步并发吞吐量高一个量级反爬对抗手动处理Headers、Cookie、代理自带中间件扩展更容易代码量逻辑直观适合毕设展示工程化适合大规模批量采集调试难度直接在脚本里print容错简单需要熟悉框架日志和调试方式租房类平台的数据量级压测过也就几十万条Requests加一个线程池完全足够。而且毕设答辩时我的采集模块核心逻辑是什么这个问题用Requests的代码讲起来比Scrapy的配置项要清晰得多。反爬强度很高、需要分布式采集的场景才真正需要Scrapy。但毕设数据源可以选反爬相对温和的公开信息页面没必要给自己加难度。2.3 可视化方案怎么选可视化层我更推荐PyECharts原因很简单Python直接生成ECharts的HTML或JSON配置不用在前端手写大量JavaScript图表类型极其丰富柱状图、饼图、折线图、散点图、地图、词云都有与Django模板的结合非常顺滑把生成的HTML组件嵌入看板页面即可也有少数情况我会建议换方案方案优点缺点PyECharts与Python交互自然图表丰富需要理解图层/配置项生成逻辑原生ECharts 前端JS前端可控性最强要写不少setOption配置代码Plotly交互性强可缩放集成到Django多一步文件体积偏大后端渲染Matplotlib简单直接出图静态图片交互感弱答辩观感一般实际项目里看板主体用原生ECharts做因为要动态拉取接口数据快速报表和导出功能里用PyECharts生成两者互补。2.4 数据库SQLite起步不要上来就连MySQL不少同学觉得毕设必须用MySQL其实真的没必要。SQLite在几十万条数据量级的表现完全够用而且给答辩省了很多心零配置不需要额外安装数据库服务拷贝一个文件就能迁移演示现场不需要连接远程数据库永远不会出现连不上库的尴尬Django的ORM对SQLite、MySQL几乎无感代码层面不需要区别对待如果你的数据量真的到了百万级别或者有并发写入需求再考虑换MySQL。对这套系统来说采集脚本是单线程顺序写入查询也是低频统计SQLite的性能瓶颈远没到。3. 爬虫采集模块从页面接口分析到数据落库的完整链路3.1 先分析页面结构再写一行代码我见过太多同学直接CtrlU看HTML源码就开始写selector结果第二天目标页面一改版就全线崩溃。正确的第一步是弄清楚数据以什么形式存在。以同城租赁平台为例列表页表面上是一张一张的房源卡片但真正的数据结构往往藏在这些地方异步接口。页面滚动的房源数据大多是通过XHR请求从后端拿JSON浏览器地址栏不变但Network面板里有明显的POST/GET接口。找到这个接口直接请求它返回的JSON结构干净到几乎不用解析。页面源码中的初始数据。有些平台为加速首屏渲染会把第一批数据以JSON形式内嵌在HTML的script标签里。纯静态HTML。少量平台是真正的服务端渲染用BeautifulSoup即可。我的实践建议是先打开开发者工具看Network里的XHR请求优先走接口方案接口拿不到再退回去解析HTML。3.2 请求头与Session管理细节决定成败Requests爬虫被封号九成原因是请求头暴露了我是脚本的特征。最基础也最重要的伪装要做全套import requests import time import random HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://xx.example.com/zufang/, Origin: https://xx.example.com, } def build_session(): session requests.Session() session.headers.update(HEADERS) return session def polite_delay(min_seconds1.0, max_seconds2.5): time.sleep(random.uniform(min_seconds, max_seconds))这里有两个我踩过坑的细节一定先去浏览器里真实访问一次目标页面把请求头完整复制下来尤其是Referer和Origin。少了Referer有些接口会返回403。polite_delay的随机范围不要是整数比如固定time.sleep(2)这种节奏很容易被识别1.3到2.7秒之间的随机值实测稳得多。3.3 异步接口的解析与字段抽取假设平台通过接口返回如下结构的JSON字段做了泛化处理{ data: { list: [ { house_id: H123456, title: 中心城区 两室一厅 近地铁 精装修 拎包入住, price: 3200, unit: 元/月, area: 62, bedrooms: 2, livingrooms: 1, bathrooms: 1, orientation: 南, floor: 中楼层/共18层, district: 中心城区, pub_time: 2025-01-12 10:30:00, tags: [近地铁, 精装, 随时看房] } ] } }这种结构直接用requests.get.json()就能拿到解析代码反而简单def parse_list_item(item): try: return { house_id: item.get(house_id), title: item.get(title, ).strip(), price: float(item.get(price) or 0), area: float(item.get(area) or 0), bedrooms: int(item.get(bedrooms) or 0), livingrooms: int(item.get(livingrooms) or 0), orientation: item.get(orientation, ), floor: item.get(floor, ), district: item.get(district, ), pub_time: item.get(pub_time, ), tags: |.join(item.get(tags) or []) } except (TypeError, ValueError) as e: print(f解析失败{item}错误{e}) return None字段解析要写成独立函数不要散落在主流程里。这样做的好处非常直接如果平台改版导致字段名变化只需要改这一个函数。3.4 异常重试与断点续爬爬虫最难的不是能爬到而是挂了之后能恢复。我建议做一个轻量级重试装饰器import functools def retry_request(max_retries3): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except requests.RequestException as e: wait 2 ** attempt print(f第{attempt 1}次请求失败{e}{wait}秒后重试) time.sleep(wait) return None return wrapper return decorator retry_request(max_retries3) def fetch_page(session, url): resp session.get(url, timeout10) resp.raise_for_status() return resp断点续爬的另一个思路是记录已经入库的最大页码。每次采集前先从数据库里查一下已经爬到第几页下一次从该页继续而不是每次从第1页扫到尾。这样即使中途断掉重跑脚本的成本也很低。def get_start_page(): latest HouseInfo.objects.order_by(-crawl_page).values_list(crawl_page, flatTrue).first() return latest or 1数据落库时一定要做按唯一ID去重否则重跑一次脚本整张表就重复一遍。用Django的update_or_createfrom apps.rent.models import HouseInfo def save_house(item, crawl_page): if not item: return HouseInfo.objects.update_or_create( house_iditem[house_id], defaults{**item, crawl_page: crawl_page} )这里的house_id就是房源在平台上的唯一标识它既是天然去重键也是将来做增量更新的锚点。4. 数据清洗与分析把原始信息变成可展示的统计结果4.1 清洗标准不是所有数据都能进统计爬下来几万条数据直接算均价不行。你会发现结果被各种异常值污染0元帖子、1元钓鱼房源、面议价格、面积写错的记录。所以清洗是分析之前最不能跳过的一步。我建议的清洗规则价格字段先转成数字保留大于等于某个最低值比如500元/月且小于某最高值比如100000元/月的记录面积字段去掉形如40平但其实是4室0厅的异常只保留面积在10到500平米之间的记录发布时间把刚刚今天发布昨天3天前这类相对时间归一化为具体日期用于时间趋势分析标签字段把近地铁精装拎包入住南北通透等拆分成可统计的关键词清洗代码示例import pandas as pd def clean_house_data(df): df df.copy() df[price] pd.to_numeric(df[price], errorscoerce) df[area] pd.to_numeric(df[area], errorscoerce) df df[(df[price] 500) (df[price] 100000)] df df[(df[area] 10) (df[area] 500)] df df[df[pub_time].notna()] df df.drop_duplicates(subset[house_id], keepfirst) return df4.2 哪些分析维度最出效果数据分析不是把所有图表堆一遍而是有逻辑地回答几个问题哪个片区的租金最贵/最便宜→ 区域维度均价、中位数排序市场供应主力是什么→ 户型分布一居/两居/三居占比预算3000块能租到什么样的房子→ 价格区间与面积/户型的交叉统计租金最近是涨还是跌→ 发布时间与价格的关系按周/月聚合市中心租金真的高很多吗→ 区域绑定面积、价格的散点对比用Pandas做聚合代码非常直观def district_price_summary(df): result ( df.groupby(district) .agg( house_count(house_id, count), avg_price(price, mean), median_price(price, median), avg_area(area, mean), ) .reset_index() .sort_values(avg_price, ascendingFalse) ) return result注意用中位数而不是平均数能有效避开极端值干扰。我实际对比过几个超高价房源能把区域均价拉高20%以上这对决策参考没意义。4.3 让统计结果直接变成接口数据分析完成后直接把DataFrame转成JSON再让Django视图读取前端就不用再做任何计算了import json def analysis_to_json(result_df): return json.loads(result_df.to_json(orientrecords, force_asciiFalse))这样做的另一个好处是答辩时可以清晰地讲统计分析在采集后异步完成结果以JSON缓存下来接口只负责读取。这个链路描述本身就很有工程感。5. Django可视化集成看板页面的接口设计与图表联动5.1 Django项目骨架与配置我习惯把功能拆成若干个app而不是把所有逻辑塞进一个views.pyrent_analysis/ # 项目配置目录 settings.py urls.py apps/ crawler/ # 爬虫与数据入库 analyzer/ # 数据清洗与统计分析 dashboard/ # 看板页面与API接口关键配置就两条# settings.py INSTALLED_APPS [ # ... apps.crawler, apps.analyzer, apps.dashboard, ] STATIC_URL /static/ECharts的JS文件直接放在static/js/下模板里用{% load static %}引入一切本地化答辩部署不需要外网。5.2 API接口设计一份JSON对应一张图看板页我设计成一个页面、四张图、两张统计卡片对应的接口如下接口路径返回内容用途/api/overview/总房源数、均价、面积均值、最新更新时间页面顶部统计卡片/api/district/各片区房源量、均价、中位数区域租金柱状图/api/floorplan/一居/两居/三居房源数与均价户型构成饼图/api/price_dist/价格区间与房源数量直方图价格分布图/api/trend/按周聚合的房源量与均价时序发布趋势折线图视图代码很简洁核心是把之前算好的结果返回from django.http import JsonResponse from apps.analyzer.services import load_cached_analysis def district_price(request): data load_cached_analysis(district_summary) return JsonResponse({code: 0, data: data})用load_cached_analysis读预先计算好的JSON而不是每次请求都现场算一遍。这样页面打开速度、接口稳定性都大幅提升。5.3 看板模板与ECharts联动前端不需要什么框架Django模板加原生JS就够。核心逻辑是页面加载时用fetch请求接口拿到JSON后调用setOption渲染图表。!-- dashboard/templates/dashboard/index.html -- div iddistrict-chart stylewidth: 100%; height: 400px;/div script src{% static js/echarts.min.js %}/script script fetch(/api/district/) .then((resp) resp.json()) .then((res) { const chart echarts.init(document.getElementById(district-chart)); chart.setOption({ title: { text: 各片区房源量与均价 }, tooltip: { trigger: axis }, xAxis: { type: category, data: res.data.map((d) d.district) }, yAxis: [{ type: value, name: 均价(元/月) }], series: [ { name: 均价, type: bar, data: res.data.map((d) d.avg_price), itemStyle: { color: #3b82f6 }, }, ], }); }); /script四张图的代码结构完全一致只有chart的DOM id和setOption的配置不同。把公共部分抽成一个函数重复代码量立刻降下来。5.4 Admin后台的加分操作Django自带Admin不能浪费。注册模型、配置列表展示和筛选器答辩时演示系统后台可对房源数据进行管理和导出非常加分# apps/crawler/admin.py from django.contrib import admin from .models import HouseInfo admin.register(HouseInfo) class HouseInfoAdmin(admin.ModelAdmin): list_display (house_id, title, district, price, area, pub_time) list_filter (district, bedrooms, orientation) search_fields (title, house_id) actions [export_to_csv] admin.action(description导出选中房源为CSV) def export_to_csv(self, request, queryset): import csv from django.http import HttpResponse response HttpResponse(content_typetext/csv) response[Content-Disposition] attachment; filenamehouses.csv writer csv.writer(response) writer.writerow([house_id, title, price, area, district, pub_time]) for obj in queryset: writer.writerow([obj.house_id, obj.title, obj.price, obj.area, obj.district, obj.pub_time]) return response不要小看这几行它的作用是把系统从能看图表升级为能管理数据评委的直观印象完全不同。6. 答辩实战与高频踩坑给开题和赶工的同学的提醒6.1 答辩老师最常问的四个问题根据我多年参与毕设评审的经验关于这个系统高频问题非常集中问题我的建议回答方向数据怎么保证时效性设计了定时采集脚本可配置时间间隔自动增量抓取目标平台改版怎么办解析逻辑集中在parse模块改版时只需更新一处为什么不用别人准备好的数据集数据集没有时效性无法体现从采集到展示的完整工程能力爬虫被限制访问怎么办限速、重试、切换请求头、单机最大采集量控制均已有实现答辩本质上是在验证这活儿是不是你自己干的、你懂不懂细节以上回答都基于真实实现细节底气就足。6.2 我实际踩过的四个坑第一个坑是请求头缺Referer导致403。第一次写的时候只加了User-Agent结果请求首页能通换到列表接口立刻403。后来发现平台会校验来源页补上Referer后马上恢复正常。所以第一版代码的请求头一定要从浏览器完整复制。第二个坑是页面异步加载。最初用requests直接请求列表页HTML拿到的是一个带动态参数的壳子房源数据根本不在里面。排查半天才看到Network里的XHR请求。建议做这个项目时第一件事就是打开开发者工具确认数据在源码还是在接口能省一天时间。第三个坑是返回的JSON编码问题。某些接口返回的是\u转义格式.json()能正常解析但如果你先做resp.text再手动json.loads中文很容易乱码。统一用resp.json()不乱来。第四个坑是Windows下控制台打印中文乱码。爬虫脚本在IDLE或CMD里打印日志时经常出现乱码不影响数据入库但影响调试效率。建议脚本开头加sys.stdout.reconfigure(encodingutf-8)或者在日志输出时统一对字符串做编码转换。6.3 大模型部分怎么落地才不翻车标题里有大模型答辩评委大概率会问。但我不建议在毕设里做很重的模型训练或推理成本和稳定性的风险都太高。更稳妥的方案是大模型作为自然语言查询入口底层实际通过规则或统计接口执行。具体思路是用户在页面上输入帮我统计一下中心城区两居室的平均租金系统先做意图匹配识别中心城区两居室平均租金三个要素再调用对应的统计接口返回结果。需要调大模型时只把用户的原始问题发送给通用模型接口让它抽取结构化查询参数拿到参数后再走本地统计逻辑。这样即使模型接口偶尔超时系统核心功能依然可用。# 伪代码示意大模型抽取查询条件 def parse_query_with_llm(user_text): # 调用通用大模型接口 schema llm_extract(user_text, fields[district, bedrooms, metric]) # 本地兜底没识别到就返回空 return schema or {}这个设计的巧妙之处在于对外叙事是基于大模型的智能查询对内实现是大模型规则双保险论文里能写演示时也不会卡壳。6.4 后续还能怎么扩展如果答辩之后还有精力或者想把这个项目作为找工作的项目经历我建议按下面顺序扩展地图热力图把各片区的均价渲染到底图直观呈现哪贵哪便宜每日定时任务用系统定时任务每天跑一次采集积累更长时间序列做租金走势预测数据导出报告一键生成PDF或Excel形式的区域租金报告方便普通用户直接使用前后端分离升级把Django改成纯API后端前端换Vue这套接口设计直接复用项目体感立刻上了一个档次实际带完几轮这个项目之后我最大的体会是毕业设计不是比谁技术花活多而是比谁把一件完整的事情做扎实。租房数据分析系统恰好是一件小但完整的事从爬虫到分析到展示到AI每一步都有明确产出。如果你正卡在选题或中期不知道怎么往下走从这篇里的最小闭环开始做。先把四张图跑出来后面每一步都是在给这个地基加分。