☰
Python商品房数据采集预测系统:爬虫+机器学习+Flask实战
2026/10/8 15:14:15 网站建设 项目流程

又是一年毕业设计季,我后台已经收到不少同学问同一个问题:毕设到底选什么题,才能既容易过审、又能在答辩时讲出东西来。我给的建议通常很朴素——把一个常见业务场景完整跑通,比跟风追新概念更稳。比如这套《Python商品房数据采集预测系统》,单看名字不算花哨,可当你把Requests爬虫、数据分析、Scikit-learn机器学习、Flask可视化这条链路从头到尾走一遍后,你会发现它其实已经覆盖了计算机专业毕业设计最拿分的几个环节:工程能力、算法落地、软件工程的完整闭环。

这套系统的定位很清晰:抓取真实的商品房挂牌数据,清洗整理后做特征分析,再用机器学习模型对房价做出预测,最后通过Flask搭建Web界面把结果可视化展示出来。它能解决的问题也很实在——对购房者来说是“这个区域的房子大概值多少钱”的参考,对学生来说是“我有没有能力独立完成一个可供答辩演示的数据类项目”。这篇文章会把选题思路、系统架构、关键代码、踩坑实录全部拆开讲一遍,无论你是零基础起步还是已经学过Python想做点综合项目,都可以直接照着搭。

1. 选题思路与系统架构怎么定

1.1 为什么说“房价预测”是毕设的稳牌

很多同学选题时容易犯一个毛病,就是把题目定得又大又空,比如“基于人工智能的智慧城市系统”,听上去很唬人,可落实到代码上根本不知道第一步该写什么。房价预测这个题目好就好在它业务边界清晰、数据容易获取、技术栈也完全能对上课程要求。

从课程考核角度看,数据结构课讲了列表和字典,数据库课讲了SQL,机器学习课讲了回归和集成学习,Web开发课讲了MVC——这套系统正好把这些知识点全部串起来。从答辩角度看,评委最关心的是“你有没有真的动手做”,而这类数据采集预测系统天然能看到实物:有爬虫抓取的数据、有可视化的图表、有预测结果的误差评估,每一环都可以现场演示,比空谈理论强得多。

还有一个特别实际的好处:房价数据每天都在更新,你做的不是死数据,你可以随时重新爬取、重新训练,答辩时就算评委问“如果数据换成别的城市怎么办”,你也可以直接现场换参数演示。这种“活”的系统很加分。

1.2 技术栈选型背后的取舍

技术选型是我想重点聊的部分。很多初学者会问:“为什么不用Scrapy?为什么不用Django?为什么不用TensorFlow?”这些问题背后其实是对工具边界不理解。以毕设场景来看,选择标准只有三条:你讲得清楚原理、你驾驭得住代码、你三天内能调通。

爬虫层我用的是Requests,而不是Scrapy。原因很直接:Scrapy虽然性能更高、扩展性更强,但它的异步框架和Item Pipeline机制对新手来说太抽象了。Requests配合BeautifulSoup解析HTML,代码是直线式的,读起来人挑人,答辩时解释起来也顺。

Web框架选了Flask而不是Django。Django自带的Admin后台、ORM和中间件体系很强大,但一个项目里往往有大量你并不熟悉的“自动配置”,出了问题你很难排查。Flask的请求-响应模型非常直观,可以一个路由一页代码、一个函数一个接口地讲清楚,部署到云服务器还很简单。

机器学习部分用Scikit-learn而不是深度学习框架,这是因为房价预测是表格型数据的回归任务,随机森林或梯度提升这类传统模型在中小样本上的表现和可解释性都要优于神经网络。而且Sklearn接口统一,fit和predict就是两个核心调用,答辩时不会卡壳。

在前端方面,ECharts几乎是可视化场景的最优解,图表类型全,配置项文档友好,动态交互效果也很容易出彩,不需要额外学习React或Vue那一套工具链。

2. 系统核心功能模块拆解

2.1 数据采集模块:先理解网页再写爬虫

我以为写爬虫最大的坎不是代码本身,而是你怎么看待“页面数据”。很多同学一上来就翻审查元素里的XHR,试图找接口、拼参数,结果绕了一大圈什么都抓到。更稳妥的方式是先用最简单的方式把网页抓下来,看看结构再说。

以链家、安居客这类房产信息站为例,数据通常有两种存在方式:一种是服务端渲染的HTML,数据直接嵌在网页标签里;另一种是前端异步加载,数据藏在XHR接口返回的JSON里。判断方式很简单:你用Requests抓一下页面源码,搜索楼盘名称、面积、单价这些关键词,如果搜得到就直接用BeautifulSoup解析;搜不到再去看Network面板里的接口。

具体到系统里,爬虫模块我拆成了四个小步骤:构造请求(带上合适的请求头)、发送GET请求拿到HTML、用BeautifulSoup定位标签提取字段、把结构化结果写入数据库。这四步逻辑清晰、职责单一,写代码时好调试,写毕业设计文档时也好拆章节。

这里要特别提醒:抓取公开数据时一定要控制频率和规模,把请求间隔设在1到3秒,不要对目标服务带来压力,不要绕过登录和访问控制,也不要用抓下来数据做商业用途。如果目标网站明确不允许爬虫,就换一个公开的数据源来练习。

2.2 数据清洗模块:脏数据才是真正的敌人

我在第一次做这个项目时也天真地以为,能把数据爬下来就万事大吉了。结果一看数据表:挂牌标题里有各种无用的“急售”“捡漏”,面积字段有些是“88平米”有些是“88㎡”,单价还有缺省值,楼层字段格式混乱。

清洗是整个项目里最无聊又最关键的一环。用Pandas做得很顺:先去除重复记录,再把字符串类型的面积用正则提取数字部分并转成浮点数,单价字段也做同样处理,最后用中位数填补缺失值少的列。对于诸如朝向、装修这样的分类特征,则用pd.get_dummies做OneHot编码,让机器学习模型能够处理。

很多同学会在这一步偷懒,直接把原始数据扔给模型训练,结果发现R2只有零点几,却找不到原因。后来逐一检查特征才发现,总价字段单位不统一,从“万”变成“万元”,导致模型上下颠倒。数据质量决定了模型性能的上限,算法只是逼近这个上限,这句话我自己做项目时感受很深。

2.3 预测模块:模型选择与特征工程的关系

房价预测本质上是一个回归任务,目标值是一个连续变量——每平方米单价。在Scikit-learn里面,最常用的两个模型是线性回归和随机森林回归。线性回归的优势是可解释性极强,它给出来的系数可以直接说明“面积每增加一平,房价大约上涨多少”;随机森林的优势则是能够捕获非线性关系,比如同一个区域里小户型单价明显高于大户型这种情况。

我建议毕设里至少训练两个模型做对比,这不只是为了让论文内容好看,更重要的是让你真正理解不同算法的差异。参数方面,随机森林里最值得做的是调n_estimators(树的数量)和max_depth(树的最大深度),这两个参数不宜过大,建议在50到200之间搜索,过大反而会过拟合。用train_test_split把数据划分成80%训练集和20%测试集,再用R2、MAE两个指标评估模型效果。

特征工程的细节也影响很大。户型里“3室2厅”这种字符串不能直接进模型,要拆成“室数”和“厅数”两个数值特征;区域要用经纬度或行政区划编码表示;房龄越老、楼层越高的房屋,对单价的影响往往都不是线性的,这些放到模型里跑一跑才知道。

2.4 展示模块:Flask让整个系统有了“脸面”

命令行里打印一堆预测数字,没人觉得你做的是个系统。Flask展示模块存在的意义,就是让你的项目变成一个“打开浏览器就能操作”的产品。

我的做法是用Flask提供三个核心路由:首页展示房源总览和区域均价柱状图,分析页展示面积/单价散点图和户型分布饼图,预测页提供一个表单,用户输入面积、户型、楼层、朝向等特征,点击提交后后台调用sklearn模型返回预测价格。

这些图表全部用ECharts渲染,Flask这边只负责把数据组装成JSON格式返回给前端。这种“前后端数据分离”的做法你在本科阶段可能不觉得有多高级,但它确实是以后工作中最常用的模式。而且答辩时你可以现场演示:“我输入一个100平方米的3室2厅,系统预测出单价是2.3万元/平”,这种演示的冲击力比读PPT强得多。

3. 实操落地:手把手复现关键代码

3.1 Flask项目骨架怎么搭

项目结构建议按功能模块划分,这样后期维护和写文档都省心:

house_price_system/ ├── app.py # Flask入口 ├── config.py # 配置文件 ├── models/ # 数据模型与数据库定义 ├── spiders/ # 爬虫模块 ├── analysis/ # 数据清洗与特征工程 ├── ml_models/ # 机器学习训练与预测 ├── static/ # 前端静态文件(js/css) └── templates/ # HTML模板

先创建虚拟环境再安装依赖,这是很多新手最容易跳过的一步,直接在全局环境pip install,等到项目多了依赖就会互相冲突。我的习惯是:

# Python 3.8以上版本均可 pip install flask requests beautifulsoup4 pandas scikit-learn

安装完成后写一个最简单的app.py,先跑通Flask的hello world,再往里面加功能。宁可把基础流程拆成最小可用版本,也不要一把梭直接写几万行。

3.2 Requests爬虫核心配置与代码

写爬虫第一件事是准备一个“像正常人”的请求头。很多网站反爬的第一道防线就是检查User-Agent,如果请求头写着Python-requests,直接被拦下来毫不奇怪。我整理了一个请求头模板:

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": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", }

把住房页面的HTML抓下来后,重点是用BeautifulSoup解析列表项。这里有一个很实用的技巧:先用开发者工具检查元素定位到房源的列表容器,找到一个小区块的HTML作为样例,再编写解析逻辑。链家这类网站的房源卡片里,标题、面积、单价、位置都在不同的class层级,提取时要先用select定位外层,再逐层找字段:

import requests from bs4 import BeautifulSoup url = "https://example.com/house/sale/" response = requests.get(url, headers=headers, timeout=10) response.encoding = "utf-8" soup = BeautifulSoup(response.text, "html.parser") house_list = soup.select(".houseList .houseItem") for item in house_list[:20]: title = item.select_one(".title a").text.strip() if item.select_one(".title a") else "" area = item.select_one(".area").text.strip() if item.select_one(".area") else "" price = item.select_one(".totalPrice").text.strip() if item.select_one(".totalPrice") else "" print(title, area, price)

这个示例中我用了虚拟的选择器名称,实际编码时你要根据自己选定的数据源来替换。一手数据抓下来后你会发现,title里混着“急售”“满五唯一”等各种杂质,area字段写着“98平米”而不是数字,这正好对接下一阶段的清洗。

爬虫里面有个细节:一定要给请求设置Timeout。我一开始没设置,某天目标网站响应慢了,进程就一直挂着,最后整个爬虫像死了一样。加一个timeout=10,再结合retry机制,稳得多。

3.3 数据分析与特征工程代码

数据清洗阶段的工作量主要集中在Pandas操作上。我写了一个clean_data函数,把字符串清理和类型转换集中处理:

import pandas as pd import re def clean_data(df): df = df.drop_duplicates(subset=["title", "area", "price"]) df["area"] = df["area"].apply(lambda x: float(re.search(r"(\d+(\.\d+)?)", str(x)).group(1)) if re.search(r"(\d+(\.\d+)?)", str(x)) else None) df["price"] = df["price"].apply(lambda x: float(re.search(r"(\d+(\.\d+)?)", str(x)).group(1)) if re.search(r"(\d+(\.\d+)?)", str(x)) else None) df["unit_price"] = df["price"] / df["area"] df = df.copy() df["floor_level"] = df["floor"].apply(lambda x: "high" if "高" in str(x) else ("low" if "低" in str(x) else "mid")) df = pd.get_dummies(df, columns=["floor_level", "orientation", "decoration"]) numeric_cols = df.select_dtypes(include=["float64", "int64"]).columns for col in numeric_cols: df[col] = df[col].fillna(df[col].median()) return df

这个函数逻辑并不复杂,但它体现了一个很重要的经验:数据清洗没有标准答案,你需要先看数据长什么样,再写对应的处理规则。每一步清洗都应该有依据,这也是毕业设计文档里最好写出来的内容,因为你可以把“原始数据长什么样、清洗规则是什么、清洗后的结果差异在哪儿”全部对比展示。

另外,做特征工程时一定要保留一份原始数据备份。我吃过一次亏:清洗处理过了头,把楼层字段直接丢掉了,后面想重新做特征分析只能重跑爬虫,白白浪费了一个多小时。清洗代码和数据备份分开管理,后面会省很多功夫。

3.4 Scikit-learn训练与预测

训练模型的核心代码并不长,难的是你要理解每一步在干什么。先把特征列和目标列划分开,特征列用X表示,目标列(unit_price单平方米单价)用y表示:

from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import r2_score, mean_absolute_error features = [col for col in df.columns if col not in ["title", "price", "unit_price"]] X = df[features] y = df["unit_price"] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) model = RandomForestRegressor(n_estimators=100, max_depth=10, random_state=42) model.fit(X_train, y_train) y_pred = model.predict(X_test) r2 = r2_score(y_test, y_pred) mae = mean_absolute_error(y_test, y_pred) print(f"R2: {r2:.3f}, MAE: {mae:.3f}")

我在跑这个模型时一个明显的体验是:random_state这个参数千万别小看。如果你不固定它,每次运行得到的结果都不一样,答辩时评委让你重新跑一遍,出来一个完全不同的评估结果,场面会很尴尬。Random_state设置为42不仅为了可复现,也是给自己留一条“按固定结果讲方案”的后路。

还有一点,n_estimators并不是“越大越好”。我试过把它从100调到500,模型训练时间翻了将近三倍,但R2几乎没有提升,甚至还有轻微下降。树的数量够用就行,这个指标在回归问题上的边际收益递减得非常快。

3.5 Flask路由与可视化接口

Flask后端和机器学习模块之间需要做一个接口对接——当用户在页面表单里输入参数后,后端程序要构造一条特征向量,调用模型做预测,再把结果返回给前端展示。这个环节要特别注意:训练时特征列的顺序,和预测时输入特征列的顺序必须完全一致。否则会报特征名对齐错误,或者更隐蔽地,预测结果全部错乱。

from flask import Flask, render_template, request, jsonify import joblib app = Flask(__name__) model = joblib.load("model.pkl") @app.route("/") def index(): return render_template("index.html") @app.route("/predict", methods=["POST"]) def predict(): data = request.get_json() area = float(data["area"]) rooms = int(data["rooms"]) input_df = create_input_df(area, rooms) # 构造与训练一致的特征向量 result = model.predict(input_df)[0] return jsonify({"unit_price": round(result, 2)}) if __name__ == "__main__": app.run(debug=True)

这里有一个很小的关键点:训练好的模型不要每次预测时重新训练,用joblib.dump(model, "model.pkl")把模型保存下来,Flask启动时直接load进内存。这样程序的启动速度快得多,也更符合实际生产部署的思路。

前端ECharts部分则是从Flask提供一个数据接口开始,比如使用/api/avg_price_by_district接口返回各区域的均价列表,前端用axios或fetch获取后交给ECharts渲染。这种模式非常通用,以后你做任何数据可视化项目都能复用。

4. 实战踩坑:高频问题与排查清单

4.1 429 Too Many Requests:爬虫最常撞的墙

如果你跑爬虫时看到“exceeded retry limit, last status: 429 too many requests”这种报错,说明你的请求频率太高,被网站限流了。429是HTTP协议里的状态码,含义是“你在短时间内发出了太多请求”。我在做这个项目的头两天,就是被这个报错反复折腾。

解决思路有几个层次。最低成本的做法是每次请求之间加个随机延迟,比如time.sleep(random.uniform(1, 3)),让访问节奏更接近人的操作行为。再进一步是加大加到2到5秒的间隔,宁可多花几分钟也不要去撞防火墙。还有,尽量避开目标网站的高峰期,比如晚上八点到十点的访问高峰期,限流更严,爬取成功率更低。从根上看,除非你只是抓几十条练练手,否则在正式项目里大规模持续抓取某个网站前,一定要确认其条款是否允许。

4.2 请求头、Cookie与对端的反爬策略

有时候你加好了随机延时,结果还是被识别成爬虫,那大概率是请求头暴露了破绽。最常见的两种情况是:User-Agent要求版本太低,或者缺少Referer、Accept-Language这些常规浏览器必带的头参数。

我的请求头里会至少放5个字段:User-Agent、Referer、Accept、Accept-Language、Connection。多研究一下别人常用的一组请求头配置,比你自己瞎试要快得多。网站如果要求登录后才能看数据,你就需要在Requests会话里带上登录后的Cookie信息,session.get(url, headers=headers, cookies=cookies)这种方式基本都能行。

还要强调一下:反爬手段层出不穷,Header伪装只是第一步。如果对方加了验证码或滑块,最好不要尝试绕过——毕业设计没有必要在这个层面死磕。换个公开数据源或者申请一个API接口,都比“对抗反爬”更有价值。

4.3 中文乱码:一套编码问题通吃的标准操作

爬虫抓回来的页面,中文经常显示成乱码,这是新手最容易怀疑人生的时刻。出现乱码,十有八九是编码声明问题:网页可能是UTF-8,也可能是GBK/GB2312,你的解析进程却用了另一种编码去decode。

我的处理方式是先探测再解码,别死记硬背某个网站用什么编码。用requests库先获取response.content,再通过response.encoding判断网页声明的字符集,如果还是不放心,可以直接用response.encoding = response.apparent_encoding强制让程序根据内容推测。

另外,网页里声明的是charset=utf-8,但实际内容里混着GBK的大陆,这种“表里不一”的情况也不少见。遇到这种混编码问题,最省事的方法是抓回来以后统一用字符串的encode和decode做一次转换,把脏数据丢给Pandas清洗。

4.4 模型评估指标不理想怎么办

如果你跑完模型发现R2只有0.3甚至更低,先别急着换算法,我建议按这个顺序排查:特征是不是选得够全面、数据量够不够、目标列有没有异常值、模型参数太低还是太高。

我第二次跑这个项目时R2从0.6涨到0.82,关键就在于多加了两个特征:房龄和距离商圈的距离。这说明房价预测的精度大量来自于特征工程而非算法复杂度。另一个常见的坑是数据里存在成交价与挂牌价混在一起的情况,直接把这两类数据混训会让模型学不到有效的规律。处理思路是设置总价上限门槛,把异常高的价格记录排除掉。

如果数据量实在太少,比如只有三五百条,随机森林的效果也会受限。这时候可以考虑用交叉验证去用满数据集的利用效率,或者在展示时侧重于“分析”而不是“精准预测”。毕设项目里,结论严谨比数字漂亮更重要。

5. 答辩和文档里,这几个经验很加分

5.1 把“过程”讲出来,比背原理更有效

答辩时评委最喜欢问的一类问题是:“你在这个项目里遇到的最大困难是什么?你是怎么解决的?”如果你只是背教材上的概念,很容易被追问垮掉。但如果你能讲出一个具体的故事,比如在爬虫限流下怎么调参、特征工程阶段怎么发现字段不统一,评委不但不会难为你,反而会觉得你真的下了功夫。

我建议把项目中几个具体的“原始数据示例”截图保存下来,处理之后的效果也截图对比展示。数据清洗环节尤其这样做,一张清洗前后的对比表,信息量很可能超过你讲十分钟概念。

5.2 把数据合规的经验写进文档

在你的项目文档和答辩PPT里,至少要留一小段讲数据采集合规性。写明抓取频率、数据用途、是否遵守robots协议。这个细节在评委眼里代表你写代码之外还有工程素养,非常加分。

我在实际操作时是这么处理的:限定采集间隔,只记录数量有限的样本,并明确标注数据来源和采集时间,论文里也写明“本系统抓取的数据仅用于学术研究”。这种做法既让项目经得起查,也显得你考虑周全。

写在最后

我回头复盘这个项目时最大的体会是,它真正难的不是哪个单独的环节,而是把爬虫、清洗、建模、展示这四步串成一条完整的链。单独抓一个网页两小时就能写完,单独训练一个随机森林模型半小时也能出结果,可是中间的衔接环节最磨人——数据格式怎么统一、特征列怎么对齐、前端图表的数据结构怎么设计,这些才是真正容易被低估的工作量。

如果你正在准备毕业设计,我强烈建议你把项目拆成阶段目标,每完成一个阶段就留一次可复现的记录。不要一上来就追求“系统完整”,先把爬虫抓下来,再把清洗跑通,再单独训练出一个能输出数字的模型,最后才套上Flask和ECharts。每一步都有实实在在的产出,心态会稳很多。

最后再分享一个小技巧:训练好的模型文件model.pkl、清洗好的数据、以及那组固定好的random_state,打包压缩保存到三个不同的位置。答辩前如果你要重跑一遍模型,只要有这“三件套”,十分钟内就能把系统恢复到全胜状态。就这一点,关键时刻能帮你避免很多无谓的焦虑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询