资讯详情 同城房产数据分析预测系统:爬虫、机器学习与Web可视化全栈实践
📅 2026/10/10 3:45:10
做毕业设计选题时如果不想从零造轮子“Python爬虫 数据分析 机器学习 Web展示”串起来的完整系统是个很聪明的选择。今天分享的这套同城房产数据分析预测系统就是一条我实测跑通的全链路项目requests负责抓取同城分类信息平台的房源数据清洗入库后由Flask提供后端接口ECharts在页面上做可视化大屏scikit-learn训练房价预测模型最后还能接一个大模型问答入口。整个项目既有实际数据流又有算法内容也有前端展示非常适合计算机科学与技术、软件工程、数据科学等方向的同学作为毕业设计参考也适合想做全栈练手项目的开发者。如果你正在找题目或者题目已经定了但不知道代码怎么组织这篇文章就把我的设计思路、实现步骤、踩坑记录一次性讲清楚。下面每个环节我都会说“为什么这么选”和“实际怎么做”而不是只丢一堆代码。1. 项目整体设计与技术选型1.1 系统要解决的几个核心问题毕设最忌讳的是“看起来什么都有细问全不会”。这套系统之所以选房产数据是因为数据获取、结构化、分析和预测四个环节都能出成果。我需要先拆任务第一数据从哪来。没有数据后面的模型全是空中楼阁。同城分类信息平台上公开的房源列表页和详情页可以用requests抓取但要解决好请求频率、反爬和解析规则。第二数据怎么存。抓下来的字段有标题、区域、户型、面积、朝向、楼层、总价、单价、发布时间等字段长短不一需要清洗后入库。第三数据怎么展示。老师打开系统第一眼看到的往往是可视化页面这决定了第一印象。第四数据怎么预测。用面积、户型等特征训练一个房价预测模型才符合“预测系统”这四个字。把这四件事拆开每一块都能单独写进论文的章节里答辩的时候也容易讲清楚。我更建议你在动手前把系统边界画出来先做信息展示再做数据分析最后加预测模型和智能问答。这样即便时间不够前面的成果也能完整展示。1.2 技术栈为什么这么选标题里已经给出了基本答案Flask、scikit-learn、requests、可视化这是比较成熟稳定的组合。我在选型时对比过Django和FlaskDjango自带Admin和ORM功能强但毕设项目往往只有两三个页面用Flask更轻路由和启动逻辑一眼能看懂。requests比urllib好用太多配合BeautifulSoup做HTML解析抓静态页面足够。scikit-learn是机器学习库里的老牌选择线性回归、随机森林、评估指标都很齐全训练代码量少效果容易解释。可视化部分我选ECharts而不是Matplotlib是因为ECharts在浏览器里渲染交互图表表格、Tooltip、缩放都现成毕业设计演示时很加分。存储先用SQLite文件型数据库不用单独装服务适合小项目如果数据量真的达到几十万条我可以平滑迁移到MySQL。至于大模型它不是核心功能而是加分亮点可以采用通用大模型API或开源模型在后面单独说明。1.3 系统架构与数据流向系统我按四层来组织采集层、存储层、服务层、展示层。采集层由爬虫脚本负责定时或手动运行抓取房源列表页、详情页解析成结构化数据。存储层用SQLite保存房源信息同时导出特征数据给模型。服务层是Flask应用对外提供两类接口一类是数据分析接口返回区域均价、户型分布等JSON数据另一类是预测接口接收面积、户型等输入返回预测价格。展示层是H5页面用Jinja2模板加载数据ECharts绘制图表页面里还有一个问答输入框通过后端代理调用大模型接口。数据流向很清晰爬虫 → 清洗 → SQLite → Flask → 前端/模型。我建议把所有采集、清洗代码放到spider/目录Flask应用放到app/目录模型代码放到model/目录这样论文里画架构图、写模块划分时更方便。不要把代码全堆在一个文件里后期维护会非常痛苦。2. 数据采集与预处理2.1 requests爬虫的注意事项与反爬策略爬虫是整套系统的数据入口最容易在第一步卡住。先用requests发送HTTP请求拿到HTML之后用BeautifulSoup定位房源节点。我做了一个比较通用的采集框架import requests import time from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36 } def fetch_html(url, paramsNone): resp requests.get(url, paramsparams, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text def parse_list(html): soup BeautifulSoup(html, html.parser) items [] for block in soup.select(div.house-list div.item): title block.select_one(.title).text.strip() price block.select_one(.price).text.strip() area_text block.select_one(.area).text.strip() items.append({title: title, price: price, area: area_text}) return items all_data [] for page in range(1, 11): html fetch_html(https://example.com/fangyuan, {page: page}) all_data.extend(parse_list(html)) time.sleep(2)这里有几个关键点。第一是User-Agent必须设置成浏览器常见值很多服务端会拦截裸Python请求。第二是解析规则不要写死先用浏览器开发者工具确认节点层级再用select_one抓取一旦页面改版要能快速定位到代码位置。第三是请求频率很多同城平台并没有严格的封锁机制但为了让项目长期可跑我控制在每两秒一个请求同时加异常重试def fetch_with_retry(url, retries3): for i in range(retries): try: return fetch_html(url) except requests.RequestException as e: time.sleep(5 * (i 1)) raise RuntimeError(f抓取失败: {url})爬虫代码写得规范一点后面写论文时还可以单列一节讲“反爬策略”。需要提醒的是做毕设采集必须在平台允许的范围内进行控制频率、不抓个人隐私信息、不用于商业用途。如果遇到登录墙或验证码最简单的处理是换一个不需要登录的公开列表入口而不是花大量时间对抗验证码毕设的时间应该花在分析上。2.2 数据清洗与字段标准化抓下来的原始字段很乱比如价格是“3000元/月”和“200万”混在一起面积写成“89.5㎡”或“89.5平”。我统一用正则把数字提取出来再根据文本中包含的关键词判断单位是“元/月”还是“万元”。对于户型、朝向这种枚举字段设计成标准映射表。import re def parse_price(text): text text.replace(, ).replace(,, ) num re.search(r(\d\.?\d*), text) if not num: return None value float(num.group(1)) if 万 in text: return value * 10000 if 元 in text: return value return value def parse_area(text): m re.search(r([\d.])\s*(㎡|平米|平)?, text) return float(m.group(1)) if m else None清洗之后还要处理缺失值。面积、总价这种核心字段缺失的记录我通常直接删除因为它们是模型特征。朝向、装修状态这种非关键字段用“未知”填充。价格单位不统一的要统一成“元”或“万元”否则后面做图时X轴全是乱数字。我还会用pandas做一次批量处理把抓下来的list转成DataFrame再用apply逐列清洗。这样清洗逻辑更清晰后面导出成CSV也方便。实际做的时候我建议把清洗后的数据单独存一份clean_data.csv方便反复查看也方便后面模型训练和可视化调试。2.3 数据入库SQLite还是MySQL毕设项目我默认建议SQLite。它就是一个文件不需要安装数据库服务程序启动时自动连接写完就能跑。建表语句类似这样CREATE TABLE IF NOT EXISTS house ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, district TEXT, area REAL, layout TEXT, orientation TEXT, floor TEXT, total_price REAL, unit_price REAL, publish_time TEXT, url TEXT UNIQUE );为什么要设置url为唯一键因为爬虫重复运行时同一套房源可能出现多次我用INSERT OR REPLACE来去重。如果后期数据量大到几十万条再迁移MySQL或PostgreSQLFlask的SQLAlchemy可以帮忙做这层适配。不过毕设答辩时SQLite通常足够老师更关心你会不会设计表结构而不是数据库本身多大。表设计还有一个容易被忽略的点把unit_price单独作为字段不要只存总面积和总价。因为很多分析场景要按单价排序单独存一秒就能查出来不用每次在SQL里做除法。我最初就是没存单价后来可视化区域均价时才发现要重新算白跑了一轮清洗流程。3. 数据分析与可视化3.1 可视化大屏要展示哪些指标可视化不是越花哨越好而是要让老师一眼看出“系统有价值”。我设计了大屏的四个核心模块区域平均单价排行、户型占比分布、面积-总价散点图、近30天新增房源趋势。第一模块能回答“哪个区域房子最贵”第二模块能回答“市场主力户型是什么”第三模块能看出面积和价格是否呈线性关系第四模块能展示数据采集的“新鲜度”。如果只做一个图表区域均价TOP10的柱状图最实用信息密度高演示效果也好。每个图表在页面上放一个卡片顶部是KPI卡片显示总房源数、最高单价、平均单价、中位数总价。我建议先画页面草图再确定接口字段避免前端写一半发现后端字段不够。我个人用这种方式整个可视化开发时间压缩到两天左右。这里要注意指标口径统一单价是“元/㎡”总价是“万元”中位数和平均数要分别标注。很多同学把平均价和中位数混在一起答辩时被问“为什么区域均价和中位数差这么多”就答不上来。其实这正好体现你对数据的理解可以在论文里写“区域内房源价格长尾分布明显中位数比均值更能代表普通房源水平”。3.2 Flask后端接口设计与前端图表联动Flask部分我采用蓝图组织路由先写API接口再写前端页面。核心接口可以分成两类一类是全局统计接口另一类是筛选接口。示例from flask import Blueprint, jsonify, request from db import query_all api Blueprint(api, __name__) api.route(/api/summary) def summary(): rows query_all(SELECT COUNT(*) AS total, AVG(unit_price) AS avg_price, MAX(unit_price) AS max_price FROM house) return jsonify({code: 0, data: rows[0]}) api.route(/api/district) def district(): rows query_all( SELECT district, COUNT(*) AS cnt, ROUND(AVG(unit_price), 2) AS avg_price FROM house GROUP BY district ORDER BY avg_price DESC ) return jsonify({code: 0, data: rows})前端用ECharts时先用fetch(/api/district)拿到数据再写到option.series[0].data里。需要注意接口统一返回{code: 0, data: ...}这样的结构方便前端做全局错误处理。表格里如果包含中文字段接口要确保返回的是UTF-8Flask的jsonify本身会处理。开发时我一般把Flask的调试模式打开同时允许跨域请求设置。如果前端是独立端口调试可以用flask-cors解决跨域如果直接放在Flask的templates目录里就不存在跨域问题。我建议毕设直接用Flask托管页面和API减少一个调试变量。3.3 大数据量下的加载优化虽然毕设数据量一般只有几千条但论文里要体现“大数据量”场景下的优化思路。我做了三个优化一是数据库查询使用聚合函数不要把全部原始数据拉到内存再统计二是前端图表开启dataZoom让柱状图可以滚动查看三是给常用查询字段加索引。如果需要缓存热点接口可以在Flask里加一道简单的内存缓存cache {} def cached_query(key, expire30): if key in cache and time.time() - cache[key][ts] expire: return cache[key][data] data run_sql(key) cache[key] {ts: time.time(), data: data} return data这套缓存逻辑虽然简单但足以应对演示时的并发刷新。真要处理更大的数据还可以引入Redis但这就超出大多数毕设需求了。我还会在前端加一个“最后更新时间”字段让老师知道数据不是写死的而是爬虫抓取后更新进系统的。这个细节在演示时很加分。4. 房价预测模型搭建4.1 特征工程与数据划分房价预测不是把原始数据直接扔进模型就行。我选择的特征是面积、户型、朝向、楼层、所在区域、房龄。其中面积是数值特征直接使用户型、朝向、区域是类别特征需要编码。最常踩的坑是直接把字符串丢给scikit-learn会直接报错。我用OneHotEncoder做独热编码并用ColumnTransformer统一处理数值和类别列减少代码错误。from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder, StandardScaler num_features [area, age] cat_features [layout, orientation, district] preprocessor ColumnTransformer([ (num, StandardScaler(), num_features), (cat, OneHotEncoder(handle_unknownignore), cat_features) ])数据划分也要注意应该先清洗、再编码、最后划分训练集和测试集顺序不能反。如果先划分再编码测试集信息会泄漏到训练集模型的评估指标会虚高。我习惯用train_test_split(X, y, test_size0.2, random_state42)固定随机种子后每次运行结果都一样写论文时数据可复现。房龄这个特征可以手动计算。如果详情页里有“竣工时间”就用当前年份减去竣工时间如果没有可以从房源描述里用正则匹配“XX年建”。实在补不上就把房龄设为“未知”单独作为一个类别。宁可缺失值多也不要用全局平均值填充那样模型会低估真实误差。4.2 scikit-learn模型选择与训练我用线性回归作为基线模型再用随机森林做对比。线性回归的优点是解释性强系数能说明每个特征对房价的影响随机森林的优点是能捕捉非线性关系。训练代码很简洁from sklearn.ensemble import RandomForestRegressor from sklearn.pipeline import Pipeline model Pipeline([ (preprocess, preprocessor), (regressor, RandomForestRegressor(n_estimators200, random_state42)) ]) model.fit(X_train, y_train) train_score model.score(X_train, y_train) test_score model.score(X_test, y_test)这里我特别建议先用train_test_split把数据按8:2切分并设置random_state固定随机种子确保每次运行结果一致。随机森林的n_estimators从100起步如果训练集得分很高但测试集得分很低就要考虑是不是过拟合可以通过降低树的深度或缩减特征来控制。训练完之后我会做一次特征重要性分析把随机森林的feature_importances_输出画成柱状图。这一步对论文和答辩特别有用能直观说明“面积影响最大”还是“区域影响最大”。比如我某次跑下来区域特征的重要性比面积还高说明样本里不同区域的价格差异很大这在论文里可以写成“房地产市场的区域性决定了房价建模必须优先考虑位置因素”。4.3 模型评估与预测结果的落地模型不能只拿准确率说事回归任务我打印三个指标R2、平均绝对误差MAE、均方根误差RMSE。R2接近1代表模型解释能力强RMSE的单位和真实房价相同容易向老师解释“平均误差在多少元”。from sklearn.metrics import r2_score, mean_absolute_error, mean_squared_error y_pred model.predict(X_test) print(R2:, r2_score(y_test, y_pred)) print(MAE:, mean_absolute_error(y_test, y_pred)) print(RMSE:, mean_squared_error(y_test, y_pred, squaredFalse))为了让预测功能真正落地我把训练好的模型保存为model.joblib然后在Flask里写一个预测接口import joblib model joblib.load(model.joblib) api.route(/api/predict, methods[POST]) def predict(): data request.get_json() x preprocess_input(data) price model.predict([x])[0] return jsonify({code: 0, predict_price: round(float(price), 2)})前端做一个表单让用户输入面积、户型、区域点击按钮就显示预测价格。这一块演示效果非常直观。我还建议加一个“真实房源对比”功能从数据库里随机取同区域、同户型的几套房源把模型预测价和真实挂牌价放在一起让老师看到预测值落在合理范围内这比干巴巴的指标更有说服力。5. 大模型问答模块从噱头到落地5.1 为什么要在房产系统里接入大模型近两年毕业设计如果只会传统机器学习已经不容易出彩。标题里有“大模型”很多同学就硬凑。我把它定位成“基于房产数据的智能问答助手”比如用户问“XX区域和XX区域哪个均价更高”“预算300万有哪些三居室推荐”系统结合数据库查询结果再调用大模型生成回答。这样既体现大模型能力又没有脱离房产场景。如果只是简单接一个聊天框答辩时很容易被问“和业务有什么关系”。我自己实际做下来发现这个模块最花时间的不是调用接口而是设计“该问什么、给大模型什么上下文”。如果只把用户问题原样甩给大模型它会一本正经地编造房源信息。我的做法是先让Flask从数据库查询出真实聚合数据再拼到Prompt里让大模型只基于给定数据作答。这样回答既自然又不会脱离真实业务。5.2 大模型模块的三种接入方式我整理过三种方案。第一种是调用通用大模型API开发量最小但依赖外部网络演示时要保证网络可用。第二种是本地部署开源大模型离线可用但需要显存或内存资源普通笔记本跑小参数模型还可以。第三种是用向量数据库存房源描述文本做基于知识库的问答效果更可控但代码量更大。毕设优先推荐第一种或第三种。调用API时要注意不要在服务端硬编码密钥可以放在环境变量里。我用一个表格对比过三种方式的优缺点写论文时可以直接用接入方式优点缺点适用场景通用大模型API开发快、效果稳定依赖外部网络、有调用成本毕设演示、快速原型本地开源大模型完全离线、可自定义需要资源、部署复杂服务器部署、数据敏感场景向量数据库大模型回答内容可控、可溯源工程量大、需要预处理想做深度应用、加分项选型时要考虑自己的机器配置和时间。如果只剩一周直接选第一种重点是让回答和数据库联动起来如果时间充裕可以尝试第三种把房源文本切片、向量化、检索再生成。我最终选择了第一种因为论文答辩场景下稳定性和演示效果更重要。5.3 与Flask系统的集成实践集成方式不复杂前端问答表单把问题POST到Flask路由Flask先从数据库查出相关数据作为上下文再把问题和上下文一起发到大模型接口最后把返回文本渲染到前端。示例api.route(/api/ask, methods[POST]) def ask(): question request.get_json().get(question, ) context query_summary_by_keywords(question) prompt f根据以下房产数据回答问题\n{context}\n问题{question} answer call_llm(prompt) return jsonify({code: 0, answer: answer})call_llm这里可以用你选好的大模型SDK或HTTP接口。核心是Prompt设计我把数据库查询结果压缩成几行摘要再让大模型做总结和推荐这样不会出现大模型乱编房源信息的情况。我也建议在回答下方展示“已根据当前房源数据生成”字样明确数据边界。前端可以做成对话框形式输入问题后调用/api/ask把返回的answer显示在聊天气泡里。如果大模型接口支持流式输出可以在前端用fetch的ReadableStream逐字显示体验会更好。但流式输出会增大前端代码量毕设阶段可以先不做直接等完整回答返回后再渲染效果完全能接受。这个模块真正能拉开差距的地方在于你能否把“数据库查询 大模型生成”这个链路讲清楚。6. 常见问题与避坑指南6.1 爬虫被封、数据不全怎么办被封最常见的原因是请求频率太快或User-Agent太明显。应对方法有三层第一每次请求间隔1到3秒第二维护一个User-Agent池随机切换第三如果发现连续报错不要硬试停止脚本并换一个时间段再采集。数据不全时优先保证核心字段完整。我真的遇到过某一天平台改版列表页结构全变导致解析不到任何数据解决办法是把HTML保存到本地先分析结构再改选择器不要盲目改代码。我还会给爬虫加一个“进度日志”每抓完一页就打印当前页码和累计条数。这样万一中断能知道抓到第几页下次从断点继续不用从头开始。日志写到文件里截图后也可以放进论文附录证明你确实做了采集实验。6.2 中文乱码与编码问题这是Python爬虫最常见的问题之一。requests拿到响应后如果页面没有声明编码直接用resp.text可能显示乱码。我的做法是resp.encoding resp.apparent_encoding如果还乱码就用resp.content.decode(utf-8, errorsignore)强制解码。数据库和Flask层面要统一用UTF-8CSV导出时也要加encodingutf-8-sig否则Excel打开中文会乱。数据库连接字符串里也要指定字符集比如SQLite是UTF-8MySQL连接时可以加charsetutf8mb4。6.3 模型过拟合和欠拟合怎么调毕设里很多同学一上来就想用随机森林最后发现测试集和训练集分数差距过大。先做基线线性回归如果线性回归表现很差说明关系非线性再上随机森林或梯度提升。调节方向可以从数据量、特征数、模型复杂度三个角度切入数据量少就再爬一些特征太杂就删掉相关性低的模型太复杂就把max_depth调小。最重要的是把所有实验记录在表格里答辩时可以展示“调参过程”。我用一个简单表格记录实验模型名称、特征组合、训练R2、测试R2、RMSE、备注。这样老师问“你试过哪些模型”时你可以直接展示这张表。记录本身不占用多少时间但对论文“实验与结果”章节帮助非常大。6.4 演示部署时的四个大坑演示当天最容易翻车的不是模型而是环境。第一绝对不要现场演示爬虫提前把数据抓下来存好现场只跑分析。第二开发机端口被占用启动Flask时换一个固定端口如python app.py里写app.run(port8000)。第三静态文件和模板路径不能写绝对路径用Flask默认的static和templates目录。第四模型文件比较大提交代码时不要忘了打包model.joblib和house.db我见过太多同学代码交了模型文件没交演示时预测接口直接报错。还有一个小细节如果要打包发给导师或评委最好写一个requirements.txt把Flask、requests、scikit-learn、pandas、joblib等依赖版本固定住。别人拿到手pip install -r requirements.txt就能跑这是最基本的交付素养。最后再多说一句。这套项目我前后改过三轮最大的体会不是代码写得多么花哨而是把数据链路跑通之后再回头优化模型。如果你也准备做类似题目建议先把爬虫、Flask、可视化这条主线做通让系统先“活”起来再逐步加预测模型和大模型问答。答辩时老师更看重的不是用了多新鲜的模型而是你能不能讲清楚每个环节为什么这么做数据从哪来误差有多少遇到问题怎么解决。这些实践经验才是毕业设计最有价值的部分。希望这篇记录能帮正在做选题的你少踩几个坑。